symfonic.tools.mcp.routing¶
routing ¶
How a discovered MCP tool is named and where a call to it goes.
Split out of :mod:symfonic.tools.mcp.provider because it is the whole of
T4.2.2's HIGH finding and it is worth reading on its own: the provider used to
keep dict[str, tuple[connection, definition]] keyed on the bare tool name,
so two servers advertising search shared one slot and the second one
registered inherited the first one's traffic — arguments included — with
nothing logged.
Two properties close that, and they are separate:
- Routing is pinned at discovery. A :class:
ToolRouteholds the connection the tool was found on. Dispatch is a lookup of a route, never a lookup of a server by name, so there is no later payload that can redirect a call. - Names are disambiguated on collision. A name advertised by more than one
server becomes
"{server}.{tool}"for every server advertising it, so neither party keeps the ambiguous name and no server is quietly favoured.
The naming rule is deliberately "on collision" rather than always. Namespacing
unconditionally is the cleaner design — it is what
:class:~symfonic.capabilities.extensions.McpExtensionAdapter does — but this
module is the shipped, publicly-exported path, and renaming every tool would
break prompts and allowlists that adopters already have, in a release that is
not a MAJOR. Disambiguating only what is genuinely ambiguous fixes the finding
and leaves every non-colliding deployment byte-identical.
ToolRoute
dataclass
¶
ToolRoute(server: str, connection: MCPServerConnection, definition: MCPToolDefinition, wire_name: str)
One discovered tool: what it is called, and where its calls go.
namespaced ¶
resolve_routes ¶
resolve_routes(catalogues: Mapping[str, tuple[MCPServerConnection, Sequence[MCPToolDefinition]]]) -> dict[str, ToolRoute]
Build the routing table for every server's current catalogue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalogues
|
Mapping[str, tuple[MCPServerConnection, Sequence[MCPToolDefinition]]]
|
Server label -> (connection, the definitions it advertises). Insertion order is preserved in the result, so discovery keeps returning tools in server-registration order. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ToolRoute]
|
Exposed tool name -> the route that serves it. |
Source code in src/symfonic/tools/mcp/routing.py
tool_name_of ¶
The tool name on a discovery row, or None when it has no usable one.
Total by design. The shipped code indexed tool_data["name"] and let a
KeyError escape, and because discovery catches Exception per
server, one malformed row discarded every other tool that server
advertised — a whole integration lost to one bad entry. Returning None
lets the caller drop the row and keep its siblings.