MCP Server

Troubleshooting the MCP Server Handler

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeManaged DedicatedSelf-Hosted

When an AI client calls a tool exposed by the MCP Server handler, a failure often surfaces as nothing more than Tool call failed: 500, with no indication of why. The underlying gateway route can return 200 while the MCP client still rejects the response, which makes these failures hard to diagnose from the client alone.

This page shows how to turn on the handler's debug logging, read the log lines that reveal a misconfiguration, and fix the most common causes: a tool that advertises an output schema but returns no structured content, and arguments that don't match the schema the tool advertises.

Most "200 on the gateway, error in the client" failures come from the includeOutputSchema and includeStructuredContent options. If you enabled one without the other, jump straight to Tool call failed: 500.

Enable debug logging

The MCP Server handler can emit verbose logs covering server startup, tool registration, and each tool call. These logs are the fastest way to confirm what the server actually advertised to the client.

  1. Turn on debugMode. Set debugMode: true in the handler options for your MCP route in routes.oas.json:

    JSONCode
    "post": { "x-zuplo-route": { "handler": { "export": "mcpServerHandler", "module": "$import(@zuplo/runtime)", "options": { "name": "example-mcp-server", "version": "1.0.0", "debugMode": true } } } }
  2. Redeploy so the server cold-starts. Some of the most useful log lines — server startup and tool registration — are written only when the MCP server initializes (a cold start). An already-running worker won't re-emit them. Deploy the working copy or environment again so a fresh worker boots with debugMode enabled.

  3. View the logs. Open your project in the Portal and go to Observability → Logs. Trigger a tool call from your MCP client, then read the entries for the MCP route. Filter on the request's zuplo-request-id to isolate a single call.

Leave debugMode off in production. It logs verbose per-call detail that adds noise and overhead. Turn it on to diagnose an issue, then turn it back off and redeploy.

Key log lines to read

With debugMode: true, the handler writes lines at each stage of its lifecycle. The exact format may change between runtime versions, but the fields below are what you're looking for:

Log lineWhen it's writtenWhat to check
MCP Server cold startA fresh worker bootsConfirms debugMode is active and a new worker started
MCP tool registeredEach tool is registeredThe includeStructuredContent and hasOutputSchema fields for a tool
MCP Server response completeA tool call finishesThe response was produced and sent back to the client

The MCP tool registered line is the important one. For a tool whose calls fail in the client, you'll typically see a line resembling:

Code
MCP tool registered { name: "get_current_weather", hasOutputSchema: true, includeStructuredContent: false }

hasOutputSchema: true with includeStructuredContent: false is the exact misconfiguration described in the next section.

Tool call failed: 500 / "has an output schema but did not return structured content"

Symptom. An AI client (such as Claude) reports Tool call failed: 500 when it invokes a tool. With more verbose client logging, the underlying error reads:

Code
Tool <tool_name> has an output schema but did not return structured content

Meanwhile, the gateway's own logs show the route that backs the tool returned 200.

Likely cause. The handler is configured with includeOutputSchema: true but not includeStructuredContent: true. Under the MCP 2025-06-18 structured-content behavior, when a tool advertises an outputSchema, the client expects every successful result to include a matching structuredContent object. With includeOutputSchema on and includeStructuredContent off, the server advertises the schema but returns only text content, so a spec-compliant client rejects an otherwise-successful 200 response.

The MCP tool registered debug line confirms it: hasOutputSchema: true, includeStructuredContent: false.

Fix. Enable both options together on the MCP Server handler:

JSONCode
"options": { "name": "example-mcp-server", "version": "1.0.0", "includeOutputSchema": true, "includeStructuredContent": true }

Then redeploy. The next MCP tool registered line should read hasOutputSchema: true, includeStructuredContent: true, and the tool call succeeds.

If you don't need output schemas at all, the alternative fix is to turn both off (the defaults). The two options are designed to move together: enable includeOutputSchema only alongside includeStructuredContent.

For the full definitions of both options, see the MCP 2025-06-18 Global Options in the handler reference.

Fields missing for clients on older protocol versions

From compatibility date 2026-11-06, the handler leaves out of each response the fields that protocol revisions newer than the client's added. The request's MCP-Protocol-Version header decides the version, and a request without the header counts as 2025-03-26:

Client's protocol versionFields the handler leaves out
Before 2025-06-18outputSchema and title on tools, title on resources, structuredContent on tools/call results, and lastModified on content annotations. A resource_link block becomes a text block of JSON.
Before 2025-11-25icons and execution on tools, and icons on resources and resource links
Before 2025-03-26annotations on tools

When a tools/call result has structuredContent but no text block, an older client receives the JSON as a text block instead. _meta is sent on every version.

