Zuplo
On this page
MCP error

Tool dropped for an invalid x-mcp-header

The string your client printed
[mcp-sdk] excluding tool '<name>' from tools/list: invalid x-mcp-header declaration

One parameter in that tool's inputSchema carries an x-mcp-header annotation that breaks a 2026-07-28 constraint, so a conforming Streamable HTTP client drops the whole tool from tools/list and logs a warning instead of raising an error.

MCP specification
2026-07-28

Is this you?

Compare what the server returns against what the client passes on. The server's tools/list response still contains the tool, so seeing it in curl and not in your client is the confirmation — and it rules out a tool that failed to register or a capability filter in front of the route.

TerminalRun this to check — it changes nothing
# 1. Does the server still return the tool? Read the raw response.
curl -sS -X POST https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
       "io.modelcontextprotocol/protocolVersion":"2026-07-28",
       "io.modelcontextprotocol/clientCapabilities":{}}}}' \
  -o tools.json
jq -r '.result.tools[].name' tools.json

# 2. Where is every annotation declared? Print the schema path of each one.
#    A valid path alternates `properties` and a property name all the way
#    down; anything else in the path is the reason the tool was dropped.
jq -r '.result.tools[]
       | .name as $n
       | [.inputSchema | paths | select(.[-1] == "x-mcp-header")]
       | .[] | "\($n): \(join("."))"' tools.json
This error
Step 1 lists the tool and your client does not. Step 2 then prints a path with another keyword in it — broken: properties.regions.items.x-mcp-header is the common one, an annotation left on an array's items — or oneOf, anyOf, allOf, not, if, then, else, $ref, or additionalProperties in the same position. Two annotations whose values differ only in case, and an annotation on a property typed number, object or array, fail the same way. If step 1 does not list the tool either, this is not your problem: the tool never registered, or something in front of the route is filtering capabilities.
Then the fix is the next section.
Working
Step 1 lists the tool and your client offers it too, which means no exclusion is happening. Every path step 2 prints alternates properties and a property name all the way down, as in execute_sql: properties.region.x-mcp-header, and each annotated property is typed string, integer or boolean.
Then this isn't your problem — try the error reference.
5 other strings clients print for this
  • [mcp-sdk] excluding tool '<name>' from tools/list: invalid x-mcp-header declaration — <reason>
  • [mcp-sdk] tool '<name>' carries an invalid x-mcp-header declaration and will be excluded by conforming Streamable HTTP clients: <reason>
  • dropping tool '<name>': invalid x-mcp-header (<reason>)
  • <path>: x-mcp-header is only permitted on primitive-typed properties (string, integer, boolean); got <type>
  • <path>: x-mcp-header is only permitted on properties statically reachable via a chain of 'properties' keys (not under items, additionalProperties, oneOf/anyOf/allOf/not, if/then/else, or $ref)

Dotted spans such as <url> are placeholders — your client prints your own value there.

Fix it

Find the annotation and fix the schema; the warning already names the tool and the reason. An x-mcp-header value must be a non-empty RFC 9110 token, case-insensitively unique across the schema, on a property typed string, integer or booleannumber is not permitted — and reachable from the schema root through a chain of properties keys only, never through items, oneOf/anyOf/allOf/not, if/then/else, or $ref. If the tool is not yours, read the raw tools/list response with curl: the server still returns the tool, and the client removes it after the fact.

Have your agent fix this

Copy a prompt that sends your coding agent to this page to find and apply the right fix for your project.

The cause depends on your platform. Pick yours.

MCP TypeScript SDK

If your server or client is the MCP TypeScript SDK 2.x

  1. 1 Read the warning from whichever side logged it, because both sides have one and they are worded differently. The client logs [mcp-sdk] excluding tool '<name>' from tools/list: invalid x-mcp-header declaration — <reason> when it drops the tool. McpServer.registerTool warns at registration time instead: [mcp-sdk] tool '<name>' carries an invalid x-mcp-header declaration and will be excluded by conforming Streamable HTTP clients: <reason>. That server-side warning fires whether or not any client has connected, so it is the faster place to look.
  2. 2 Take the reason literally — the SDK names the failing property path and the constraint. Its messages cover the five cases separately: not statically reachable via a chain of 'properties' keys, not a non-empty string, not a valid RFC 9110 token, not on a primitive-typed property, and not case-insensitively unique.
  3. 3 Expect the exclusion on the client to be skipped over stdio. The client applies it only on a modern-era connection that is not stdio, so the same malformed tool stays visible to a stdio client and vanishes over Streamable HTTP. The server-side registration warning does not make that distinction and is the one to trust.
  4. 4 Clear the client's cached listing after you fix the schema. The exclusion is applied to the cached aggregated tools/list result as well as to each returned page, so a still-fresh cache entry keeps the tool missing; cacheMode: 'refresh' on the call forces a fetch.
  5. 5 Do not reach for x-mcp-header in a browser-resident client. The SDK skips mirroring there, because dynamically named headers cannot be statically allowlisted for credentialed CORS, and a conforming server then rejects the tools/call for the missing header. The SDK documents this as a known limitation rather than a bug.
TypeScriptts
// Rejected: the annotation is under `items`, so it is not statically
// reachable through a chain of `properties` keys.
inputSchema: {
  type: 'object',
  properties: {
    regions: {
      type: 'array',
      items: { type: 'string', 'x-mcp-header': 'Region' }
    }
  }
}

// Accepted: a primitive property reached through `properties` only.
inputSchema: {
  type: 'object',
  properties: {
    region: { type: 'string', 'x-mcp-header': 'Region' },
    query: { type: 'string' }
  },
  required: ['region', 'query']
}
MCP TypeScript SDK: supporting 2026-07-28

MCP Python SDK

If the client dropping the tool is the MCP Python SDK 2.x

  1. 1 Search your logs for dropping tool rather than for the tool name. The client session logs dropping tool '<name>': invalid x-mcp-header (<reason>) at warning level as it filters the listing, and it also evicts any argument-to-header map it had cached for that tool from an earlier, valid listing.
  2. 2 Expect the Python client to drop the tool on stdio as well. It gates the filter on the negotiated protocol version alone, with no transport carve-out, so a tool that a TypeScript client still shows over stdio disappears here. That difference is a real one between the two SDKs, not a misconfiguration in your server.
  3. 3 Read the reason string for the property path. The validator reports the five cases distinctly: it says the annotation was "found at a schema position not reachable via a pure properties chain" for the unreachable case, and is only permitted on integer/string/boolean properties for a wrong type.
  4. 4 Check the annotation survived schema generation when you declare it through Pydantic. It travels as json_schema_extra={"x-mcp-header": "Region"} on the field, and a wrapper that rebuilds the schema — an optional field widened to type: ["string", "null"], for instance — can move it somewhere the validator will not accept.
python
from pydantic import Field
from typing import Annotated

@mcp.tool()
def execute_sql(
    region: Annotated[
        str,
        Field(json_schema_extra={"x-mcp-header": "Region"}),
    ],
    query: str,
) -> str:
    ...
MCP Python SDK: v1 to v2 migration guide

A server you don't control

If the tool belongs to someone else's server and you only consume it

  1. 1 Confirm the annotation is the cause before you file anything. A tool present in the raw tools/list and absent from your client is the signature of this exclusion; a tool absent from both is a different problem entirely.
  2. 2 Send the maintainer the property path and the reason string. The SDK warnings name both, which is most of a bug report, and a server author who only ever tested over stdio may never have seen the warning — the TypeScript client skips the exclusion on stdio, so the tool works there.
  3. 3 Reach the tool over stdio in the meantime, if the server offers that transport and your client is the TypeScript SDK. The exclusion is scoped to non-stdio modern connections there. The Python client applies it on every transport, so this is not a general workaround.
  4. 4 Pin the connection to an earlier protocol revision as the other stopgap. The rule arrived in 2026-07-28, so a client that negotiates 2025-11-25 against a server that still serves it returns the tool unfiltered. Treat that as buying days, not as a resting place.
  5. 5 Do not expect a capability allowlist to bring it back. mcp-capability-filter-inbound and its equivalents decide which capabilities a route exposes; nothing in front of the client can restore a tool the client removed after reading a successful response.
