Hacktoberfest 2026: die Issues, die Maintainer für den Oktober markiert haben – offen und einsteigerfreundlich. Hacktoberfest-Issues durchsuchen

Client never retries after -32020 HeaderMismatch, and there is no public way to pre-load the x-mcp-header map (SEP-2243 client SHOULD/MAY both unimplemented)

Offen
#3,483 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Anfängerfreundlichkeit
52/100
Issue-Typ
Feature
Klarheit
Größtenteils klar
Aktivitätsstatus
Aktiv
Tech-Stack
python
Bereich
api, backend

Rechercherichtung

Beginne in mcp/client/session.py bei ClientSession._resolve_param_headers und verfolge _absorb_tool_listing; vergleiche dies mit der serverseitigen HEADER_MISMATCH-Behandlung in mcp/server/_streamable_http_modern.py und mcp/shared/inbound.py. Führe die mitgelieferte Reproduktion aus, lege dann den Umfang für preload, begrenzte Wiederholungsversuche oder Warnverhalten fest und verifiziere das ausgewählte Verhalten mit Client-Tests und der Reproduktion.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

spec-2026-07-28 v2
Summary

At 2026-07-28, a ClientSession that has not listed a tool sends tools/call with no Mcp-Param-* headers, the server rejects it with -32020, and the client stops there. SEP-2243 says it SHOULD re-list and retry, and there is no way for an application to supply the map instead — _x_mcp_header_maps is private and _absorb_tool_listing is its only writer.

Everything below reproduces against the SDK's own client and server, no third-party server involved.

Reproduction
import anyio, httpx2, uvicorn
from typing import Annotated
from pydantic import Field
from mcp.client import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.server.mcpserver import MCPServer
from mcp.shared.exceptions import MCPError

server = MCPServer("repro")

@server.tool()
async def fetch(
    owner: Annotated[str, Field(json_schema_extra={"x-mcp-header": "owner"})],
) -> str:
    """One annotated argument, as the SEP's examples have."""
    return f"fetched for {owner}"

URL = "http://127.0.0.1:8931/mcp"

async def exercise() -> None:
    await anyio.sleep(1.5)
    async with httpx2.AsyncClient() as http:
        print("== a session that never listed ==")
        async with Client(streamable_http_client(URL, http_client=http), mode="auto") as client:
            print("negotiated:", client.protocol_version)
            try:
                r = await client.call_tool("fetch", {"owner": "octocat"})
                print("   result:", r.content)
            except MCPError as exc:
                print(f"   MCPError code={exc.error.code}")
                print(f"   message={exc.error.message!r}")

        print("== a session that listed first ==")
        async with Client(streamable_http_client(URL, http_client=http), mode="auto") as client:
            await client.list_tools()
            r = await client.call_tool("fetch", {"owner": "octocat"})
            print("   result:", r.content)

async def main() -> None:
    config = uvicorn.Config(server.streamable_http_app(), host="127.0.0.1", port=8931, log_level="error")
    http_server = uvicorn.Server(config)
    async with anyio.create_task_group() as tg:
        tg.start_soon(http_server.serve)
        await exercise()
        http_server.should_exit = True

anyio.run(main)

Output on mcp 2.1.1:

== a session that never listed ==
negotiated: 2026-07-28
   MCPError code=-32020
   message="Mcp-Param-owner header is missing but the request body's 'owner' argument is present"
== a session that listed first ==
   result: [TextContent(type='text', text='fetched for octocat', ...)]

The -32020 arrives as HTTP 400. The client neither re-lists nor retries.

What the spec asks for

SEP-2243, Client Behavior:

Implementation Note: Clients MUST construct Mcp-Param-* headers using the most recently obtained inputSchema for the tool. A client that has never obtained the tool's inputSchema SHOULD send the request without Mcp-Param-* headers. If the server rejects the request because required Mcp-Param-* headers are missing or do not match the body, the client SHOULD call tools/list to obtain the current inputSchema, then retry the original request with the appropriate headers. Clients MAY pre-load tool definitions via other means (e.g., from a previous session or configuration) to enable header emission without a prior tools/list call.

Two mechanisms in one paragraph. The SDK implements neither.

HEADER_MISMATCH has zero readers under mcp/client/ — every occurrence is server-side (mcp/server/_streamable_http_modern.py, mcp/shared/inbound.py), producing the rejection rather than reacting to one.

Why the second half matters as much as the first

An application that opens a session per operation — for connection hygiene, or because it holds no long-lived session — has the schema in hand already, from the listing it built its tool surface with. The SEP explicitly blesses using it ("MAY pre-load tool definitions via other means"). But _x_mcp_header_maps is private, and the only writer is _absorb_tool_listing, reachable only through an in-session list_tools(). So the sanctioned cheap path is unreachable and the only route is a redundant wire request per call.

For us that request is ~1.3 s and ~122 KiB against a 44-tool server, paid on the calling session purely to repopulate state we already had.

What this cost, concretely

We hit this in production against the GitHub MCP server: 36 of its 44 tools were refused with -32020, because our client opens one session per tools/call and never lists on it. The client-side symptom was a bare -32020 from the server; nothing on our side could say why, because:

ClientSession._resolve_param_headers (mcp/client/session.py:1118) returns {} silently when the session holds no map for the tool:

def _resolve_param_headers(self, name: str, arguments: Mapping[str, Any]) -> dict[str, str]:
    """`Mcp-Param-*` headers for a `tools/call`, or empty when the tool was never listed."""
    header_map = self._x_mcp_header_maps.get(name)
    if header_map is None:
        return {}
    return mcp_param_headers(header_map, arguments)

At a modern protocol version, "this tool was never listed on this session" is a strong signal that the call is about to be refused. The listing filter one screen away already logs when it drops a tool (logger.warning("dropping tool %r: invalid x-mcp-header (%s)", ...)), so the precedent for saying something here is right there.

Suggested fixes, in the order we would value them
  1. A public way to seed the map — e.g. ClientSession.preload_tool_listing(result), or a tools= argument to adopt(). Implements the SEP's "MAY pre-load" sentence, and lets a session-per-call client emit correct headers with no extra round trip.
  2. The -32020 retry, bounded to one attempt: re-list, rebuild, resend. This is the SHOULD, and it is the only thing that heals a schema that gains an annotation mid-session.
  3. A warning in _resolve_param_headers when a modern session has no map for the tool it is calling. Cheapest of the three, and the one that would have turned our incident into a log line instead of an investigation.

Happy to open a PR for any of these if the direction is agreeable — 1 and 3 look small; 2 needs a decision about where the retry lives relative to validate_tool_result.

Environment
  • mcp 2.1.1, mcp-types 2.1.1, Python 3.12.9
  • Transport: streamable HTTP, mode="auto", negotiated 2026-07-28
Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 11 Std.
Gemergte PRs (30 T.)
30

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus modelcontextprotocol/python-sdk

Alle Issues in modelcontextprotocol/python-sdk

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.