Repository navigation
1.0.81-1 sends legacy initialize after successful modern server/discover, causing -32022 #4525
Description
Describe the bug
Copilot CLI 1.0.81-1 fails MCP initialization against a stdio server using the Python MCP SDK 2.0.0 dual-era runner.
The CLI opens the connection with a modern server/discover probe whose _meta declares io.modelcontextprotocol/protocolVersion: 2026-07-28. The server replies with a valid successful DiscoverResult (resultType: "complete", supportedVersions: ["2026-07-28"]). The CLI then sends a legacy initialize requesting 2025-11-25 on that same connection, and the server rejects it with -32022.
Per the stdio backward-compatibility rules, a successful DiscoverResult identifies a modern server; legacy initialize fallback is only for a non-modern error or timeout. The CLI declares two protocol eras on one connection.
Related to #4370, but exercises the other probe outcome: #4370 concerns fallback after a non-modern error; this occurs after a successful modern DiscoverResult.
Affected version
1.0.81-1
Verified working on 1.0.80, 1.0.80-1 and 1.0.81-0 with the same server and config. 1.0.81-0 (works) and 1.0.81-1 (fails) were both run via node index.js from the same cache layout, so the difference is the version, not the invocation path. 1.0.81-1 also fails when run as the native binary.
Steps to reproduce the behavior
- Configure a stdio MCP server that implements
server/discover. Reproduced withscrapling 0.4.14(scrapling mcp), resolved dependencymcp==2.0.0. - Run Copilot CLI 1.0.81-1.
- The server's tools are missing. The CLI log shows:
failed to initialize MCP client: JSON-RPC error: -32022: connection is serving the 2026-07-28 protocol; the initialize handshake is not accepted({"supported":["2026-07-28"],"requested":"2025-11-25"})
Expected behavior
After a successful DiscoverResult, the CLI should select a mutually supported advertised version and continue in that era without sending legacy initialize on that connection. Legacy initialize fallback should occur only when the probe returns a non-modern error or times out.
Additional context
Wire capture on 1.0.81-1 (abridged — capabilities and instructions elided, marked "..."):
// 1. client -> server
{"jsonrpc":"2.0","id":0,"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"copilot-cli","version":"0.0.0"},
"io.modelcontextprotocol/clientCapabilities":"..."}}}
// 2. server -> client (valid modern discovery success)
{"jsonrpc":"2.0","id":0,"result":{"resultType":"complete",
"supportedVersions":["2026-07-28"],"capabilities":"...","instructions":"..."}}
// 3. client -> server (legacy handshake on a modern-locked connection)
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25",
"capabilities":"...","clientInfo":{"name":"copilot-cli","version":"0.0.0"}}}
// 4. server -> client (verbatim)
{"jsonrpc":"2.0","id":1,"error":{"code":-32022,
"message":"connection is serving the 2026-07-28 protocol; the initialize handshake is not accepted",
"data":{"supported":["2026-07-28"],"requested":"2025-11-25"}}}Server side, the Python SDK locks the connection's era from the first message: a non-initialize opening request carrying the reserved io.modelcontextprotocol/protocolVersion key selects the modern runner, which then rejects initialize (mcp/server/runner.py).
Environment: Linux x64, Node v24.15.0, server scrapling 0.4.14 on mcp 2.0.0.
Activity
MilkyWay008 commentedon Aug 19, 2026
Saw the -32022, and the fact it works on 81-0 but not 81-1 with the same server is all the evidence you need, 81-1 dropped the legacy initialize onto a connection that already negotiated the modern protocol, which breaks the stdio backward-compat rules. If you just need it unblocked, pin to 1.0.81-0, you already confirmed that path works with your scrapling + mcp 2.0.0 setup.
TheDr1ver commentedon Aug 26, 2026
Independent confirmation on a different server and a newer CLI build, in case it helps bound the regression.
Still broken on
1.0.81-3(you reported1.0.81-1), and also withmcp==2.1.1, not just2.0.0. For what it's worth,mcp/server/runner.pyis byte-identical between2.0.0and2.1.1, so the server side of this hasn't moved.Reproduced against
jupyter-mcp-server==2.0.0(which pinsmcp[cli]>=2,<3) — so this is not specific toscrapling. Captured with a transparent stdio tee-proxy between the CLI and the real server:Minimal repro with no third-party server — this is enough to trigger it, which suggests any Python mcp-2.x stdio server is affected:
Sending the two frames above to that server on
mcp==2.1.1yields the same-32022.Workaround for anyone blocked, since the server side can't fix this — force the dependency back to the v1 SDK so the server has no modern era, answers
server/discoverwith a benign-32602, and stays legacy:The
--with 'mcp[cli]<2'part matters: v1-line packages often pinmcp>=1.10.1unbounded and will otherwise resolve straight back to 2.x.Rossbro2 commentedon Aug 31, 2026
Another reproduction, plus timing data that I think re-frames this issue — and explains why it hits some servers and not others.
Copilot CLI 1.0.82, Windows, Python servers on
mcp==2.1.1in Docker. Same-32022, all five servers.The fallback is not happening after a successful
DiscoverResult. The issue body describes the CLI sendinginitializeafter the modern probe succeeds, i.e. a backward-compatibility violation. The wire order says otherwise — and @TheDr1ver's capture above shows the same thing, both requests sent before either response. With timestamps (stdio tee-proxy, seconds from process spawn):initializegoes out ~0.8s before theDiscoverResultarrives. This is the timeout path, which the stdio backward-compatibility rules explicitly permit. The CLI's own runtime strings confirm the design:So the client isn't violating the fallback rule — it's racing server startup, then discarding a valid result it did receive.
Two controls confirm latency is the variable:
server/discoverin 116ms works perfectly — modern path end to end,tools/listandtools/callboth fine,initializenever sent. The CLI's 2026-07-28 support is not the problem.python -c pass) in the same container still answers only at ~0.85s — already past the deadline. Measured floor fordocker run+ interpreter start is ~0.96s.So this is a startup-latency race, and any containerized Python stdio server on mcp 2.x is systematically affected regardless of how fast its own code is. That likely also explains the 1.0.81-0 vs 1.0.81-1 difference and why fast local servers appear unaffected.
Suggested fix, in preference order:
DiscoverResult. The CLI already has it in hand when it fails. If it arrives beforeinitializeis answered, adopt the modern era instead of discarding it. Fixes this without any timeout tuning.-32022as "server is modern, retry on the modern path" rather than fatal, since the error names the supported versions.Worth noting the server side genuinely cannot fix this: [python-sdk#3151](modelcontextprotocol/python-sdk#3151) proposed that an enveloped
server/discover"is a probe that is answered but pins nothing, so a client that probes and falls back toinitializestill finds the handshake available" — exactly this scenario. It was closed unmerged in favor of [#3152](modelcontextprotocol/python-sdk#3152), which pins the era on the opening request. So the era lock is intentional upstream.Confirming the workaround in this thread: pinning
mcp<2resolves it. mcp 1.x has no modern era, answers the late probe with a benign-32602, and the fallbackinitializethen succeeds.jasonko commentedon Sep 1, 2026
Another reproduction, on a platform not yet represented in this thread, plus a data point that constrains the recommended workaround.
macOS 26.6.2 (arm64), Copilot CLI 1.0.82, no container. Two
uvx-launched stdio servers onmcp==2.1.1. Same-32022, same wire order as @Rossbro2's capture — so the startup-latency race is not Docker- or Windows-specific. Plainuvxprocess spawn is enough to miss the deadline.Timestamped tee-proxy capture (seconds from spawn):
Confirming @Rossbro2's re-framing: the
DiscoverResultis valid and in hand when the CLI fails. The CLI then retried 4 times, each spawning a fresh process, and gave up (exit 143).The latency is not in the server's own code. The failing server logs
Loaded 4 cookiesbefore any MCP traffic — colduvxresolution plus interpreter start is the whole budget. A second server in the same config (workspace-mcp) starts faster and intermittently wins the race: across restarts it sometimes connected and sometimes emitted the same-32022. Same machine, same CLI, same config — which I think is good evidence this is timing and not server behaviour.mcp<2is not universally available as a workaround. This is the part I'd flag. The server I need (parentsquare-mcp 0.4.0) declaresmcp>=2.0.0,<3, so pinning back makes the environment unresolvable:uvexits 1 before the handshake and the server never starts. Pinning within 2.x doesn't help either —mcp/server/runner.pycarries the modern era in 2.0.1 as well. So for any server that has already moved tomcp>=2, there is currently no version-pin escape.Still present in
1.0.83-1(prerelease, published today).app.jsstill contains theserver/discoverprobe, and I found no changelog entry addressing the negotiation between 1.0.81 ("Ship MCP 2026-07-28 support to CLI, SDK, IDE, and in-memory clients") and 1.0.83-1.Client-side workaround that doesn't require touching the server's pin. Since the era is decided by the first request the server sees, a thin stdio wrapper can answer the probe itself and never forward it, so
initializeis always the opening frame and the connection is deterministically legacy:has_modern_envelopejust checks forparams._meta["io.modelcontextprotocol/protocolVersion"]. Config becomes"command": "python3", "args": ["mcp-legacy-shim.py", "<real command>", ...].Both servers connected cleanly on the first restart after applying it — 48 and 72 tools,
tools/callverified on each, and no-32022in the logs (previously the failing server never survived a single startup). It gives up the modern era entirely, so it's a stopgap rather than a fix, but it's usable whenmcp<2isn't.Fwiw @Rossbro2's suggestion 1 (accept a late but valid
DiscoverResult) would fix this cleanly — the CLI has the successful result in hand in the very same read as the error it fails on.VishwasGajawada commentedon Sep 2, 2026
Does anyone know how to escalate this issue? Is anyone already working on fixing this from copilot cli client side?
Similar to @jasonko , I had come up with another workaround on my mcp server which basically makes the mcp server talk only in old protocol even in mcp 2.x.x
Paul-Eccleston-i2 commentedon Sep 3, 2026
Yep this breaks most of the AWS MCP Servers, please fix this!.
Workaround for now is to roll back to v1.0.80.
Rossbro2 commentedon Sep 14, 2026
Confirming this is genuinely fixed in 1.0.83 (final), for anyone still running a workaround.
The close came with no comment and I couldn't find a changelog entry for it, so I re-tested rather than assume. Same setup as my earlier capture — Windows, Python MCP servers in Docker — now on
mcp==2.2.0and CLI 1.0.83:The client now waits for and accepts the discover result instead of racing it. No legacy
initializeis sent at all, and the connection stays on the modern path.There's real margin, too. My slowest server (does a network call during startup) answers at 1.99s and is still accepted — roughly 3x the old ~500–650ms budget that nothing containerized could meet. All servers connect and list tools.
Note the boundary is 1.0.83 final, not
1.0.83-1. @jasonko reported1.0.83-1still broken on Sept 1, and the final released Sept 4, so the fix landed late in that cycle. If you pin CLI versions, treat the prerelease as below the floor (standard semver ordering).You can drop the workarounds on 1.0.83+:
mcp-legacy-shim.pyand @VishwasGajawada's_has_modern_envelopepatch — both gave up the modern era entirely, so removing them gets you 2026-07-28 rather than just restoring service.mcp<2pins can be lifted, which also unblocks packages likeparentsquare-mcpthat requiremcp>=2and had no pin-based escape at all.Since the fix isn't documented anywhere I could find, 1.0.83 is an empirically-determined floor rather than a stated guarantee — worth verifying on your own setup before removing a workaround in CI.
Thanks for getting this one landed.