So if a client doesn't receive the outputSchema or structuredContent that includeOutputSchema and includeStructuredContent turn on, check the MCP-Protocol-Version header the client sends. The client must send 2025-06-18 or later to receive them. See Compatibility dates.

Invalid arguments for tool

Symptom. A tool call fails with a message that starts Invalid arguments for tool '<name>': and names an argument. How the failure reaches the client depends on the MCP-Protocol-Version header the client sends:

  • 2025-11-25 — the call returns a tool result with isError: true and the message as its only text content. The model reads the message and can retry with corrected arguments.
  • 2025-06-18 or earlier, or no header — the call fails with JSON-RPC error -32602 and the message. error.data carries the validation details when there are any.

Either way, the HTTP status is 200 and the tool doesn't run, so no request reaches the route behind it. For example, on 2025-11-25:

JSONCode
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "Invalid arguments for tool 'create_order': must NOT have additional properties (sku)" } ], "isError": true } }

The text after the colon describes each failed check. Its exact wording depends on your project's compatibility date.

Likely cause 1: the argument isn't in the schema. The request body schema backing the tool sets additionalProperties: false, so the tool's input schema is closed and an argument it doesn't declare is invalid. Read the tool's inputSchema from tools/list and compare the argument names. A model that invents a field name lands here.

Likely cause 2: the tool takes nested arguments. A tool takes nested arguments when its toolInputStyle is nested, which is the default, or when its OpenAPI can't be flattened. Its arguments sit under body, queryParams, pathParams, or headers. When the message reports a missing body, the caller sent flat arguments to a nested tool:

Code
Invalid arguments for tool 'create_order': must have required property 'body'

When a flat tool still advertises nested arguments, the MCP server logs why when it registers the tool: a name collision between two input channels (logged as a warning), or a request body that isn't a plain object (logged at debug level — enable debugMode to see it). From compatibility date 2026-11-06, both cases fail the build instead. See When arguments stay nested for both cases and how to resolve them.

Fix. Always craft arguments against the inputSchema the tool advertises in tools/list rather than assuming a shape. The MCP Inspector shows both the schema and the error for a call.

Build warning: tool name doesn't follow the MCP tool naming rule

Symptom. The build log shows a warning such as:

Code
MCP tool name "search items/v2" on route POST /mcp does not follow the MCP tool naming rule

Or, when two tools share a name:

Code
MCP tool name "duplicate_name" is used by more than one tool on route POST /mcp

Likely cause. MCP tool names should be 1 to 128 characters of A-Z, a-z, 0-9, _, -, and ., and unique within a server. A tool's name comes from x-zuplo-route.mcp.name or, when that's not set, the operation's operationId. Names with spaces, /, or non-ASCII characters break the rule, and clients that enforce it reject or rename those tools. The build warns about every such name, whatever your compatibility date.

What the handler does with the name depends on your compatibility date:

  • Before 2026-11-06 — the handler advertises each tool under its name as is. When two tools share a name, the later one replaces the earlier one.
  • From 2026-11-06 — the handler rewrites each name that breaks the rule: every character outside the allowed set becomes _, an empty name becomes tool, and the name is cut to 128 characters. A rewritten or repeated name that's already taken gets a _2, _3, and so on suffix, so both tools stay available. For example, search items/v2 becomes search_items_v2, or search_items_v2_2 when another tool already uses search_items_v2.

From compatibility date 2026-11-06, tools/list and tools/call use the rewritten name, and tools/call no longer accepts the old name. A client or prompt that calls a tool by its old name gets a Tool "<name>" not found error (-32602) after you move to the new compatibility date.

Fix. Set x-zuplo-route.mcp.name on each operation the warning names to a unique name that follows the rule. The name then stays the same on every compatibility date. See MCP Server tools for the mcp route options and Compatibility dates for the change.

JSON-RPC errors the handler returns

The MCP Server handler answers protocol mistakes with the JSON-RPC error codes that JSON-RPC 2.0 and the MCP specification assign. On 2025-11-25 and earlier, every error message is a single line. When the handler can't read the request's id, the error has no id. The table lists the answers on 2025-11-25 and earlier; see JSON-RPC errors on 2026-07-28 for that revision.

CodeHTTP statusMeaning
-32700400Parse error: the body isn't valid JSON.
-32600400Invalid request: valid JSON that isn't a JSON-RPC request (for example, no method, or jsonrpc isn't "2.0"), an empty batch, or a refused batch.
-32601200Method not found: the handler doesn't serve the method.
-32602200Invalid params: a missing or mistyped param, an unknown tool or prompt name, a list cursor (the handler never returns nextCursor), or invalid tool arguments before 2025-11-25.
-32002200Resource not found: resources/read of a URI that no resource uses. error.data.uri holds the requested URI.
-32603200Internal error: the handler failed while it served a valid request, such as a resource route that throws.
-320004xxTransport error: 405 for an unserved HTTP method, 406 for a missing Accept type, 415 for a non-JSON Content-Type, and 400 for an unsupported MCP-Protocol-Version.

