Error Handling
Detect and branch on the three MCP failure layers — HTTP/auth, JSON-RPC protocol errors, and tool isError results — and handle timeouts robustly.
A tools/call can fail in three structurally different ways, and each one shows up
in a different part of the response. A client that treats them as one bucket will
either crash on a recoverable failure or silently swallow a malformed request. This
guide shows how to detect each layer and branch on them in the right order; for the
exhaustive list of codes and messages, see the
Error codes reference.
Overview
When you call a tool over /mcp, the request passes through three checkpoints
before you get a result back:
- HTTP / auth layer — the shared HTTP server validates the origin and the
Authorizationheader before any JSON-RPC is parsed. Failures here surface as the JSON-RPC auth error-32010(the body still parses as JSON-RPC). - JSON-RPC protocol layer — the connector parses the envelope and resolves the
method. A malformed body, an unknown method, or bad params produce a JSON-RPC
errorobject with noresult. - Tool execution layer — the envelope was valid and a tool ran, but the
operation failed (reserved channel, missing agent, timeout). The response is a
normal
resultcarryingisError: true.
The decisive split is between layers 2 and 3: a protocol error means the tool never ran, so retrying the same request fails identically — the fix is to correct the request. A tool error means a valid request ran and failed for an operational reason, which may be transient (a timeout) and worth retrying.
Every JSON-RPC response — success or error — comes back as HTTP 200. The outcome is encoded in the JSON body, not the status line. Do not branch on the HTTP status code for JSON-RPC calls.
How it works
Branch on the layers in a fixed order so a recoverable tool failure is never mistaken for a protocol failure, and vice versa.
A tools/call clears three checkpoints; each failure surfaces in a different field, so inspect them in order.
Layer 1 — HTTP and auth
Authentication and origin checks run in the shared HTTP server, ahead of JSON-RPC
parsing. When JWT auth is enabled and the Authorization: Bearer header is missing
or invalid, the /mcp endpoint returns the JSON-RPC auth error -32010 — the
response is still valid JSON-RPC, so you read it the same way as any other error
object:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32010,
"message": "authentication failed"
}
}Origin validation can also reject the request before a tool runs. Auth and origin
behavior are shared across all connectors and documented once in
Auth & security; the MCP-specific
validateOrigin/TrustedOrigins rules are covered in
Authentication.
Layer 2 — JSON-RPC protocol errors
Protocol errors mean the envelope itself was rejected: malformed JSON, the wrong
Content-Type, an unknown method, or invalid params. They appear in the error
field and there is no result field at all.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found"
}
}| Code | Trigger |
|---|---|
-32700 | Malformed JSON body, or wrong Content-Type (e.g. text/plain instead of application/json) |
-32600 | Empty method field, or jsonrpc is not "2.0" |
-32601 | Unknown method name (e.g. tools/unknown) |
-32602 | params is not an object, or required tool arguments are missing |
Retrying an identical request after a protocol error fails the same way — correct the request instead of retrying. The full catalog, with response examples for each code, lives in the Error codes reference.
Layer 3 — tool execution errors
A tool error means the envelope was valid and a tool was invoked, but the operation
failed. The response is structurally a success — HTTP 200, a populated
result — with isError: true and a human-readable cause in
result.content[].text. A client that only checks the error field treats this as
a successful call, so always inspect result.isError as a second step.
{
"jsonrpc": "2.0",
"id": 9,
"result": {
"content": [{ "type": "text", "text": "Command timed out: no subscriber on channel 'example-commands' within 10s" }],
"isError": true
}
}Common tool-level failures and the tools that raise them:
| Failure | Affected tools |
|---|---|
Reserved-channel rejection (channel starts with _AGENTS_.) | queue_send, events_publish, events_store_publish, command_send, query_send |
| Non-existent agent | agent_info, agent_send, agent_query |
| Timeout exceeded / no subscriber | command_send, query_send, agent_send, agent_query |
| Non-existent channel | channel_info |
Channels beginning with _AGENTS_. are reserved for the agent bridge, so any
publish or request tool targeting one is rejected at the tool layer. To reach an
agent, use the agent-bridge tools
(agent_send, agent_query) instead.
Handling timeouts
The synchronous tools — command_send, query_send, agent_send, agent_query —
block until a responder replies or the timeout elapses. When no responder is
listening, the call does not raise a protocol error; it returns a normal result
with isError: true and a timeout message. Detect a timeout by reading
result.isError, not by catching a transport exception.
- The per-call timeout is the
timeout_secondsargument (default 10 for commands, 30 for queries, 60 for the agent-bridge tools), capped at 300. - The connector also enforces a server-side
ToolTimeoutSecondsceiling (default 300) — see Configuration. - A timeout may be transient (the responder was briefly absent). It is one of the few tool errors that is reasonable to retry with backoff — unlike a reserved-channel or invalid-params error, which will fail identically.
Detecting both layers in code
Branch in a fixed order: check the error field first, then result.isError. The
example below calls command_send (which surfaces a timeout as a tool error) and
inspects the result. Each snippet assumes an initialized MCP session — for the
connect-and-initialize handshake, see
Client setup.
# The JSON-RPC envelope always returns HTTP 200. Inspect the body:
# .error -> protocol error (no .result)
# .result.isError -> tool execution error
curl -X POST http://localhost:9090/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "command_send",
"arguments": {
"channel": "example-commands",
"body": "do-work",
"timeout_seconds": 10
}
}
}'result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "command_send",
Arguments: map[string]any{
"channel": "example-commands",
"body": "do-work",
"timeout_seconds": 10,
},
},
})
if err != nil {
// Layer 1/2: transport or JSON-RPC protocol error — the tool never ran.
log.Fatal(err)
}
// Layer 3: a valid call that failed at the tool layer (e.g. timeout).
if result.IsError {
fmt.Printf("tool error: %+v\n", result.Content)
return
}
fmt.Printf("Result: %+v\n", result)result = await session.call_tool("command_send", {
"channel": "example-commands",
"body": "do-work",
"timeout_seconds": 10,
})
# Layer 3: tool execution error (timeout, reserved channel, ...).
# Layer 1/2 protocol errors raise an exception before reaching here.
if result.isError:
print(f"tool error: {result.content[0].text}")
else:
print(f"Result: {result.content[0].text}")const result = await client.callTool({
name: "command_send",
arguments: {
channel: "example-commands",
body: "do-work",
timeout_seconds: 10,
},
});
// Layer 3: inspect isError before trusting the content.
if (result.isError) {
console.error("tool error:", result.content);
} else {
console.log(JSON.stringify(result, null, 2));
}var result = client.callTool(new CallToolRequest(
"command_send",
Map.of(
"channel", "example-commands",
"body", "do-work",
"timeout_seconds", 10
)
));
// Layer 3: a valid call that failed at the tool layer.
if (Boolean.TRUE.equals(result.isError())) {
System.out.println("tool error: " + result.content());
} else {
System.out.println(result);
}var result = await client.CallToolAsync("command_send", new Dictionary<string, object>
{
["channel"] = "example-commands",
["body"] = "do-work",
["timeout_seconds"] = 10,
});
// Layer 3: tool execution error (e.g. timeout).
if (result.IsError)
{
Console.WriteLine($"tool error: {result.Content}");
}
else
{
Console.WriteLine($"Result: {result}");
}val result = client.callTool("command_send", mapOf(
"channel" to "example-commands",
"body" to "do-work",
"timeout_seconds" to 10
))
// Layer 3: inspect isError before using the content.
if (result.isError == true) {
println("tool error: ${result.content}")
} else {
println("Result: $result")
}result = client.call_tool("command_send", {
"channel" => "example-commands",
"body" => "do-work",
"timeout_seconds" => 10,
})
# Layer 3: tool execution error (e.g. timeout).
if result.is_error
puts "tool error: #{result.content}"
else
puts "Result: #{result}"
endlet result = client.call_tool("command_send", json!({
"channel": "example-commands",
"body": "do-work",
"timeout_seconds": 10
})).await?;
// Layer 3: a valid call that failed at the tool layer.
if result.is_error.unwrap_or(false) {
eprintln!("tool error: {:#?}", result.content);
} else {
println!("Result: {result:#?}");
}let result = try await client.callTool("command_send", arguments: [
"channel": "example-commands",
"body": "do-work",
"timeout_seconds": 10,
])
// Layer 3: inspect isError before trusting the content.
if result.isError == true {
print("tool error: \(result.content)")
} else {
print("Result: \(result)")
}A timeout is reported through result.isError, not as a JSON-RPC error and
not as a non-200 HTTP status. Code that only catches transport exceptions or
checks the status code will silently treat a timed-out command as a success.
Best practices
- Check
errorfirst, thenresult.isError. A protocol error has noresult; a tool error is structurally a success withisError: true. - Do not branch on the HTTP status for JSON-RPC calls — every response is
HTTP 200 (auth/origin rejections excepted, which return
-32010in the body). - Retry only transient tool errors (timeouts, no-subscriber). Protocol errors and reserved-channel/invalid-params errors fail identically on retry — fix the request instead.
- Log the full response when debugging, not just the field you branched on.
Related
Error codes reference
The canonical catalog: JSON-RPC codes, tool-level messages, and HTTP status codes.
Session management
The initialize handshake, MCP-Session-Id, batches, and keepalive.
Authentication
JWT auth, the -32010 auth error, and origin validation for /mcp.
Command & query tools
The synchronous tools whose timeouts surface as tool-layer isError results.
Was this page helpful?
Session Management
Establish and reuse an MCP session with the KubeMQ connector — initialize handshake, MCP-Session-Id, batch requests, and the GET /mcp keepalive stream.
Endpoints
HTTP and JSON-RPC endpoint reference for the KubeMQ MCP connector — POST/GET /mcp, the initialize/tools/list/tools/call/ping methods, headers, and status codes.