2222
2323import mcp .types as mcp_types
2424
25+ from ._argument_ownership import (
26+ cache_listed_tool_ownership ,
27+ resolve_lowlevel_tool_ownership ,
28+ )
2529from ._context_parameters import is_context_enabled , schema_has_param
2630from ._conversation_id import build_prompt_back
2731from ._event_types import MCPAnalyticsEventType
2832from ._instrumentation import (
2933 advertised_tool_names ,
3034 apply_virtual_tool_injection ,
3135 collect_listed_tools ,
36+ copy_tools_list_result ,
3237 extract_tools ,
3338 is_first_listing_page ,
3439 mutate_tool_schema ,
5459
5560
5661def instrument_low_level (server : Any , data : MCPAnalyticsData ) -> None :
57- """Instrument a raw ``mcp.server.Server``. ``context`` is injected as an
58- optional schema property and NOT stripped — that schema is also the call's
59- validation schema, and a typical ``(name, arguments)`` handler ignores extra keys."""
62+ """Instrument a raw ``mcp.server.Server``.
63+
64+ The adapter removes arguments that a listing or resolver proves PostHog
65+ owns. Unknown arguments pass through unchanged.
66+ """
6067 data .server_name = getattr (server , "name" , None )
6168 data .server_version = getattr (server , "version" , None )
6269 _wrap_call_tool (server , data , strip_injected = False )
@@ -186,11 +193,21 @@ def _wrap_call_tool(
186193 async def handler (req : Any ) -> Any :
187194 name = req .params .name
188195 arguments = dict (req .params .arguments or {})
189- strip , model_ours , input_schema = (
190- await _standalone_ownership (data , high_level , name , req .params .meta )
191- if strip_injected
192- else (set (), data .tool_model_parameter_injected .get (name ), None )
193- )
196+ parameter_ownership = None
197+ if strip_injected :
198+ strip , model_ours , input_schema = await _standalone_ownership (
199+ data , high_level , name , req .params .meta
200+ )
201+ else :
202+ parameter_ownership , input_schema = await resolve_lowlevel_tool_ownership (
203+ data , name
204+ )
205+ strip = set (parameter_ownership or ())
206+ model_ours = (
207+ "llm_model" in parameter_ownership
208+ if parameter_ownership is not None
209+ else data .tool_model_parameter_injected .get (name )
210+ )
194211 client_name , client_version = _client_info (server )
195212 protocol_version = _protocol_version (server )
196213 mcp_session_id = _mcp_session_id (server )
@@ -213,6 +230,7 @@ async def handler(req: Any) -> Any:
213230 protocol_version = protocol_version ,
214231 extra = {"session_id" : mcp_session_id , "ctx" : _request_context (server )},
215232 input_schema = input_schema ,
233+ analytics_owned_parameters = parameter_ownership ,
216234 )
217235
218236 if lifecycle .is_missing_capability and (
@@ -327,6 +345,7 @@ async def handler(req: Any) -> Any:
327345
328346
329347def _inject_tool_schemas (
348+ server : Any ,
330349 data : MCPAnalyticsData ,
331350 tools : list ,
332351 * ,
@@ -345,6 +364,7 @@ def _inject_tool_schemas(
345364 verdicts : Dict [str , bool ] = {}
346365 for tool in tools :
347366 schema = getattr (tool , "inputSchema" , None )
367+ cache_listed_tool_ownership (data , tool , schema_attribute = "inputSchema" )
348368 mutate_tool_schema (
349369 data ,
350370 tool ,
@@ -363,6 +383,11 @@ def _inject_tool_schemas(
363383 # Which one dispatches is unknown, so the strip fails closed.
364384 data .tool_model_parameter_injected [tool .name ] = False
365385
386+ cache = getattr (server , "_tool_cache" , None )
387+ if isinstance (cache , dict ):
388+ for tool in tools :
389+ cache [tool .name ] = tool
390+
366391
367392def _wrap_list_tools (
368393 server : Any ,
@@ -404,34 +429,36 @@ async def probe_raw_tool_names(_ctx: Any = None) -> Optional[Set[str]]:
404429 "registering your handlers."
405430 )
406431 return None
407- result = await original (mcp_types .ListToolsRequest (method = "tools/list" ))
432+ result = copy_tools_list_result (
433+ await original (mcp_types .ListToolsRequest (method = "tools/list" ))
434+ )
408435 tools = extract_tools (result )
409- # `original` is usually the SDK's own list_tools decorator, which rebuilds
410- # `Server._tool_cache` from these un-injected schemas every time it runs.
411- # That cache is what the SDK validates real tool arguments against, so
412- # without re-injecting here the next real call is rejected for sending the
413- # `context` we advertised. Same reason the `req is None` branch below
414- # injects.
436+ # Resolve ownership from the host listing. Apply injection only to the
437+ # copy so a shared host descriptor remains original.
415438 _inject_tool_schemas (
416- data , tools , context_required = context_required , high_level = high_level
439+ server ,
440+ data ,
441+ tools ,
442+ context_required = context_required ,
443+ high_level = high_level ,
417444 )
418445 return advertised_tool_names (tools )
419446
420447 data .raw_tool_names_probe = probe_raw_tool_names
421448
422449 async def handler (req : Any ) -> Any :
423450 # The server calls the handler with None to populate its tool cache.
424- # Skip analytics there — but still inject, because that cache is the
425- # schema the SDK validates calls against. This adapter advertises
426- # `context`/`conversation_id` without stripping them, so a cache built
427- # from un-injected schemas rejects the very arguments we told the agent
428- # to send ("Additional properties are not allowed") on any tool with
429- # `additionalProperties: false`.
451+ # Skip analytics there, but return the same injected schema as a client
452+ # listing.
430453 if req is None :
431- result = await original (req )
454+ result = copy_tools_list_result ( await original (req ) )
432455 tools = extract_tools (result )
433456 _inject_tool_schemas (
434- data , tools , context_required = context_required , high_level = high_level
457+ server ,
458+ data ,
459+ tools ,
460+ context_required = context_required ,
461+ high_level = high_level ,
435462 )
436463 return result
437464
@@ -464,7 +491,7 @@ async def handler(req: Any) -> Any:
464491
465492 start = time .monotonic ()
466493 try :
467- result = await original (req )
494+ result = copy_tools_list_result ( await original (req ) )
468495 except Exception as error :
469496 await lifecycle .record_error (error , (time .monotonic () - start ) * 1000 )
470497 raise
@@ -481,7 +508,11 @@ async def handler(req: Any) -> Any:
481508 )
482509
483510 _inject_tool_schemas (
484- data , tools , context_required = context_required , high_level = high_level
511+ server ,
512+ data ,
513+ tools ,
514+ context_required = context_required ,
515+ high_level = high_level ,
485516 )
486517
487518 result = apply_virtual_tool_injection (
0 commit comments