Skip to content

feat: Upgrade apps/mcp-server to MCP SDK v2 and protocol revision 2026-07-28 - #1099

Draft
mistryrn wants to merge 5 commits into
mainfrom
feat/mcp-v2-migration
Draft

feat: Upgrade apps/mcp-server to MCP SDK v2 and protocol revision 2026-07-28#1099
mistryrn wants to merge 5 commits into
mainfrom
feat/mcp-v2-migration

Conversation

@mistryrn

@mistryrn mistryrn commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Upgrades apps/mcp-server to MCP SDK v2 and protocol revision 2026-07-28, which removes protocol sessions, the initialize handshake, and server-initiated requests, so the transport and the confirm-before-execute flow both had to be rebuilt rather than ported. Breaking for consumers: the endpoint serves 2026-07-28 only, and every SDK client negotiates the 2025 era by default, so a host must opt into modern negotiation explicitly.

Please refer to .dev/docs/mcp-sdk-v2-changes.md for a short summary of the key changes in MCP v2 and how they impacted our MCP Server.

Issues

  • N/A

Description of Changes

MCP Server

  • Upgraded to @modelcontextprotocol/{server,node}@2 and protocol revision 2026-07-28
  • Dropped support for 2025-era clients, because that path cannot carry execute_query's confirmation: the SDK's legacy shim reads capabilities declared at initialize, which per-request serving never sees
  • Replaced Express with plain node:http, keeping the SDK's Host and Origin guards and re-adding the 100kb body cap express.json() used to provide
  • Rebuilt confirm-before-execute on the multi-round-trip flow: execute_query returns an input_required result and the client re-invokes the tool with the answer, since servers can no longer initiate requests
  • Refused clients that do not declare elicitation, rather than executing unconfirmed, which was the last route to running a query nobody approved
  • Bound the approval to the query it approved, by sealing an HMAC-signed digest of the built query into requestState, which travels through the client and returns as untrusted input
  • Published ttlMs and cacheScope on the six cacheable results, and read serverInfo.version from the package manifest now that the revision stamps it onto every result
  • Deleted http/app.ts and utils/inMemoryEventStore.ts, both of which existed only to manage sessions the revision removed

Integration Tests

  • Pinned the test client to 2026-07-28, which no SDK client negotiates by default, so the suite would otherwise have covered the wrong wire era silently
  • Replaced the ping liveness probe with server/discover

Documentation

  • Added .dev/docs/mcp-sdk-v2-changes.md, a short read on what changed and why, and mcp-sdk-v2-upgrade-plan.md, the implementation record behind each decision
  • Updated apps/mcp-server/README.md and the root CHANGELOG.md, and set "protocolEra": "modern" in mcp-inspector.json, without which the MCP Inspector negotiates 2025-era and is refused

Special Instructions

Before running these changes, you will need to install the latest dependencies and rebuild your local modules:

npm ci                 # dependencies changed: the v1 SDK and Express are gone
npm run modules:build  # rebuild workspace modules

Any MCP client used to test this branch must pin protocol revision 2026-07-28; the endpoint refuses anything else.

New Environment Variables

  • MCP_ALLOWED_HOSTS: hostnames clients use to reach the server, matched against Host for DNS rebinding protection. Required whenever MCP_HOST is not loopback, which the 0.0.0.0 default is not, or the server exits at startup.
  • MCP_ALLOWED_ORIGINS: browser origins allowed to call the server.
  • MCP_MAX_BODY_BYTES: largest request body accepted, default 102_400.
  • MCP_REQUEST_STATE_SECRET: signs query confirmations. Optional at a single replica, required across several.

For full details refer to the environment variable table in apps/mcp-server/README.md and apps/mcp-server/.env.schema.

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Automated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

…n `2026-07-28`

* Replaced `@modelcontextprotocol/sdk@1` with `@modelcontextprotocol/{server,node}@2` and dropped Express, serving the web-standard handler on plain `node:http` through `toNodeHandler`
* Served protocol revision `2026-07-28` only, with `legacy: 'reject'`, rather than falling back to stateless 2025-era serving
* Capped request bodies at the `100kb` `express.json()` used to apply, since serving on `node:http` removes that parser and the SDK replaces neither it nor the limit
* Added `MCP_ALLOWED_HOSTS` and `MCP_ALLOWED_ORIGINS`, and made a routable bind with no Host allowlist fail at startup rather than warn as the SDK does
* Disabled confirm-before-execute in `execute_query`, which the next commit rebuilds on the multi-round-trip flow
* Pinned the integration-test client to `2026-07-28`, since `@modelcontextprotocol/client@2` negotiates the 2025 era by default and the suite would otherwise have covered the wrong wire era silently
* Replaced the `ping` liveness probe with `server/discover` and loosened a prompt-argument assertion, both of which this revision changed out from under the suite
* Replaced `execute_query`'s push-style `elicitInput()` call with an `input_required` return, so confirmation survives revision `2026-07-28` removing the server-to-client request channel
* Refused clients that do not declare `elicitation` rather than executing unconfirmed
* Restored the two confirmation tests unchanged
* Added unit coverage for the confirmation states the integration suite cannot reach, because a well-behaved client never sends them
* Consolidated the test clients onto one helper, dropping `withModernNegotiation` now that `connectMcpClient` can configure a client before it connects
* Sealed a digest of the built query, its variables and its endpoint into `requestState`, so an agent cannot show one query for confirmation and re-enter with another
* Refused an answer carrying no `requestState` exactly like a mismatched one, since nothing forces a client to echo it and comparing only when present would leave the binding opt-out at the caller's discretion
* Refused rather than re-asked on both failures, which would otherwise hand a caller an unlimited retry loop against the confirmation gate
* Installed `codec.verify` as `ServerOptions.requestState.verify`, so a forged, expired or wrongly bound value is refused at the seam and never reaches the tool
* Built the codec once per process and passed it through `McpServerDeps` rather than inside the per-request server factory, which would mint and verify the two rounds of one confirmation under different keys
* Added `MCP_REQUEST_STATE_SECRET`, falling back to a per-process key and a startup warning, so a single replica needs no configuration and an operator running several is told why confirmations fail
* Corrected `SERVER_INSTRUCTIONS`, which still told the model that a client without elicitation gets no prompt, describing the branch the previous commit replaced with a refusal
…2026-07-28` added

* Published freshness hints on all six cacheable results, so the SDK default stops telling every client to cache nothing
* Read `serverInfo.version` from the package manifest, now that the revision stamps it onto every result rather than onto a handshake that no longer exists
* Pinned `server/discover`'s instructions and capabilities, `tools/list` ordering, and every cache hint as it reaches the wire
* Recorded Arranger introspection caching as tech debt, since confirmation becoming two requests doubled the round trips a confirmed query costs and no commit here owns it, and removed two entries the SDK v2 migration had already obsoleted
* Stated the served revision and the modern-negotiation a consumer must opt into in `apps/mcp-server/README.md`, which still described the v1 SDK and named neither, and noted that `execute_query` refuses a client that cannot elicit
* Recorded every operator-facing change of the upgrade in `CHANGELOG.md`, headlined by 2025-era clients no longer being served at all
* Set `protocolEra: "modern"` in `mcp-inspector.json`, without which the Inspector negotiates 2025-era and this endpoint refuses it
* Added `mcp-sdk-v2-changes.md` for a developer who needs what changed and why but not the alternatives rejected, covering the single-revision endpoint, the loss of sessions, `node:http` replacing Express, confirmation becoming two requests, the `requestState` binding, and the cache hints
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.

1 participant