Repository navigation
build(deps): move @modelcontextprotocol/server and client to 2.1.0 together - #242
Conversation
…gether Dependabot split the bump into #225 (server) and #226 (client), but both depend on @modelcontextprotocol/core, so they have to move as one. Both stay exact-pinned. bun.lock changes only in the three SDK entries. 2.1.0 includes typescript-sdk#2494: StdioServerTransport now closes when stdin ends, and requests still in flight are aborted and not answered. That broke two raw-pipe tests in bin.test.ts that wrote every frame and then closed stdin before reading anything; this is #225's CI failure. Those tests now read the responses they wait for before closing stdin, as the smoke scripts already did, and they assert a clean exit (0, empty stderr) on EOF. A new case covers a client hanging up with a request in flight. The shutdown and read-before-close assertions were each observed failing under a mutation (ADR-0016). Records the dual-era re-verification in ADR-0018: headless MCP Inspector 2.8.0 on default, modern and auto; all four tools; byte-identical across eras and against a 2.0.0 build. Adds a changelog entry and a README note on the stdin-EOF lifecycle. Signed-off-by: Mark Beacom <m@beacom.dev>
…tions/listen claim Signed-off-by: Mark Beacom <m@beacom.dev>
Decisions governing this change
Active proposals touching this changeThese are not yet ratified and do not bind this change:
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved compatibility and ADR conflicts, plus test reliability issues, remain.
Review effort: Lite
Findings: 2
Open (5)
What changed in this PR
Upgrades the MCP server and client dependencies to 2.1.0 and adapts stdio lifecycle tests and documentation for EOF shutdown behavior.
Changes:
- Updates MCP dependency pins and lockfile.
- Adds EOF and in-flight request coverage.
- Documents lifecycle changes and records ADR-0018 verification.
| File | Summary |
|---|---|
packages/mcp/test/bin.test.ts |
Updates stdio subprocess tests and lifecycle coverage. |
packages/mcp/README.md |
Documents stdin EOF behavior. |
packages/mcp/package.json |
Pins MCP dependencies to 2.1.0. |
docs/adr/0018-adopt-mcp-sdk-v2-and-serve-protocol-revision-2026-07-28-dual-era.md |
Records SDK verification and compatibility guidance. |
CHANGELOG.md |
Announces dependency and lifecycle changes. |
bun.lock |
Updates resolved SDK packages and integrity hashes. |
| On this record's "how we would know this was wrong" clause: a 2025-era **client** | ||
| sees no change. It keeps stdin open until it has its responses, and every response | ||
| below is byte-identical to 2.0.0. A **script** that pipes frames in and closes | ||
| stdin at once does see a change, because it can lose trailing responses. That | ||
| behaviour was never part of the protocol, and the SDK now follows the binding's | ||
| shutdown rule. This record therefore treats it as outside what it guarantees, and | ||
| flags it as a user-visible note in the changelog and in `packages/mcp/README.md` |
There was a problem hiding this comment.
This is the substantive question, and it's explicitly left to the maintainer; the PR description's Notes for reviewers say so. Facts: every response to a client that keeps stdin open until it has read its responses is byte-identical to the 2.0.0 build, on both eras (the Inspector 2.8.0 table in the appended section). That includes every MCP client exercised here and both repo smoke scripts. Only a writer that closes stdin before reading loses trailing responses, and the stdio binding treats stdin close as shutdown. adrkit can't preserve the old EOF behaviour without hand-wiring the transport instead of using serveStdio, which ADR-0018 relies on for the modern era. So the realistic options are: (a) accept, optionally with a proposed record that narrows the clause to protocol-conforming clients; or (b) hold 2.1.x with a Dependabot ignore. @mbeacom to decide.
There was a problem hiding this comment.
Resolved by the maintainer's decision, recorded in 3070671. @mbeacom accepted the change, and ADR-0018 now carries a marked Amendment ratified by @mbeacom (2026-09-28). "Client" in How we would know this was wrong means one that follows the MCP stdio binding and keeps stdin open until it has read its responses. A writer that closes stdin at once can lose trailing responses since typescript-sdk#2494. That is accepted as outside the guarantee, because avoiding it would mean replacing serveStdio, which the 2026-07-28 era depends on. A protocol-following 2025-era client that sees a changed response still falsifies the record. The ratified text is kept, with pointers beside it.
| "@modelcontextprotocol/server": "2.1.0", | ||
| "zod": ">=4.4.3 <4.5" | ||
| }, | ||
| "devDependencies": { | ||
| "@modelcontextprotocol/client": "2.0.0", | ||
| "@modelcontextprotocol/client": "2.1.0", |
There was a problem hiding this comment.
I'm flagging this for the maintainer rather than deciding it here. My reading: .github/dependabot.yml (lines 90-95) already treats a @modelcontextprotocol/* bump as expected under ADR-0018, provided the Inspector dual-era run is repeated. That run is recorded in the appended section. The 2.0.0 in the Decision text reads as the version the migration landed on, not a standing prohibition. Precedent: #236 moved ADR-0018's stated zod range (^4.2.0 → >=4.4.3 <4.5) without a superseding record. If @mbeacom prefers a short proposed record amending ADR-0018 for 2.1.0, it can be added here before merge.
There was a problem hiding this comment.
Resolved in 3070671. The same ratified amendment moves the exact pin to 2.1.0 for server and client together. The Decision's 2.0.0 named the version adopted, not a ceiling. The rule it implies stands: each SDK bump stays out of Dependabot's grouped updates and lands only after the two-era Inspector run is repeated and appended to ADR-0018, which this PR does. A pointer sits beside the Decision sentence.
| // Awaiting no ids closes stdin straight after the write. Whether id 2 is answered | ||
| // is the SDK's call (since 2.1.0 it is aborted); what adrkit owns is that the | ||
| // hang-up is a shutdown, not a transport error, and nothing but JSON-RPC leaks. | ||
| const { messages, exitCode, stderr } = await rawStdioExchange(repo, frames, []); |
There was a problem hiding this comment.
Agreed, the test couldn't guarantee id 2 was in flight. Fixed in 3536105. It's renamed to "a client that hangs up straight after writing gets a prompt, clean exit", and the comment now says outright that it asserts neither that id 2 was in flight nor whether it was answered, since that is SDK timing. What it pins is what adrkit owns, whichever way the race falls: exit 0, empty stderr, JSON-RPC-only stdout. I didn't try to force the in-flight case, because making that deterministic would mean hooking the tool handler inside the child. The abort itself is the SDK's documented behaviour (typescript-sdk#2494).
…p test to what it proves Signed-off-by: Mark Beacom <m@beacom.dev>
Resolve conflicts with #241 (schema v0.2.0, zod ^4.6.5): - packages/mcp/package.json and bun.lock: server 2.1.0 from this branch, zod ^4.6.5 from main. One @modelcontextprotocol/core@2.1.0 resolves. - CHANGELOG.md: keep both Changed entries, #241's breaking schema entry first. Verified on Bun 1.3.14: frozen install accepted and plain install stable; typecheck, build, lint, check:clean-clone/deps/freeze-hashes/changelog/ stale-refs/doc-pins pass; bun test 3168 pass; smoke-node passes on both MCP protocol eras; packages/ci/dist and schema/ equal main byte for byte. Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom accepted the stdin-close change in review of #242. Record it as a marked amendment rather than rewriting ratified text: - the exact pin moves to 2.1.0; 2.0.0 named the adopted version, not a ceiling, and each bump still needs the two-era Inspector run appended; - 'client' in 'How we would know this was wrong' means one following the MCP stdio binding. A writer that closes stdin at once can lose trailing responses since typescript-sdk#2494; that is accepted as outside the guarantee, because avoiding it means replacing serveStdio. Pointers are added beside the two passages amended. Signed-off-by: Mark Beacom <m@beacom.dev>



What and why
Moves
@modelcontextprotocol/server(runtime) and@modelcontextprotocol/client(dev-only test driver) from 2.0.0 to 2.1.0 together, both still exact-pinned.Supersedes #225 and #226. I recommend closing both. They share
@modelcontextprotocol/core, and #225 on its own failsclean-clone-builds. The failure is not the split, though. It comes from a real 2.1.0 behaviour change, described next.The one 2.1.0 change that reaches adrkit
typescript-sdk#2494:
StdioServerTransportnow closes itself when stdin ends, following the stdio binding's "SHOULD exit promptly when stdin is closed". Requests still in flight at EOF are aborted and not answered.Two
bin.test.tscases wrote every frame and then closed stdin before reading anything. Under 2.1.0 they lose responses; in #225's CI run that even included theserver/discoverresult. Those cases now read every response they wait for before closing stdin, which is whatsmoke-node.mjsandsmoke-container.mjsalready did. This PR also:0and empty stderr, on both eras;Everything else in 2.1.0 (HTTP body limits, OAuth/DPoP, scope challenges, request-id
0,Mcp-Name, client HTTP transports) is outside a local stdio server. zod interop is unchanged, since core 2.1.0 still declareszod ^4.2.0.Dual-era dogfood (ADR-0018), recorded in an appended section of ADR-0018
The Inspector runs were against the built
packages/mcp/dist/bin.jsunder Node 22, on this repo's corpus of 42 records, through official MCP Inspector 2.8.0 in headless CLI mode. A tee wrapper recorded every frame the Inspector sent.initialize@2025-11-25.protocolEra: "modern"and"auto":server/discover@2026-07-28.search_decisionsquerybun,get_decision0018,get_decision_contextpackages/mcp/src/server.ts,list_superseded) returned the same outcomes and byte-identicalcontentandstructuredContentacross all three configs.mainon both eras.2026-07-28negotiated2026-07-28and got the same results.Checklist
git commit -s).docs/adr/, an ADR is added or supersedes the affected record. Not applicable: ADR-0018's decision text is untouched, and only a dated verification section is appended, following its existing post-release section.packages/ci/srcor@adrkit/corechanged, … Not applicable: neither changed, and a build under Bun 1.3.14 leavespackages/ci/distbyte-identical (the CI bundles contain no MCP code).mainreturning1(3 fail), and closing stdin before reading (2 fail).bun run typecheck && bun run build && bun test && bun run lintpass (Bun 1.3.14; 3148 pass / 0 fail). Also green:check:clean-clone,check:deps,check:freeze-hashes,check:changelog,check:stale-refs,check:doc-pins,check:site-grammar,adr lint,emit:manifest(no diff),bun install --frozen-lockfile, andnode scripts/smoke-node.mjs(both eras).Notes for reviewers
packages/mcp/README.md: a script that pipes requests intoadrkit-mcpand closes stdin straight away (printf … | adrkit-mcp) can now lose its trailing responses. Conforming MCP clients keep stdin open until they have their responses and are unaffected. ADR-0018's "how we would know this was wrong" clause mentions a 2025-era client observing a changed response. The appended section explains why I read pipe-and-close as outside that guarantee, but that reading is your call. The alternative is a Dependabot ignore for@modelcontextprotocol/*@2.1.x.@adrkit/mcp.--protocol-era legacy|auto|modern, and I observed it working. AGENTS.md now says so; ADR-0018's original sentence is left alone because it is dated to Inspector 2.0.0.subscriptions/listenon the modern era. The reply log shows no error and no reply before the Inspector disconnected; it cannot tell "still open" apart from "aborted at hang-up".zod ^4.2.0, and there is no textual overlap beyondbun.lock, which may need a trivial rebase for whichever PR lands second.scripts/,.github/workflows/,packages/ci/unchanged).