promptdojo_

Wire an MCP tool — load tools from a server, not a registry — step 3 of 9

Two endpoints, one wrapping shape

The whole protocol is JSON-RPC 2.0. Two methods do all the work: tools/list and tools/call. Three things to lock in.

1. tools/list — discovery

{"jsonrpc":"2.0","id":1,"method":"tools/list"}{"jsonrpc":"2.0","id":1,"result":{"tools":[
    {"name":"search","description":"...","inputSchema":{...}},
    ...
]}}

Note inputSchema (camelCase, not input_schema). MCP is Python-team-staffed but the protocol is camelCase on the wire.

2. tools/call — invocation

{"jsonrpc":"2.0","id":42,"method":"tools/call",
   "params":{"name":"search","arguments":{"q":"ramen"}}}{"jsonrpc":"2.0","id":42,"result":{
    "content":[{"type":"text","text":"3 results"}],
    "isError":false
}}

Three fields to remember in the response:

  • result.content — a list of content blocks, like the Anthropic message content shape from chapter 13. Each block has a type (text, image, resource). For most tools you'll iterate the list and concatenate the text fields.
  • result.isError — boolean. True means the tool ran, the protocol succeeded, but the tool reported failure. The content in this case is the error message. Treat it like a tool that returned an error string.
  • The wrap matters. Beginners read response["content"] expecting the content list directly. The list is one level deeper — inside response["result"]. This is the bug step 6 fixes.

3. JSON-RPC errors are different from tool errors

There are TWO kinds of errors:

KindWhere it appearsWhen it fires
Protocol errorresponse["error"] (no result key)Method doesn't exist, malformed JSON-RPC
Tool errorresponse["result"]["isError"] == TrueTool ran but reported failure

Tool errors are normal — the user mistyped a query, an upstream API is down, the model passed bad args. Protocol errors are broken — your code or the server has a bug.

Step 7 fixes the bug where code ignores isError and treats every non-protocol-error response as success — feeding actual error messages to the model as successful tool results, which makes the agent confidently relay nonsense.

What this maps to in your registry

The bridge function turns each MCP tool definition into:

TOOLS["search"] = lambda **args: call_mcp("search", args)
SCHEMAS["search"] = mcp_tool["inputSchema"]

The agent loop doesn't change. It still does TOOLS[name](**args) and validate(args, SCHEMAS[name]). The only new thing is the closure inside call_mcp that makes the JSON-RPC request and unwraps result.content/isError.

read, then continue.