← chapter

MCP servers and the Model Context Protocol

Chapter 8 · one tool, any model

The hour

The problem

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.

MCP in one breath

Write the tool once; any client, any model, can reach it.

The server (no model in sight)

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.

The handshake

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.

One server, any model

# 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.

Three primitives

Actions, context, instructions. A server's whole vocabulary.

Put it to work — three apps

One shared mcp_client.py. The servers are the interesting part.

Function calling vs MCP

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.

Takeaway

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.