Chapter 8 · one tool, any model
Every framework has its own tool format.
OpenAI schemas, Gemini FunctionDeclarations, the next one, the next.
The tool underneath is identical every time. You keep rewriting the wiring.
Write the tool once; any client, any model, can reach it.
if method == "tools/list":
result = {"tools": _TOOLS}
elif method == "tools/call":
payload = call(params["name"], params["arguments"])
result = {"content": [{"type": "text", "text": json.dumps(payload)}]}
Reads JSON-RPC from stdin, writes to stdout. That's the stdio transport.
self._req("initialize", {"protocolVersion": "2025-06-18",
"capabilities": {}, "clientInfo": {...}})
self._send({"jsonrpc": "2.0", "method": "notifications/initialized"})
initialize → reply → initialized notification. Then you can call.
# OpenAI: MCP tool -> function tool (JSON Schema is already the right shape)
{"type": "function", "name": t["name"], "parameters": t["inputSchema"]}
# Gemini: MCP tool -> FunctionDeclaration
types.FunctionDeclaration(name=t["name"], parameters=to_schema(t["inputSchema"]))
Only the translation changes. The server doesn't move.
tools/call)resources/read)prompts/get)Actions, context, instructions. A server's whole vocabulary.
doc:// URIsOne shared mcp_client.py. The servers are the interesting part.
Function calling: the format of a single tool call.
MCP: the whole lifecycle — discovery, invocation, results, errors — across many tools and many servers, model-agnostic.
Complementary, not competing.
MCP is JSON-RPC with a handshake and three verbs.
Build a tool once, publish it as a server, and every
MCP client — Claude Desktop, your IDE, this blog's /mcp —
can reach it. Next: embeddings, and the road to retrieval.