servers, tools, and the protocol — how ai agents plug into your stack
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 itdescription— natural-language description Claude reads to pick the right toolinputSchema— 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 thecontent/isErrorpayload 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 forresponse["result"]["content"], not justresponse["content"].