The handler accepts MCP-Protocol-Version values 2024-10-07, 2024-11-05, 2025-03-26, 2025-06-18, and 2025-11-25, and 2026-07-28 from compatibility date 2026-11-06. A request without the header counts as 2025-03-26. Any other value gets HTTP 400 and -32000, and so does 2026-07-28 on an earlier compatibility date.

JSON-RPC batches

A JSON-RPC batch sends several requests as one JSON array. MCP removed batches in protocol version 2025-06-18, so the handler serves a batch only when the request's MCP-Protocol-Version header is 2025-03-26 or earlier, or when the header is missing. On 2025-06-18 or later, the handler refuses the whole batch and runs none of it:

JSONCode
{ "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Request: protocol version 2025-11-25 does not support JSON-RPC batches" } }

The response has HTTP status 400. To fix it, send each request in its own HTTP request. In a batch the handler serves, each invalid member gets its own -32600 error in its position and the valid members still run. A batch holds at most 100 requests. From compatibility date 2026-11-06, the answer to a batch is always an array. On an earlier date, a batch whose answer holds a single response gets that response as a bare object.

JSON-RPC errors on 2026-07-28

From compatibility date 2026-11-06, a request whose MCP-Protocol-Version header is 2026-07-28 gets these answers. A request that names 2026-07-28 only in its params._meta envelope, without the header, gets HTTP 400 with -32020, because 2026-07-28 requires the header. A request whose _meta names 2025-11-25 or an earlier revision, without a 2026-07-28 header, is served on that revision and gets the answers in the table above.

CodeHTTP statusMeaning
-32602400Missing envelope: params._meta is missing or malformed.
-32020400Header mismatch: the MCP-Protocol-Version, Mcp-Method, or Mcp-Name header is missing or disagrees with the body.
-32022400Unsupported revision: the request names a revision the handler doesn't serve. error.data.supported lists 2026-07-28, then 2025-11-25 through 2024-10-07.
-32601404Method not found: 2026-07-28 removed initialize, ping, logging/setLevel, resources/subscribe, and resources/unsubscribe.
-32600400Invalid request: a JSON-RPC batch or a posted JSON-RPC response. 2026-07-28 has neither, so the handler runs none of it.
-32600406, 415Transport error: an Accept header that doesn't allow application/json (406), or a non-JSON Content-Type (415). On 2025-11-25 and earlier, these carry -32000.
-32602200Invalid params: a missing or mistyped param, a list cursor, an unknown tool (Tool <name> not found), an unknown prompt (Prompt <name> not found), or an unknown resource (Resource not found: <uri>, with error.data.uri).

An unknown resource gets -32602 on 2026-07-28, not -32002. A notification gets HTTP 202 with no body.

Client-side debugging tools

When the gateway looks healthy, reproduce the failure against the client to see the protocol-level error the AI client hides:

  • MCP Inspector — the official inspector (npx @modelcontextprotocol/inspector) connects to your remote server, lists tools, and calls them while showing the raw JSON-RPC messages. See Testing for connection steps.
  • mcpjam — the mcpjam inspector is an open-source MCP testing client that's useful for exercising tool calls and inspecting responses outside of an AI client.
  • Claude Code — run with the --debug flag (for example, claude --debug) to print MCP protocol traffic, including the full tool-call error that the chat UI summarizes as Tool call failed: 500.

Checklist: the gateway returns 200 but the client errors

When a tool call fails in the client but the gateway reports success, verify each of these in order:

  1. debugMode is on and a fresh worker booted. Confirm a MCP Server cold start line appears after your latest deploy. Without a cold start, you're reading stale logs.
  2. Schema and structured content move together. In the MCP tool registered line, hasOutputSchema and includeStructuredContent should either both be true or both be false — never hasOutputSchema: true with includeStructuredContent: false.
  3. The output schema is a valid type: object JSON Schema. Some clients reject schemas that aren't type: object. The same applies to the structuredContent the server returns. See the compatibility caveat.
  4. The route really returns JSON. includeStructuredContent parses the response body as JSON to build structuredContent. A non-JSON 200 body can't be parsed into the advertised schema.
  5. Reproduce in a raw client. Call the tool through the MCP Inspector or curl to read the JSON-RPC error directly, rather than the client's summarized 500.

Next steps

Last modified on