MCP tool calling looks a lot like OpenAI-style function calling, with one twist: the tool lives behind its own JSON-RPC protocol, and as of the current spec, that protocol is stateless — no session, no handshake, every request self-contained.
Example: "What's the weather in Tokyo?"
Below is what the flow looks like under the latest MCP spec version 2026-07-28. The LLM doesn't know current conditions, but knows a get_weather tool exists via MCP.
sequenceDiagram
actor User
box rgb(232,244,248) Client Environment
participant Client as MCP Client
(Agent)
end
box rgb(240,240,240) LLM Provider Environment
participant LLM as LLM API
end
box rgb(255,243,224) MCP Server Environment
participant MCPServer as MCP Server
(weather tool)
end
autonumber 1.1 0.1
Note over Client,MCPServer: Phase 1: Tool discovery (tools/list) — no handshake required
Client->>+MCPServer: tools/list (JSON-RPC request)
{ "jsonrpc": "2.0", "id": 1,
"method": "tools/list",
"params": { "_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "my-agent", "version": "1.0"} } } }
// HTTP headers: MCP-Protocol-Version, Mcp-Method: tools/list
MCPServer-->>-Client: tools/list result
{ "jsonrpc": "2.0", "id": 1,
"result": { "resultType": "complete",
"tools": [{ "name": "get_weather",
"description": "Get current weather for a location",
"inputSchema": { "type": "object",
"properties": { "location": {"type": "string"} },
"required": ["location"] } }],
"ttlMs": 300000, "cacheScope": "public" } }
Client->>Client: Cache tool list for ttlMs,
convert inputSchema →
LLM function-calling format
autonumber 2.01 0.01
Note over User,MCPServer: Phase 2: Runtime — user request
User->>+Client: "What's the weather in Tokyo?"
Client->>+LLM: POST /v1/chat/completions
{ "model": "gpt-4o",
"messages": [{ "role": "user",
"content": "What's the weather in Tokyo?" }],
"tools": [{ "type": "function",
"function": { "name": "get_weather",
"description": "Get current weather for a location",
"parameters": { "type": "object",
"properties": { "location": {"type": "string"} },
"required": ["location"] } } }] }
LLM->>LLM: Decide tool use is needed
(extract location="Tokyo")
LLM-->>-Client: Response (finish_reason: tool_calls)
{ "choices": [{ "message": { "role": "assistant",
"content": null,
"tool_calls": [{ "id": "call_abc123",
"type": "function",
"function": { "name": "get_weather",
"arguments": "{\"location\":\"Tokyo\"}" } }] },
"finish_reason": "tool_calls" }] }
Client->>Client: Parse tool_calls,
map function name →
MCP tool + server
Client->>+MCPServer: tools/call (JSON-RPC request)
{ "jsonrpc": "2.0", "id": 2,
"method": "tools/call",
"params": { "name": "get_weather",
"arguments": { "location": "Tokyo" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "my-agent", "version": "1.0"} } } }
// HTTP headers: Mcp-Method: tools/call, Mcp-Name: get_weather
MCPServer->>MCPServer: Call external weather API
(e.g. OpenWeatherMap)
MCPServer-->>-Client: tools/call result
{ "jsonrpc": "2.0", "id": 2,
"result": { "resultType": "complete",
"content": [{ "type": "text",
"text": "{\"temp\": 22, \"condition\": \"clear\", \"humidity\": 60}" }],
"isError": false } }
Client->>Client: Extract result content,
convert MCP result →
tool-role message
Client->>+LLM: POST /v1/chat/completions (continued)
{ "messages": [
{"role": "user", "content": "What's the weather in Tokyo?"},
{"role": "assistant", "tool_calls": [...]},
{ "role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temp\": 22, \"condition\": \"clear\"}" } ] }
LLM->>LLM: Integrate tool result,
generate natural language answer
LLM-->>-Client: Response (finish_reason: stop)
{ "choices": [{ "message": { "role": "assistant",
"content": "The weather in Tokyo is clear with a temperature of 22°C." },
"finish_reason": "stop" }] }
Client->>Client: response.choices[0].message.content
Client-->>-User: "The weather in Tokyo is clear
with a temperature of 22°C."
Key points
Phase 1: Tool discovery
- No handshake. The old
initialize/initializedexchange is gone. The client sendstools/listdirectly; protocol version and client info travel in the_metafield and theMCP-Protocol-Versionheader. - Schema translation is where vendor portability lives. The client converts MCP's
inputSchemainto the LLM provider's function-calling format (tools: [...]for OpenAI, etc.). One MCP server, any LLM. ttlMs/cacheScopein the response tells the client how long it can reuse the tool list without re-fetching.
Phase 2: Runtime
- The LLM returns
finish_reason: tool_callsinstead of a final answer when it needs external data. The client parses the call, maps it to the right MCP tool, and dispatches atools/callrequest. - Stateless by design. Every
tools/callis self-contained — arguments +_meta, routed viaMcp-Method/Mcp-Nameheaders. No session state means any instance behind a load balancer can handle the request. - The MCP result comes back in a
contentarray (resultType: complete). The client reshapes it into arole: "tool"message and resends the conversation to get the final answer.
A production implementation should also handle tools/list pagination, distinguish protocol errors from tool errors (isError: true), and prompt the user before executing sensitive tool calls.
This covers the core MCP call cycle. Authentication is covered in the next blog post.
I hope it helps.
0 comments:
Post a Comment