Skip to content

build(deps): move @modelcontextprotocol/server and client to 2.1.0 together - #242

Merged
mbeacom merged 5 commits into
mainfrom
deps/mcp-sdk-2-1-0
Sep 28, 2026
Merged

mbeacom merged 5 commits into
mainfrom
deps/mcp-sdk-2-1-0

Conversation

@mbeacom

@mbeacom mbeacom commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

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 fails clean-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: StdioServerTransport now 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.ts cases 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 the server/discover result. Those cases now read every response they wait for before closing stdin, which is what smoke-node.mjs and smoke-container.mjs already did. This PR also:

  • asserts the new lifecycle: stdin EOF gives exit 0 and empty stderr, on both eras;
  • adds a case for a client hanging up with a request in flight: the process exits cleanly and nothing but JSON-RPC reaches stdout.

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 declares zod ^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.js under 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.

  • default config: initialize @ 2025-11-25. protocolEra: "modern" and "auto": server/discover @ 2026-07-28.
  • All four tools (search_decisions query bun, get_decision 0018, get_decision_context packages/mcp/src/server.ts, list_superseded) returned the same outcomes and byte-identical content and structuredContent across all three configs.
  • The same calls were also byte-identical to a 2.0.0 build of main on both eras.
  • An SDK 2.1.0 client pinned to 2026-07-28 negotiated 2026-07-28 and got the same results.

Checklist

  • Commits are DCO signed off (git commit -s).
  • If this changes a recorded decision in 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.
  • If the schema changed, … Not applicable: no schema change.
  • If packages/ci/src or @adrkit/core changed, … Not applicable: neither changed, and a build under Bun 1.3.14 leaves packages/ci/dist byte-identical (the CI bundles contain no MCP code).
  • New or changed behavior is covered by tests, and each test was observed failing before it passed. The two rewritten cases are exactly build(deps): Bump @modelcontextprotocol/server from 2.0.0 to 2.1.0 #225's CI failures. Two mutations were each observed failing the intended tests: main returning 1 (3 fail), and closing stdin before reading (2 fail).
  • bun run typecheck && bun run build && bun test && bun run lint pass (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, and node scripts/smoke-node.mjs (both eras).

Notes for reviewers

  • User-visible, called out in CHANGELOG and packages/mcp/README.md: a script that pipes requests into adrkit-mcp and 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.
  • Not exercised: the Inspector web UI, and any npm-published artifact (none exists yet). The post-release Inspector run in ADR-0018 should be repeated against the next published @adrkit/mcp.
  • Doc drift, fixed in the second commit: AGENTS.md said the Inspector has no CLI flag for the era. Inspector 2.8.0 adds --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.
  • Inspector 2.8.0 also opens subscriptions/listen on 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".
  • feat(schema)!: publish ADR schema v0.2.0 (RFC 3339 seconds, zod ≥4.5), retain v0.1.0 #241 (zod ^4.6.5 / schema v0.2.0): orthogonal. Core/server/client 2.1.0 all still declare zod ^4.2.0, and there is no textual overlap beyond bun.lock, which may need a trivial rebase for whichever PR lands second.
  • No gate paths touched (scripts/, .github/workflows/, packages/ci/ unchanged).

…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>
Copilot AI lite review requested due to automatic review settings September 28, 2026 02:20
…tions/listen claim

Signed-off-by: Mark Beacom <m@beacom.dev>
@github-actions

Copy link
Copy Markdown

Decisions governing this change

  • 0001 — Record architecture decisions as versioned markdown in git
    • via path: docs/adr/**
  • 0007 — Isolate integrations as optional adapters and build only against public surfaces
    • via path: packages/*/package.json
  • 0016 — Require every check to be observed failing before it counts as coverage
    • via path: packages/*/test/**
  • 0017 — Keep dependency audit scope explicit and release-scoped
    • via path: packages/*/package.json
  • 0018 — Adopt MCP SDK v2 and serve protocol revision 2026-07-28 dual-era
    • via path: packages/mcp/**

Active proposals touching this change

These are not yet ratified and do not bind this change:

  • 0040 — Keep derived surfaces in lockstep with three mechanisms matched to three classes of drift (proposed)
    • via path: AGENTS.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved compatibility and ADR conflicts, plus test reliability issues, remain.

Review effort: Lite
Findings: 2 High severity · 2 Medium severity · 1 Low severity

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.

Comment on lines +415 to +421
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`

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/mcp/package.json
Comment on lines +49 to +53
"@modelcontextprotocol/server": "2.1.0",
"zod": ">=4.4.3 <4.5"
},
"devDependencies": {
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/client": "2.1.0",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/mcp/test/bin.test.ts
Comment thread packages/mcp/test/bin.test.ts Outdated
Comment on lines +318 to +321
// 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, []);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 mbeacom self-assigned this Sep 28, 2026
@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>
@mbeacom
mbeacom merged commit e8bdde1 into main Sep 28, 2026
17 checks passed
@mbeacom
mbeacom deleted the deps/mcp-sdk-2-1-0 branch September 28, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants