Skip to content

Header parameters

Most servers never need this.

A gateway or load balancer in front of your server can only route on what it can read without parsing the body. Mark a tool argument with x-mcp-header, and clients on the 2026-07-28 protocol version send its value as an HTTP header as well.

Mark an argument

The mark is one extra key in the argument's JSON Schema. On MCPServer, Field puts it there:

server.py
from typing import Annotated

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def check_stock(
    title: str,
    region: Annotated[str, Field(json_schema_extra={"x-mcp-header": "Region"})],
) -> str:
    """Count the copies of a book in one region's warehouses."""
    return f"{title}: 3 copies in {region}."
  • Over Streamable HTTP on 2026-07-28, a client sends Mcp-Param-Region alongside the body, and the server rejects a call where the two disagree.
  • A client that hasn't listed the tool has never seen the mark: it sends no header, and the call is rejected. This SDK's Client then lists the tools and resends the call once, so listing first only saves a round trip.
  • Every other connection ignores the annotation.

Your function doesn't change: region still arrives as an argument.

What can be marked

str, int and bool arguments. Anything else is refused when the tool is registered, with InvalidSignature.

That includes str | None, which has no single type. An optional argument needs its schema spelled out, with pydantic's WithJsonSchema:

region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None

On the low-level Server

There you write input_schema by hand, so the key goes straight in:

server.py
from mcp.server import Server, ServerRequestContext
from mcp.types import (
    CallToolRequestParams,
    CallToolResult,
    ListToolsResult,
    PaginatedRequestParams,
    TextContent,
    Tool,
)

CHECK_STOCK = Tool(
    name="check_stock",
    description="Count the copies of a book in one region's warehouses.",
    input_schema={
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "region": {"type": "string", "x-mcp-header": "Region"},
        },
        "required": ["title", "region"],
    },
)


async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
    return ListToolsResult(tools=[CHECK_STOCK])


async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
    args = params.arguments or {}
    text = f"{args['title']}: 3 copies in {args['region']}."
    return CallToolResult(content=[TextContent(type="text", text=text)])


server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
app = server.streamable_http_app()
  • Nothing checks the annotation for you: an invalid one is served, and 2026-07-28 clients leave the tool out of their listing.

Schemas by name

To check the header, the SDK needs the tool's input schema before it dispatches the call. Without get_tool_input_schema it gets it by running your on_list_tools handler on every call that carries arguments, whether or not any tool is marked.

server.py
from typing import Any

from mcp.server import Server, ServerRequestContext
from mcp.types import (
    CallToolRequestParams,
    CallToolResult,
    ListToolsResult,
    PaginatedRequestParams,
    TextContent,
    Tool,
)

CHECK_STOCK = Tool(
    name="check_stock",
    description="Count the copies of a book in one region's warehouses.",
    input_schema={
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "region": {"type": "string", "x-mcp-header": "Region"},
        },
        "required": ["title", "region"],
    },
)

TOOLS = {CHECK_STOCK.name: CHECK_STOCK}


async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
    return ListToolsResult(tools=list(TOOLS.values()))


async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
    args = params.arguments or {}
    text = f"{args['title']}: 3 copies in {args['region']}."
    return CallToolResult(content=[TextContent(type="text", text=text)])


def tool_input_schema(name: str) -> dict[str, Any] | None:
    tool = TOOLS.get(name)
    return tool.input_schema if tool else None


server = Server(
    "Bookshop",
    on_list_tools=list_tools,
    on_call_tool=call_tool,
    get_tool_input_schema=tool_input_schema,
)
app = server.streamable_http_app()
  • Pass the function to answer from what you already have.
  • Return None for a tool with nothing to check.

Recap

  • x-mcp-header on a tool argument makes 2026-07-28 clients repeat it as an Mcp-Param-* HTTP header.
  • The server rejects a call whose header and body disagree.
  • Only str, int and bool arguments can be marked. MCPServer raises InvalidSignature for anything else.
  • The low-level Server checks nothing, and clients drop a tool whose annotation is invalid.
  • get_tool_input_schema keeps the low-level Server from running on_list_tools on every call.

The rest of the hand-written Server API is The low-level Server.