promptdojo_

Servers, tools, and the protocol — how AI agents plug into your stack — step 3 of 9

The two methods you actually use: tools/list and tools/call

The MCP protocol has more verbs than these — initialize (the handshake), resources/list and resources/read (for the resources primitive), prompts/list and prompts/get (for the prompts primitive), and notifications for things like progress and log events. In 95% of real agent work, only two matter:

tools/list

The client calls this once on connect to discover what the server can do. The response is {"tools": [...]}, where each tool has:

  • name — what you call when you want to invoke it
  • description — natural-language description Claude reads to pick the right tool
  • inputSchema — JSON Schema for the arguments

The model never sees the server itself. It only sees the list of tools the client forwards from tools/list. The description and schema are the model's user manual for that tool.

tools/call

When Claude decides to use create_task, the client makes a tools/call request with:

{"name": "create_task", "arguments": {"title": "ship docs"}}

The server runs whatever it runs — hits an API, queries a DB, writes a file — and returns:

{
  "content": [{"type": "text", "text": "Created task #42: ship docs"}],
  "isError": false
}

That content array is the same shape as a Claude assistant message. That's not coincidence — it lets the model treat tool output as just another piece of context, no parsing dance required.

Why isError matters

When something goes wrong, the server still returns a normal response, just with "isError": true and an error message in the text content. This lets the model recover — it can read "permission denied" and try a different approach. Crashing the connection would force the whole agent loop to start over.

Run the editor. We render a fake tools/call response — the same shape a real one would have.

Wire-format note: real JSON-RPC responses also include a "jsonrpc": "2.0" field and an "id" matching the request, and they wrap the content / isError payload inside a top-level "result" key. We've stripped those here so the shape that matters for your code (the result body) reads cleanly. When you parse a real MCP response, reach for response["result"]["content"], not just response["content"].

read, then continue.