MCP specification: Streamable HTTP custom headers

Something else

Whatever the SDK, the shape is the same: the schema is the fault and the client is the enforcement point, so nothing you change at the transport or the authorization layer will bring the tool back. Read the raw tools/list response to confirm the server still publishes the tool, then find every x-mcp-header in that tool's inputSchema and check each one against the five constraints — non-empty, RFC 9110 token, case-insensitively unique, on a string, integer or boolean property, and reachable through properties keys only. Fix the schema where you can, and expect a cached listing to keep the tool hidden until it is refreshed. Where the server is not yours, send the maintainer the property path from that jq output and treat stdio or an earlier negotiated revision as the stopgap. One thing to rule out before you start: a valid annotation still adds a header to every matching tools/call, so a signing or header-allowlisting layer that did not expect Mcp-Param-* fails separately and later.

Why it happens

The 2026-07-28 revision lets a server mirror a tool argument into an Mcp-Param-{Name} HTTP header by annotating the parameter with x-mcp-header, so an intermediary can route on a value without parsing the body.

An invalid annotation is fatal to the tool rather than to the request: "Clients using the Streamable HTTP transport MUST reject tool definitions where any x-mcp-header value violates these constraints. Rejection means the client MUST exclude the invalid tool from the result of tools/list." That "ensures that a single malformed tool definition does not prevent other valid tools from being used."

Logging is only a SHOULD and nothing raises an error, so both sides behaved correctly and the sole trace is a warning on whichever stream the client logs to.

The rule is narrower than it looks. It is transport-scoped, because clients on other transports "MAY ignore x-mcp-header annotations entirely", and era-scoped, so the same server and client keep working together until they negotiate 2026-07-28.

The exchange

  • Model sees the tool list
  • MCP client Streamable HTTP, 2026-07-28
  • MCP server
  1. Both sides are conformant here. The server is entitled to publish the annotation and returns every tool it has, so a capture of this exchange shows nothing wrong.

    MCP client to MCP server

    POST /mcp Mcp-Method: tools/list
  2. MCP server to MCP client

    200 OK 4 tools, one with x-mcp-header
  3. The exclusion happens inside the client, after a successful response. Nothing is sent back to the server, so server-side logs record a normal tools/list and the tool count the model was offered appears nowhere.

    Inside MCP client

    scan x-mcp-header declarations annotation under items, not properties
  4. MCP client to Model

    3 tools (this step fails) warning logged, no error raised

    Flow stops here

The MCP client posts tools/list to the server over Streamable HTTP on a 2026-07-28 connection. The server returns 200 with all of its tools, including one whose inputSchema carries an x-mcp-header annotation on a property that is not statically reachable. The client scans the returned definitions and drops that tool from the result before returning it, logging a warning. The model is offered the remaining tools and never learns the dropped one exists.
Zuplo

The headers that annotation was trying to populate exist so whatever sits in the request path can act on them, which is where the next questions land.

We route on a mirrored header. Can a client downgrade its way past that?

mcp_request_rejected

Only if nothing checks the version. Rejections are recorded per request with a named reason, so a request arriving without the header validation the version implies is visible rather than silently trusted.

Which tools does this route publish at all, whatever a server lists?

mcp-capability-filter-inbound

Exactly the ones you put on it: capabilities outside the route's allowlist are refused with MethodNotFound, so the exposed surface is a route property rather than an upstream one — and accessControl narrows it further per caller.

Can we meter the tool that header was naming the tenant for?

complex-rate-limit-inbound

Yes — per tenant: named counters, each with its own limit and window, keyed on the authenticated caller, so one tenant's volume is bounded without a shared global bucket. An Enterprise policy.

A tool call failed on a header. Which caller, and on which tool?

capability_invocation

Both, from the event stream: every call carries the caller, the capability and the outcome, so a header-validation failure resolves to a client and a tool rather than to a 400 in an access log. Arguments are not in there — request bodies are never logged.

All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out. Your MCP server keeps the code and the authentication it has today.

Stop debugging auth on every MCP server

A gateway in front of your MCP servers handles discovery, audience binding, and token validation once — for every server and every client.