Discover tools over the wire: a JSON-RPC inspector for MCP

Part 1's tools were a hardcoded Python dict baked into one process. The Model Context Protocol turns "what tools do you have and how do I call them" into a wire protocol. A SERVER advertises capabilities; a HOST discovers and calls them over JSON-RPC. Step through the exact frames, then connect two servers and watch their tools merge into one run-time palette.

The world: a host speaking protocolVersion 2025-06-18 to a support-server (capabilities tools, resources, prompts) and a catalog-server (tools only). The handshake negotiates the protocol; tools/list returns JSON inputSchemas; the discovered schema feeds Part 1's validator unchanged. No dict was imported.
verified The default is an in-process JSON-RPC shim: the host calls server.handle(request) directly, but every frame below is a real JSON-RPC message printed verbatim, the exact bytes that would cross a socket. The real stdio subprocess path (two OS processes, no network) is shown at the bottom as an illustrative transcript, not a frozen run.
1 · The wire inspector: step the JSON-RPC frames
id 1initialize id 2tools/list id 3tools/call
wire frames · host → support-server (JSON-RPC 2.0) frame 1 / 3
2 · Host multiplex: connect servers, watch the palette assemble
one host, N servers · capability negotiation 0 / 2 connected
support-server offline
tools resources prompts
catalog-server offline
tools
run-time tool palette (union across servers) 0 tools
3 · The discovered schema drives Part 1's validator (over the wire)
process_refund inputSchema · validate_against_schema()
inputSchema (arrived from tools/list, not hardcoded) The schema is just data. Part 1's validator reads it the same way whether it was hand-written or discovered over MCP.
Tools are no longer a hardcoded dict. The same inputSchema Part 1 hand-wrote now arrives over the wire from tools/list, and feeds the same validator and controller unchanged. One host MULTIPLEXES many servers: the agent's action space is the union of support-server and catalog-server, assembled at run time, not at author time. A multi-hop task draws search_products from catalog and process_refund from support transparently, off one palette.
The real transport (illustrative, not executed here)
server as a subprocess over stdio
Illustrative only. Two local processes, no network. NOT a frozen, verified run; the verified run above uses the in-process shim. The frames are the same, now crossing a pipe between two OS processes.
$ python mcp_server.py            # process A: reads JSON-RPC from stdin
host -> server (stdin):  {"jsonrpc":"2.0","id":1,"method":"initialize",...}
server -> host (stdout): {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":...}}
host -> server (stdin):  {"jsonrpc":"2.0","id":2,"method":"tools/list"}
server -> host (stdout): {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
(same frames as above, now crossing a pipe between two OS processes)
How we keep it honest: the in-process shim is fully reproducible and every frame is a verbatim JSON-RPC message. The protocolVersion string and SDK shapes move fast; only the transport and version string would need edits for a live run. The real subprocess-over-stdio path, like the generate() LLM swap in earlier parts, is one flag away. The three primitives (tools, resources, prompts) are shown in full in the companion figures.

host / request   server / response   schema   connected / OK   rejected / stdio