You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ServerCapabilities drops unknown keys, so a server can't advertise events for ChatGPT's MCP Events #3640
What happens.ServerCapabilities is documented as "Not a closed set", but unknown keys are silently dropped (see the example below). Spec results are also sieved against the schema in ServerRunner._serialize, so even a custom server/discover handler that returns a dict with capabilities.events loses it before it reaches the wire.
What I expected. Extra capability keys to be kept, as the docstring suggests.
Why it matters. ChatGPT's MCP Events (launched 29 September 2026, docs at https://developers.openai.com/plugins/build/mcp-events) only subscribes to servers whose server/discover result contains top-level capabilities.events. With the SDK as it is, a Python server can't advertise it without working around the SDK.
Workaround. The events/* methods themselves register fine with add_request_handler. The capability can be added by a ServerMiddleware that edits the serialised result after call_next. That works, and I've tested it end to end with ChatGPT, but it relies on internals.
Possible fixes, in rough order of effort:
Let ServerCapabilities keep extra keys (extra="allow"), so the "not a closed set" docstring holds and the sieve passes them through.
A supported way to add capability keys, like extensions already has.
First class Events support, following the draft in modelcontextprotocol/experimental-ext-triggers-events.
For anyone who needs it before then, I've published the workaround plus the full Events implementation (verification, signing, retries, persistence) as mcp-webhook-events on PyPI, MIT licensed: https://github.com/s1980amber-commits/mcp-webhook-events
Example Code
importmcp_typesastcaps=t.ServerCapabilities(tools={}, events={})
print(caps.model_dump(by_alias=True, exclude_none=True))
# prints {'tools': {}} (the 'events' key is dropped)
Reproduced on main 2118f14f with mcp/mcp-types 2.3.0. The public capability models discard unknown keys, and the generated wire model discards them again through ServerRunner._serialize → serialize_server_result.
I prepared a fork candidate that preserves unknown keys only on ClientCapabilities and ServerCapabilities, including their generated schema-era models. Known fields still validate; nested known capability objects stay closed. The wire sieve still removes cross-era tasks on 2026-07-28 and extensions on older versions. This is not a hardcoded events field.
The regression checks actual Streamable HTTP discover bytes, modern and legacy SDK consumers, another unknown capability key, malformed known fields and an ordinary tools call. Independent remote replay passes the full pytest suite and the strict 100% coverage gate, plus the edited-test Ruff/Pyright checks.
This is a fork patch, not an upstream release or complete Events implementation. It does not register events/* methods, verify callbacks or deliver subscriptions; your middleware remains the workaround for installed releases without this fix. Prepared with AI assistance and independently reviewed/tested. No new PR is being opened or the automatically closed draft reopened.
Thanks for this. I tested it against mcp-webhook-events, which currently relies on the middleware workaround.
Setup: mcp and mcp-types 2.3.0 from PyPI, with the mcp-types part of your change applied (it applies cleanly to the 2.3.0 release). A real MCPServer with the library installed, our middleware removed, and events added through the low-level server's get_capabilities, the same way as your test. Checked with an actual server/discover call over Streamable HTTP (2026-07-28).
Stock 2.3.0: capabilities come back as prompts, resources, tools. No events.
2.3.0 with your fix: events, prompts, resources, tools.
With your fix and our middleware still in place: no clash, a single events key, and all 33 of our tests pass, including the HTTP ones.
So once something like this lands, servers can advertise events without any middleware, and I'll switch the library over to the get_capabilities route. It would be good to see it upstream.
Thanks, I independently replayed the published 2.3.0 wheels with the four-file types patch and your library at 2a5a005. All 33 library tests pass with both stock and patched packages. The stock pass uses your existing middleware; it does not mean the unpatched get_capabilities path works.
The patched no-middleware path advertises events: {"listChanged": false} over Streamable HTTP. With both enabled, your middleware's capabilities["events"] = {} wins after serialization, without a duplicate key. A second custom capability, including a nested null, survives too. Malformed known fields still fail validation and ordinary tools calls work.
This second replay used Python 3.14.8/Pydantic 2.13.5; the earlier replay used Python 3.12.3. The installed runner is unchanged. The types-only fork change remains a release-specific compatibility patch, not an upstream release or an Events method implementation. Prepared with AI assistance and independently replayed.
Thanks for taking the time to replay all that, it's really appreciated. Good to know the patched route works without the middleware and that the two don't clash. If the change lands upstream I'll switch the library over to get_capabilities and keep the middleware for older SDK versions.
Activity
epistemedeus commentedon Oct 4, 2026
Reproduced on main
2118f14fwith mcp/mcp-types 2.3.0. The public capability models discard unknown keys, and the generated wire model discards them again throughServerRunner._serialize→serialize_server_result.I prepared a fork candidate that preserves unknown keys only on
ClientCapabilitiesandServerCapabilities, including their generated schema-era models. Known fields still validate; nested known capability objects stay closed. The wire sieve still removes cross-erataskson 2026-07-28 andextensionson older versions. This is not a hardcodedeventsfield.The regression checks actual Streamable HTTP discover bytes, modern and legacy SDK consumers, another unknown capability key, malformed known fields and an ordinary tools call. Independent remote replay passes the full pytest suite and the strict 100% coverage gate, plus the edited-test Ruff/Pyright checks.
This is a fork patch, not an upstream release or complete Events implementation. It does not register
events/*methods, verify callbacks or deliver subscriptions; your middleware remains the workaround for installed releases without this fix. Prepared with AI assistance and independently reviewed/tested. No new PR is being opened or the automatically closed draft reopened.s1980amber-commits commentedon Oct 4, 2026
Thanks for this. I tested it against mcp-webhook-events, which currently relies on the middleware workaround.
Setup: mcp and mcp-types 2.3.0 from PyPI, with the mcp-types part of your change applied (it applies cleanly to the 2.3.0 release). A real
MCPServerwith the library installed, our middleware removed, andeventsadded through the low-level server'sget_capabilities, the same way as your test. Checked with an actualserver/discovercall over Streamable HTTP (2026-07-28).prompts,resources,tools. Noevents.events,prompts,resources,tools.eventskey, and all 33 of our tests pass, including the HTTP ones.So once something like this lands, servers can advertise
eventswithout any middleware, and I'll switch the library over to theget_capabilitiesroute. It would be good to see it upstream.epistemedeus commentedon Oct 4, 2026
Thanks, I independently replayed the published 2.3.0 wheels with the four-file types patch and your library at
2a5a005. All 33 library tests pass with both stock and patched packages. The stock pass uses your existing middleware; it does not mean the unpatchedget_capabilitiespath works.The patched no-middleware path advertises
events: {"listChanged": false}over Streamable HTTP. With both enabled, your middleware'scapabilities["events"] = {}wins after serialization, without a duplicate key. A second custom capability, including a nested null, survives too. Malformed known fields still fail validation and ordinary tools calls work.This second replay used Python 3.14.8/Pydantic 2.13.5; the earlier replay used Python 3.12.3. The installed runner is unchanged. The types-only fork change remains a release-specific compatibility patch, not an upstream release or an Events method implementation. Prepared with AI assistance and independently replayed.
s1980amber-commits commentedon Oct 4, 2026
Thanks for taking the time to replay all that, it's really appreciated. Good to know the patched route works without the middleware and that the two don't clash. If the change lands upstream I'll switch the library over to
get_capabilitiesand keep the middleware for older SDK versions.