Skip to content

feat!: 3.0 — major dependency upgrades + MCP draft-spec forward-compat#56

Draft
SamMorrowDrums wants to merge 5 commits into
sammorrowdrums/mcp-spec-update-checkfrom
sammorrowdrums/3-0-major-upgrades
Draft

feat!: 3.0 — major dependency upgrades + MCP draft-spec forward-compat#56
SamMorrowDrums wants to merge 5 commits into
sammorrowdrums/mcp-spec-update-checkfrom
sammorrowdrums/3-0-major-upgrades

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Stacked on top of #55 (sammorrowdrums/mcp-spec-update-check). Targets that branch so the in-range bumps land first.

Summary

Refresh of the whole dependency tree to land 3.0, plus a forward-compatibility pass on the MCP draft spec (publishes end of month). The probe is hardened so a server upgrading its SDK across MCP spec revisions — 2025-06-182025-11-25draft — produces a clean diff when the public surface is unchanged. SDK v2 is not yet released so we don't take a dependency on it here — that will be a follow-up.

Major dependency bumps

  • zod ^3 → ^4 — breaking: z.record now requires (keyType, valueType); the one call site in src/probe.ts (custom-message response schema) was updated to z.record(z.string(), z.unknown()).
  • undici ^6 → ^8 — also updates the overrides block (resolves remaining v6 WebSocket advisories on top of chore: in-range dep updates + dist rebuild (supersedes #51) #55).
  • diff ^8 → ^9 — no code change required; the package isn't imported directly any more (the diff logic in src/diff.ts is custom).
  • @actions/core ^1 → ^3, @actions/exec ^1 → ^3, @actions/io ^1 → ^3
  • eslint ^9 → ^10, @eslint/js ^9 → ^10
  • typescript ^5 → ^6 — required adding "types": ["node"] to tsconfig.json (TS 6 no longer auto-includes ambient @types/node).
  • jest ^29 → ^30, @types/jest ^29 → ^30
  • @types/node ^22 → ^24 (CI still tests Node 20 + 22, action recipes are unchanged)
  • @vercel/ncc ^0.38 → ^0.44
  • prettier, eslint-config-prettier, typescript-eslint: latest

Kept at current major:

  • ts-jest stays on ^29.4.x — there is no v30 published yet, but 29.4.11 peer-depends on jest ^29 || ^30 and typescript >=4.3 <7, so it works with the new jest + ts.
  • @modelcontextprotocol/sdk stays on ^1.13.2 — v2 is not published.

package.json is bumped to 3.0.0-rc.0. dist/ was regenerated via npm run build (never hand-edited).

MCP draft-spec forward-compat

Targets the changes in the draft changelog without depending on SDK v2:

  • CacheableResult stripping (SEP-2461). normalizeProbeResult accepts { stripCacheHints } and removes top-level ttlMs / cacheScope from tools/list, prompts/list, resources/list, and resources/templates/list results before snapshotting.
  • initializeserver/discover (SEP-2575). Not renaming the snapshot file yet — SDK v2 isn't out. New CANONICAL_SNAPSHOT_NAMES table in probe.ts is wired in now so when the rename happens we map both spellings to the same initialize snapshot file. That way a server moving across the rename shows up as a content diff on one file instead of "removed + added".
  • capabilities.extensions (SEP-2589). No code change needed; InitializeInfo.capabilities is Record<string, unknown> with a comment confirming it stays open-ended.
  • Deterministic ordering. Already handled; doc comment now notes the draft mandates this.

Cross-spec-version diff cleanliness

This is the larger of the two probe-level changes. During the draft-spec rollout it's completely normal for the base ref to be on 2025-06-18 / 2025-11-25 and the branch to be on the draft. The diff should highlight intentional API surface changes, not protocol churn.

What now gets normalized away before snapshotting (in addition to CacheableResult above):

  • _meta protocol plumbing — an exact-key denylist is stripped from every _meta object at any depth: io.modelcontextprotocol/protocolVersion, clientInfo, clientCapabilities, subscriptionId, logLevel. Not by prefix — official extensions live under the same reserved namespace (MCP Apps' _meta.ui per SEP-1865, Tasks' io.modelcontextprotocol/related-task) and must round-trip. An emptied _meta is dropped entirely.
  • W3C trace context inside _metatraceparent, tracestate, baggage (transport-injected for OTel propagation) are stripped from _meta.
  • initialize envelope churnprotocolVersion and capabilities.experimental are excluded from the initialize diff body. Drift on those would otherwise dominate every cross-spec diff.

What is not normalized (intentionally):

  • serverInfo.version — the SDK version is a legitimate signal worth tracking.
  • Nested ttlMs / cacheScope that live inside a tool/prompt/resource definition (those would be part of the public surface, not envelope hints).
  • Any _meta key not on the exact denylist above — including the entire MCP Apps surface (_meta.ui) and vendor extensions (x.acme/*, etc.).

Protocol-version capture

The SDK does not expose the negotiated protocol version through a public getter. We attach (or wrap) transport.setProtocolVersion(...) before client.connect(...) — the SDK calls it after a successful initialize. The captured value lands on result.initialize.protocolVersion and is then propagated into TestResult.branchProtocolVersion / baseProtocolVersion in runner.ts. Works for both stdio and HTTP transports.

Reporter banner

When base vs branch negotiated different protocol versions, the report annotates the affected configuration with:

ℹ️ MCP protocol version changed: 2025-11-25draft. Protocol-level plumbing is normalized away; any diff below reflects real public-surface changes.

The PR summary surfaces the same drift at the very top, so reviewers immediately know the diff was taken across spec revisions even when the diff body is empty.

Test coverage

src/__tests__/probe.test.ts (new in this PR) covers:

  • normalizeProbeResult strip behaviour for ttlMs / cacheScope (top-level only).
  • _meta scrubbing of io.modelcontextprotocol/* and W3C trace-context keys, including the "drop empty _meta" path.
  • probeResultToFiles strips cache hints for all four list/templates endpoints and leaves initialize intact except for protocolVersion + capabilities.experimental.
  • Cross-version cleanliness suite that feeds the same logical server twice — once with a clean envelope (2025-11-25) and once with the draft envelope (CacheableResult + protocol _meta) — and asserts the normalized tools/list and initialize snapshots are byte-identical. A negative test confirms a real tools delta still produces a diff.

src/__tests__/reporter.test.ts (extended): unit tests for formatProtocolVersionBanner (null when versions match / either side unknown, formatted when they differ) and integration tests asserting both the markdown report and the PR summary surface the banner.

npm run check passes — typecheck + lint + prettier + 72 jest tests across 4 suites.

Follow-up (not in this PR)

  • Adopt @modelcontextprotocol/sdk v2 once released, switch the SDK call from initialize to server/discover, and re-evaluate whether client.getProtocolVersion() lands publicly so we can drop the transport-hook trick.

Notes

Follow-up: MCP Apps + extension preservation (commit 8181d45)

Caught a regression in my own _meta scrubber before merge: the original implementation matched io.modelcontextprotocol/* by prefix, which would silently delete the entire MCP Apps surface (_meta.ui per SEP-1865) and Tasks' io.modelcontextprotocol/related-task. Flipped to an exact-key denylist so transport plumbing is stripped but extension surfaces round-trip.

Also widened ToolsResult / PromptsResult / ResourcesResult / ResourceTemplatesResult element types with index signatures so annotations, outputSchema, _meta, and MCP Apps fields are first-class at the type level (they were already captured at runtime — the types just understated it).

New kitchen-sink test in src/__tests__/probe.test.ts round-trips a tool with annotations + outputSchema + _meta.ui, a UI resource with the full MCP Apps _meta.ui shape (CSP connectDomains / resourceDomains / frameDomains / baseUriDomains, permissions), prompt arguments, and a resource template — every advertised field has to survive normalization. 74 tests pass.

SamMorrowDrums and others added 5 commits June 10, 2026 22:40
- zod ^3 → ^4 (z.record signature now requires key+value schemas)
- undici ^6 → ^8 (and overrides block)
- diff ^8 → ^9
- @actions/{core,exec,io} ^1 → ^3
- eslint ^9 → ^10, @eslint/js ^9 → ^10
- typescript ^5 → ^6 (tsconfig now needs explicit "types": ["node"])
- jest ^29 → ^30, @types/jest ^29 → ^30
- @types/node ^22 → ^24 (CI still tests Node 20+22)
- @vercel/ncc ^0.38 → ^0.44
- prettier, eslint-config-prettier, typescript-eslint to latest
- ts-jest kept at ^29.4.x (peer-compatible with jest 30 + ts 6, no v30 yet)

Bump package version to 3.0.0-rc.0 and rebuild dist/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prepare for the MCP draft spec (publishes end of month) without taking
a dependency on SDK v2:

- normalizeProbeResult accepts { stripCacheHints } and removes top-level
  `ttlMs` / `cacheScope` (CacheableResult, SEP-2461) from tools/list,
  prompts/list, resources/list, and resources/templates/list. These
  freshness hints vary run-to-run and would otherwise create diff noise.
- New probe.test.ts covers the strip behaviour and confirms nested
  ttlMs/cacheScope are preserved (only the result envelope is touched).
- TODO comment near initialize snapshot referencing SEP-2575 (the draft
  renames `initialize` → `server/discover`).
- Comment on InitializeInfo.capabilities confirming it stays open-ended
  for the draft's `capabilities.extensions` (SEP-2589).
- Doc comment notes the draft now mandates the deterministic ordering
  we already do.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summarise the major dep bumps, the new CacheableResult stripping
(SEP-2461), and note that adopting the renamed server/discover method
(SEP-2575) is deferred until SDK v2 ships.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The base ref and the current branch may negotiate different MCP protocol
versions during the draft-spec rollout. We want a server upgrading its
SDK without changing its public surface to produce an empty diff.

probe.ts
- Capture the negotiated MCP protocol version via a transport-level
  setProtocolVersion hook (works for stdio + HTTP). The SDK doesn't
  expose this through a public getter, so we wrap/attach it ourselves.
  Store it on result.initialize.protocolVersion.
- normalizeProbeResult now recursively scrubs `_meta` of protocol-only
  plumbing: keys prefixed with `io.modelcontextprotocol/`
  (protocolVersion, clientInfo, clientCapabilities, subscriptionId,
  logLevel) and W3C trace context (traceparent, tracestate, baggage).
  An emptied `_meta` is dropped entirely so it never appears in diffs.
- New normalizeInitializeForDiff drops `protocolVersion` and
  `capabilities.experimental` from the initialize snapshot body. Drift
  on those would otherwise dominate every cross-spec diff; the reporter
  surfaces protocol-version changes separately.
- Add CANONICAL_SNAPSHOT_NAMES so future endpoint renames (e.g. SEP-2575
  initialize → server/discover) map to one stable filename instead of
  showing up as removed+added.

types.ts
- InitializeInfo gains optional protocolVersion.
- TestResult gains branchProtocolVersion + baseProtocolVersion.

runner.ts
- Propagate per-probe protocolVersion into TestResult.

reporter.ts
- New formatProtocolVersionBanner helper.
- Per-config markdown report inserts the banner inline above the diff.
- PR summary surfaces version drift at the very top so reviewers
  immediately know the diff was taken across spec revisions.

tests
- probe.test.ts: cross-version cleanliness suite asserts that
  tools/list and initialize snapshots are byte-identical when only
  protocol envelope differs, and that real public-surface changes are
  still flagged. Plus unit coverage for the new _meta scrubbing.
- reporter.test.ts: banner unit tests + integration through
  generateMarkdownReport / generatePRSummary.

README
- New "Cross-spec-version diffing" subsection in Migration to 3.0
  documenting exactly what's normalized away (and what isn't).

Rebuild dist/. All 72 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous _meta scrubber stripped every key under the
io.modelcontextprotocol/* prefix. That namespace is reserved by the spec
but is also where official extensions live, so the prefix-based filter
would silently delete:

- MCP Apps (SEP-1865) UI metadata at _meta.ui — CSP, permissions, etc.
- Tasks' io.modelcontextprotocol/related-task linkage
- Any future official extension under the reserved namespace

Switch to an exact-key denylist limited to true transport plumbing
(protocolVersion, clientInfo, clientCapabilities, subscriptionId,
logLevel) plus W3C trace context. Everything else round-trips, which is
the whole point of this tool.

Also widen ToolsResult / PromptsResult / ResourcesResult /
ResourceTemplatesResult element types with index signatures so the type
layer no longer undersells the runtime shape (annotations, outputSchema,
_meta, MCP Apps fields are all first-class now).

Adds a kitchen-sink regression test that round-trips a tool with
annotations + outputSchema + _meta.ui, a UI resource with the full MCP
Apps _meta.ui surface (csp, permissions), prompt arguments, and a
resource template — every field must survive normalization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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