Lesson 12 / 31

Errors, Validation and Recoverable Failures

Distinguish protocol errors from tool errors and help the model recover.

Two kinds of failure

There are two places things go wrong. Protocol errors are JSON-RPC errors such as an unknown method (-32601) or malformed request, handled by the SDK. Tool execution errors happen inside a valid call (bad arguments, a downstream service failing); they should be reported in the tool result with isError: true and a clear message so the model can read it and correct course ("order_id must be exactly 6 digits") instead of the whole run failing. Validate input with the schema and in code; never trust model-supplied arguments, especially for file paths, SQL, shell commands and URLs. Do not leak stack traces, secrets or internal hostnames in error text.

Both failure kinds on the real server, run

I ran this against the real server shown earlier, using the official mcp Python SDK 2.2.0 in a virtual environment; the client side is raw JSON-RPC over stdio with no SDK. Save the server as server.py in the same folder. SDK names and defaults change between versions, so check the current documentation. An invalid id returns a normal result whose text is a clear error message the model can act on. A missing argument fails schema validation and comes back with isError true. An unknown method produces a protocol error with code -32601.

import json, subprocess, sys

proc = subprocess.Popen([sys.executable, "server.py"], stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)

def send(msg):
    proc.stdin.write(json.dumps(msg) + "\n"); proc.stdin.flush()

def call(id_, method, params=None):
    send({"jsonrpc": "2.0", "id": id_, "method": method, **({"params": params} if params is not None else {})})
    return json.loads(proc.stdout.readline())

init = call(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
                              "clientInfo": {"name": "demo-client", "version": "0.1"}})
print("server:", init["result"]["serverInfo"]["name"], "| protocol:", init["result"]["protocolVersion"])
print("capabilities:", sorted(init["result"]["capabilities"]))
send({"jsonrpc": "2.0", "method": "notifications/initialized"})     # a notification: no id, no reply

tools = call(2, "tools/list")["result"]["tools"]
print("tools:", [t["name"] for t in tools])
print("input schema:", json.dumps(tools[0]["inputSchema"]))

ok = call(3, "tools/call", {"name": "get_order_status", "arguments": {"order_id": "481516"}})
print("call ok     :", ok["result"]["content"][0]["text"], "| isError:", ok["result"].get("isError"))
bad = call(4, "tools/call", {"name": "get_order_status", "arguments": {"order_id": "12"}})
print("call bad id :", bad["result"]["content"][0]["text"])
missing = call(5, "tools/call", {"name": "get_order_status", "arguments": {}})
print("missing arg :", "isError" , missing["result"].get("isError"), "|", missing["result"]["content"][0]["text"][:60].replace(chr(10), " "))
unknown = call(6, "no/such/method")
print("unknown     :", unknown["error"]["code"], unknown["error"]["message"])

res = call(7, "resources/list")["result"]["resources"]
print("resources:", [r["uri"] for r in res])
print("read:", call(8, "resources/read", {"uri": "policy://refunds"})["result"]["contents"][0]["text"])
pr = call(9, "prompts/get", {"name": "triage", "arguments": {"ticket": "I was charged twice"}})
print("prompt:", pr["result"]["messages"][0]["content"]["text"])
proc.stdin.close(); proc.wait()

Output:

server: demo-orders | protocol: 2025-06-18
capabilities: ['experimental', 'prompts', 'resources', 'tools']
tools: ['get_order_status']
input schema: {"properties": {"order_id": {"title": "Order Id", "type": "string"}}, "required": ["order_id"], "type": "object", "title": "get_order_statusArguments"}
call ok     : {
  "order_id": "481516",
  "status": "shipped"
} | isError: False
call bad id : {
  "error": "order_id must be exactly 6 digits"
}
missing arg : isError True | Error executing tool get_order_status: 1 validation error fo
unknown     : -32601 Method not found
resources: ['policy://refunds']
read: Refunds above 5000 rupees need approval.
prompt: Classify this ticket as billing, technical or other: I was charged twice

Quick check: How should a tool report bad arguments to help the model?

  • By crashing the server
  • With a clear, specific message in the tool result
  • With a full stack trace
  • By returning nothing
Answer

With a clear, specific message in the tool result — Actionable messages let the model self-correct on the next step.