@@ -186,10 +186,10 @@ def _wrap_call_tool(
186186 async def handler (req : Any ) -> Any :
187187 name = req .params .name
188188 arguments = dict (req .params .arguments or {})
189- strip , model_ours = (
189+ strip , model_ours , input_schema = (
190190 await _standalone_ownership (data , high_level , name , req .params .meta )
191191 if strip_injected
192- else (set (), data .tool_model_parameter_injected .get (name ))
192+ else (set (), data .tool_model_parameter_injected .get (name ), None )
193193 )
194194 client_name , client_version = _client_info (server )
195195 protocol_version = _protocol_version (server )
@@ -212,6 +212,7 @@ async def handler(req: Any) -> Any:
212212 client_version = client_version ,
213213 protocol_version = protocol_version ,
214214 extra = {"session_id" : mcp_session_id , "ctx" : _request_context (server )},
215+ input_schema = input_schema ,
215216 )
216217
217218 if lifecycle .is_missing_capability and (
@@ -565,10 +566,11 @@ def _tool_lookup_not_found_errors() -> Tuple[type, ...]:
565566
566567async def _standalone_ownership (
567568 data : MCPAnalyticsData , high_level : Any , name : str , meta : Any
568- ) -> Tuple [set , Optional [bool ]]:
569+ ) -> Tuple [set , Optional [bool ], Optional [ Dict [ str , Any ]] ]:
569570 """Ownership of the injected arguments on jlowin's standalone FastMCP: the
570571 keys to strip before it validates the call, and whether ``llm_model`` is
571- ours (``None`` when nothing can say).
572+ ours (``None`` when nothing can say). The third item is the trusted schema
573+ for input-name analytics, or ``None`` when middleware can change dispatch.
572574
573575 Only keys injected under the current options are candidates. ``context``
574576 and ``conversation_id`` are stripped unless the registered schema (or,
@@ -580,17 +582,44 @@ async def _standalone_ownership(
580582 stays and is still read (posthog-js ADR-0011).
581583 """
582584 try :
583- declared , model_injectable = await _registry_view (high_level , name , meta )
585+ declared , model_injectable , input_schema = await _registry_view (
586+ high_level , name , meta
587+ )
584588 model_ours = data .tool_model_parameter_injected .get (name , model_injectable )
585589 if _dispatch_can_differ (high_level ):
586590 model_ours = False
591+ input_schema = await _listed_input_schema (high_level , name , meta )
587592 except Exception : # noqa: BLE001 - ownership inference must never prevent dispatch
588- declared , model_ours = None , None
593+ declared , model_ours , input_schema = None , None , None
589594 candidates = _injected_keys (data )
590595 strip = {k for k in candidates - {"llm_model" } if k not in (declared or set ())}
591596 if "llm_model" in candidates and model_ours :
592597 strip .add ("llm_model" )
593- return strip , model_ours
598+ return strip , model_ours , input_schema
599+
600+
601+ def _tool_schema_view (
602+ high_level : Any , tool : Any
603+ ) -> Tuple [Optional [set ], Optional [bool ], Optional [Dict [str , Any ]]]:
604+ if tool is None :
605+ return None , None , None
606+ schema = getattr (tool , "parameters" , None )
607+ if isinstance (schema , dict ):
608+ dereferenced = _server_dereferences (high_level )
609+ declared , injectable = _schema_view (schema , dereferenced = dereferenced )
610+ return (
611+ declared ,
612+ injectable ,
613+ schema ,
614+ )
615+ fn = getattr (tool , "fn" , None )
616+ if fn is None :
617+ return set (), True , None
618+ try :
619+ declared = {k for k in _INJECTED_KEYS if k in inspect .signature (fn ).parameters }
620+ except Exception : # noqa: BLE001 - introspection is best-effort
621+ return set (), True , None
622+ return declared , "llm_model" not in declared , None
594623
595624
596625def _injected_keys (data : MCPAnalyticsData ) -> set :
@@ -609,35 +638,24 @@ def _injected_keys(data: MCPAnalyticsData) -> set:
609638
610639async def _registry_view (
611640 high_level : Any , name : str , meta : Any
612- ) -> Tuple [Optional [set ], Optional [bool ]]:
641+ ) -> Tuple [Optional [set ], Optional [bool ], Optional [ Dict [ str , Any ]] ]:
613642 """What the registered tool says about the injected keys: which of
614643 ``_INJECTED_KEYS`` it declares itself, and whether a listing would have
615644 injected ``llm_model`` into its schema (the same test the listing applies,
616645 on the schema as the client would see it). Read from the schema (a ``Tool``
617646 subclass may have no function) else the signature. The registry is read
618647 directly, never through middleware, so a cold instance answers without a
619648 listing and rate limiters are not charged. ``(None, None)`` when the
620- registry has no such tool or cannot be read."""
649+ registry has no such tool or cannot be read. The third item is the tool's
650+ input schema when the registry supplies one."""
621651 try :
622652 tool = await _registered_tool (high_level , name , meta )
623653 except Exception as error : # noqa: BLE001 - introspection is best-effort
624654 if not isinstance (error , _tool_lookup_not_found_errors ()):
625655 warn_ownership_lookup_failed (name , error )
626- return set (_INJECTED_KEYS ), None
627- return None , None
628- if tool is None :
629- return None , None
630- schema = getattr (tool , "parameters" , None )
631- if isinstance (schema , dict ):
632- return _schema_view (schema , dereferenced = _server_dereferences (high_level ))
633- fn = getattr (tool , "fn" , None )
634- if fn is None :
635- return set (), True
636- try :
637- declared = {k for k in _INJECTED_KEYS if k in inspect .signature (fn ).parameters }
638- except Exception : # noqa: BLE001 - introspection is best-effort
639- return set (), True
640- return declared , "llm_model" not in declared
656+ return set (_INJECTED_KEYS ), None , None
657+ return None , None , None
658+ return _tool_schema_view (high_level , tool )
641659
642660
643661async def _registered_tool (high_level : Any , name : str , meta : Any ) -> Any :
@@ -651,6 +669,29 @@ async def _registered_tool(high_level: Any, name: str, meta: Any) -> Any:
651669 return await high_level .get_tool (name , version = VersionSpec (eq = version ))
652670
653671
672+ async def _listed_input_schema (
673+ high_level : Any , name : str , meta : Any
674+ ) -> Optional [Dict [str , Any ]]:
675+ """Return the schema that middleware advertises for the current request."""
676+ try :
677+ version = _requested_tool_version (meta )
678+ candidates = [
679+ tool for tool in await high_level .list_tools () if tool .name == name
680+ ]
681+ if version is not None :
682+ candidates = [
683+ tool
684+ for tool in candidates
685+ if str (getattr (tool , "version" , "" )) == version
686+ ]
687+ tool = candidates [- 1 ] if candidates else None
688+ schema = getattr (tool , "parameters" , None )
689+ except Exception as error : # noqa: BLE001 - analytics must not break dispatch
690+ log (f"PostHog MCP: could not resolve schema for tool { name !r} - { error } " )
691+ return None
692+ return schema if isinstance (schema , dict ) else None
693+
694+
654695def _requested_tool_version (meta : Any ) -> Optional [str ]:
655696 """Ownership must follow dispatch. Only a FastMCP that exposes the
656697 ``_meta`` version extractor its own dispatch uses honours a pinned
0 commit comments