feat!: 3.0 — major dependency upgrades + MCP draft-spec forward-compat - #56
Merged
Merged
Conversation
SamMorrowDrums
changed the base branch from
sammorrowdrums/mcp-spec-update-check
to
main
June 29, 2026 08:21
SamMorrowDrums
marked this pull request as ready for review
June 29, 2026 08:21
SamMorrowDrums
force-pushed
the
sammorrowdrums/3-0-major-upgrades
branch
from
June 29, 2026 08:24
258c4c9 to
e774c8f
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR prepares mcp-server-diff for a 3.0 release by upgrading major dependencies and updating the probe/reporting pipeline to remain forward-compatible with upcoming MCP draft-spec changes, with an emphasis on producing clean diffs across spec revisions.
Changes:
- Major dependency upgrades (TypeScript 6, Jest 30, Zod 4, Undici 8, Actions toolkit v3, ESLint 10) and regenerated
dist/. - Added stateless
server/discoverprobing with normalization to strip protocol-shaped noise (_metaplumbing, CacheableResult hints, tool-annotation defaults) and canonicalize snapshot naming. - Added/extended tests and documentation covering cross-spec diff cleanliness and protocol-version drift banners.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Adds types: ["node"] for TS6 ambient node typings behavior. |
| src/types.ts | Widens probe result types for forward-compatible fields; adds protocol version fields to results. |
| src/runner.ts | Plumbs negotiated protocol versions into TestResult for reporting. |
| src/reporter.ts | Adds protocol-version drift banners in markdown report and PR summary. |
| src/probe.ts | Implements server/discover stateless probing, protocol-version capture hook, and normalization/canonical snapshot behavior. |
| src/tests/reporter.test.ts | Adds unit/integration coverage for protocol-version banner formatting and placement. |
| src/tests/probe.test.ts | Adds extensive normalization + cross-spec cleanliness + regression test coverage. |
| src/tests/fixtures/github-mcp-server-wire.json | Adds real-wire fixture data for initialize vs discover cross-version normalization tests. |
| README.md | Documents 3.0 migration notes and cross-spec-version diffing behavior. |
| package.json | Bumps package version to 3.0.0-rc.0 and upgrades major dependency versions. |
| dist/types.d.ts | Regenerated types reflecting updated source interfaces. |
| dist/reporter.d.ts | Regenerated declarations including protocol banner API. |
| dist/probe.d.ts | Regenerated declarations including discover probing + canonical snapshot naming exports. |
| dist/licenses.txt | Updated bundled license attributions from rebuild. |
| dist/cli/types.d.ts | Regenerated CLI types reflecting updated source interfaces. |
| dist/cli/reporter.d.ts | Regenerated CLI declarations including protocol banner API. |
| dist/cli/probe.d.ts | Regenerated CLI declarations including discover probing + canonical snapshot naming exports. |
| dist/cli/licenses.txt | Updated bundled CLI license attributions from rebuild. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 9/19 changed files
- Comments generated: 1
- Review effort level: Low
SamMorrowDrums
added a commit
that referenced
this pull request
Jun 29, 2026
undici 8 (already on the 3.0 branch) requires Node 22+: the v8 runtime uses webidl.util.markAsUncloneable which is only present on Node 21+. This surfaced as a CI failure on PR #56 (test (20) red, test (22) ok). Node 20 reached LTS end-of-life on 2026-04-30, so dropping it from a major release is the right call. Matrix replaces 20 with 24 so CI exercises current LTS (22) plus current Current (24). - .github/workflows/ci.yml: matrix [20, 22] → [22, 24]; the dist-up- to-date guard now runs on the 22 leg (was 20) - action.yml: setup_node default 20 → 22 - package.json: add engines.node ">=22" - README.md: replace "tested on Node 20 + 22" with a dedicated "Dropped support: Node.js 20" note in the Migration section - CONTRIBUTING.md: Node 20+ → Node 22+ 86/86 tests still pass locally on Node v22.23.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines
+565
to
+569
| // The SDK doesn't expose the negotiated protocol version via a public | ||
| // getter. It does, however, call `transport.setProtocolVersion(...)` after | ||
| // initialize if the transport implements it. Wrap (or attach) that hook so | ||
| // we can capture the version for the snapshot. Works for stdio + HTTP. | ||
| let negotiatedProtocolVersion: string | undefined; |
| { | ||
| "name": "mcp-server-diff", | ||
| "version": "2.2.0", | ||
| "version": "3.0.0-rc.0", |
- 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>
The MCP spec defines defaults for ToolAnnotations hints (readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true). A server that omits a hint is semantically identical to one that emits the default value, but an SDK upgrade can flip the wire encoding between the two — for example go-sdk v1.7.0-pre.1 dropped `omitempty` on ReadOnlyHint and IdempotentHint, so every tool gains `readOnlyHint: false` / `idempotentHint: false` on the wire. Without normalization, a pure SDK bump produces a noisy ~113-tool diff on github-mcp-server. normalizeProbeResult now drops annotation fields that equal their spec default, and drops the annotations object entirely if nothing is left. This is the tools-list analogue of the existing _meta and CacheableResult scrubbing — same principle: protocol-shape churn out, public surface in. Verified by a four-case suite including an explicit omit-vs-default-emit regression test mirroring the go-sdk v1.6 ↔ v1.7 transition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adopt the SEP-2575 / SEP-2243 stateless probe path so each server is
probed at its OWN newest spec version, not silently negotiated down onto
the legacy initialize handshake. This is what makes the cross-version
diff honest:
- New-spec server (go-sdk >= v1.7.0-pre.1): probed via server/discover
at 2026-07-28. No initialize, no notifications/initialized, no
Mcp-Session-Id. Reserved _meta on every request (protocolVersion,
clientInfo, clientCapabilities). HTTP responses parsed as SSE
(text/event-stream, single frame); stdio uses line-delimited JSON-RPC.
- Legacy server (go-sdk v1.6.1 / pre-2026 SDKs): falls back to the
existing client.connect() initialize handshake.
Fallback detection follows the empirical wire shape: a v1.6.1 server
returns HTTP 400 text/plain ('Unsupported protocol version' or
'JSON RPC not handled server/discover unsupported'), NOT a JSON-RPC
-32601. The orchestrator treats any HTTP non-200, any non-JSON body,
any JSON-RPC error, or a missing supportedVersions array as 'fall
back'; only HTTP 200 + a DiscoverResult with supportedVersions[]
counts as success.
normalizeInitializeForDiff now also strips top-level ttlMs / cacheScope
because the discover handshake carries CacheableResult hints too. The
canonical 'initialize' snapshot filename means base-via-initialize and
branch-via-discover collapse to one content diff. Public-interface
signals like 'instructions' and the capabilities shape are deliberately
NOT normalized — the go-sdk v1.7.0-pre.1 discover-omits-instructions
regression is exactly the kind of change this tool exists to surface.
Tests use fixtures lifted from real wire transcripts against
github-mcp-server v1.6.1 (initialize @ 2025-11-25) and v1.7.0-pre.1
(discover @ 2026-07-28). The integration test's stdio fixture only
speaks the legacy spec, so it exercises the discover-fails-fallback
path live.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…1.7.0-pre.1 Adds src/__tests__/fixtures/github-mcp-server-wire.json with the raw JSON-RPC bodies captured live against: - v1.7.0-pre.1 stateless server/discover @ 2026-07-28 (tools/list with the SEP-2243 reserved _meta path, get_me as the representative tool, the -32020 header-mismatch error envelope, the -32602 missing-_meta error envelope) - v1.6.1 legacy initialize @ 2025-11-25 (paired baseline: instructions present, listChanged + logging capabilities, get_me annotations without the omitempty-dropped idempotentHint default) The new test suite drives the case-study assertion off these bodies verbatim: - get_me tools-list snapshot: annotation defaults stripped, cache hints stripped, real `icons` field preserved as public surface - initialize slot: protocolVersion + cache hints stripped, but the capability-shape delta (logging dropped, listChanged dropped, resources added) stays as a visible signal - discover-omits-instructions vs initialize-emits-instructions regression preserved as a diff signal base64 image payloads on icons are pinned as `<TRUNCATED>` to keep the fixture small; refresh instructions are documented in the JSON header so future contributors can regenerate from new wire captures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…egression `instructions` is OPTIONAL in both InitializeResult and DiscoverResult per the draft spec (required keys on DiscoverResult are capabilities, resultType, serverInfo, supportedVersions). So a server emitting instructions on initialize but not discover is spec-conformant — it's a behavioral inconsistency / public-interface difference, not a conformance failure. The diff still surfaces it (clients adopting discover observe the gap), just labelled neutrally. No code change — only test names, comments, and README narrative. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eslint 10.4→10.6, prettier 3.8→3.9, undici 8.4→8.5, typescript-eslint 8.61→8.62, @types/node 24.13.1→24.13.2. SDK already on 1.29.0 (latest). Production audit: 0 vulnerabilities. Dev-only vulns are jest transitives that can't be fixed without downgrading ts-jest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
undici 8 (already on the 3.0 branch) requires Node 22+: the v8 runtime uses webidl.util.markAsUncloneable which is only present on Node 21+. This surfaced as a CI failure on PR #56 (test (20) red, test (22) ok). Node 20 reached LTS end-of-life on 2026-04-30, so dropping it from a major release is the right call. Matrix replaces 20 with 24 so CI exercises current LTS (22) plus current Current (24). - .github/workflows/ci.yml: matrix [20, 22] → [22, 24]; the dist-up- to-date guard now runs on the 22 leg (was 20) - action.yml: setup_node default 20 → 22 - package.json: add engines.node ">=22" - README.md: replace "tested on Node 20 + 22" with a dedicated "Dropped support: Node.js 20" note in the Migration section - CONTRIBUTING.md: Node 20+ → Node 22+ 86/86 tests still pass locally on Node v22.23.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SamMorrowDrums
force-pushed
the
sammorrowdrums/3-0-major-upgrades
branch
from
June 29, 2026 08:33
1421255 to
ce959c6
Compare
Contributor
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (1)
src/probe.ts:518
- probeViaInitialize still identifies as version "2.0.0" while the stateless path sends PROBE_CLIENT_INFO.version "3.0". This makes the client identity inconsistent across probe paths and could affect server behavior and/or show up in protocol metadata (even if normalized away). Use the shared PROBE_CLIENT_INFO version to keep both paths aligned.
const client = new Client(
{
name: "mcp-server-diff-probe",
version: "2.0.0",
},
- Files reviewed: 12/22 changed files
- Comments generated: 2
- Review effort level: Low
| strategy: | ||
| matrix: | ||
| node-version: [20, 22] | ||
| node-version: [22, 24] |
- src/probe.ts: probed-child stderr now logs at debug instead of info. Servers can write tokens/headers to stderr during startup; surfacing that at info on every CI run is noisy and risks leaking secrets into workflow logs. The existing comment already said "without spamming"; this matches the intent. - src/version.ts: new module reading the version from package.json (build-time import-attribute; ncc inlines the JSON). Single source of truth for the package version. - src/cli.ts: --version reads from PACKAGE_VERSION instead of the stale hardcoded "v2.1.1". - src/probe.ts: PROBE_CLIENT_INFO.version reads from PACKAGE_VERSION; the legacy initialize Client() constructor now consumes the same constant instead of the stale hardcoded "mcp-server-diff-probe/2.0.0". Wire captures show the same clientInfo regardless of whether the probe took the stateless server/discover path or the initialize fallback. - src/__tests__/version.test.ts: smoke test asserting PACKAGE_VERSION matches package.json verbatim and is non-empty semver-shaped, so the version wiring can't drift silently. 88/88 tests now pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SamMorrowDrums
added a commit
that referenced
this pull request
Jun 29, 2026
undici 8 (already on the 3.0 branch) requires Node 22+: the v8 runtime uses webidl.util.markAsUncloneable which is only present on Node 21+. This surfaced as a CI failure on PR #56 (test (20) red, test (22) ok). Node 20 reached LTS end-of-life on 2026-04-30, so dropping it from a major release is the right call. Matrix replaces 20 with 24 so CI exercises current LTS (22) plus current Current (24). - .github/workflows/ci.yml: matrix [20, 22] → [22, 24]; the dist-up- to-date guard now runs on the 22 leg (was 20) - action.yml: setup_node default 20 → 22 - package.json: add engines.node ">=22" - README.md: replace "tested on Node 20 + 22" with a dedicated "Dropped support: Node.js 20" note in the Migration section - CONTRIBUTING.md: Node 20+ → Node 22+ 86/86 tests still pass locally on Node v22.23.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-18→2025-11-25→draft— 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.recordnow requires(keyType, valueType); the one call site insrc/probe.ts(custom-message response schema) was updated toz.record(z.string(), z.unknown()).undici^6 → ^8 — also updates theoverridesblock (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 insrc/diff.tsis custom).@actions/core^1 → ^3,@actions/exec^1 → ^3,@actions/io^1 → ^3eslint^9 → ^10,@eslint/js^9 → ^10typescript^5 → ^6 — required adding"types": ["node"]totsconfig.json(TS 6 no longer auto-includes ambient@types/node).jest^29 → ^30,@types/jest^29 → ^30@types/node^22 → ^24 (CI now tests Node 22 + 24; Node 20 is dropped — see breaking changes below)@vercel/ncc^0.38 → ^0.44prettier,eslint-config-prettier,typescript-eslint: latestKept at current major:
ts-jeststays on^29.4.x— there is no v30 published yet, but29.4.11peer-depends onjest ^29 || ^30andtypescript >=4.3 <7, so it works with the new jest + ts.@modelcontextprotocol/sdkstays on^1.13.2— v2 is not published.package.jsonis bumped to3.0.0-rc.0.dist/was regenerated vianpm run build(never hand-edited).MCP draft-spec forward-compat
Targets the changes in the draft changelog without depending on SDK v2:
normalizeProbeResultaccepts{ stripCacheHints }and removes top-levelttlMs/cacheScopefromtools/list,prompts/list,resources/list, andresources/templates/listresults before snapshotting.initialize→server/discover(SEP-2575). Not renaming the snapshot file yet — SDK v2 isn't out. NewCANONICAL_SNAPSHOT_NAMEStable inprobe.tsis wired in now so when the rename happens we map both spellings to the sameinitializesnapshot 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.capabilitiesisRecord<string, unknown>with a comment confirming it stays open-ended.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-25and 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):
_metaprotocol plumbing — an exact-key denylist is stripped from every_metaobject at any depth:io.modelcontextprotocol/protocolVersion,clientInfo,clientCapabilities,subscriptionId,logLevel. Not by prefix — official extensions live under the same reserved namespace (MCP Apps'_meta.uiper SEP-1865, Tasks'io.modelcontextprotocol/related-task) and must round-trip. An emptied_metais dropped entirely._meta—traceparent,tracestate,baggage(transport-injected for OTel propagation) are stripped from_meta.initializeenvelope churn —protocolVersionandcapabilities.experimentalare excluded from theinitializediff 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.ttlMs/cacheScopethat live inside a tool/prompt/resource definition (those would be part of the public surface, not envelope hints)._metakey 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(...)beforeclient.connect(...)— the SDK calls it after a successfulinitialize. The captured value lands onresult.initialize.protocolVersionand is then propagated intoTestResult.branchProtocolVersion/baseProtocolVersioninrunner.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:
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:normalizeProbeResultstrip behaviour forttlMs/cacheScope(top-level only)._metascrubbing ofio.modelcontextprotocol/*and W3C trace-context keys, including the "drop empty_meta" path.probeResultToFilesstrips cache hints for all four list/templates endpoints and leavesinitializeintact except forprotocolVersion+capabilities.experimental.2025-11-25) and once with the draft envelope (CacheableResult + protocol_meta) — and asserts the normalizedtools/listandinitializesnapshots are byte-identical. A negative test confirms a realtoolsdelta still produces a diff.src/__tests__/reporter.test.ts(extended): unit tests forformatProtocolVersionBanner(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 checkpasses — typecheck + lint + prettier + 96 jest tests across 7 suites (including the folded-in #60 metadata-diff coverage and the #58 runner / reporter / config-missing tests).Breaking changes — runtime
undiciv8 (the version this PR bumps to) requires Node 22+ — it callswebidl.util.markAsUncloneable, only present on Node 21+. The CI matrix is now[22, 24](current LTS + current Current).package.jsondeclares"engines": { "node": ">=22" }. The action'ssetup_nodeinput defaults to22(was20); workflows passing an explicitnode_versionof 20 should bump to 22 or newer.Folded-in PRs (closing on merge)
Closes test: cover tool metadata diffs #60 —
test: cover tool metadata diffs(@kigland). First direct unit tests for the diff engine (compareProbeResults): asserts tool-leveldescriptionchanges AND nestedinputSchema.properties.<arg>.descriptionchanges both surface under thetoolsendpoint diff. Zero production-code change, applied clean.Fixes One-sided startup failure should show a fail on that side and a full diff against empty on the other #57 (closes feat: diff one-sided startup failures against an empty baseline #58) —
feat: diff one-sided startup failures against an empty baseline. When exactly one side fails to start (e.g. a PR introduces a new server configuration behind a CLI flag that doesn't exist on the compare ref), the failed side is now treated as an emptyProbeResultand the working side's full surface renders as added/removed instead of collapsing to an opaqueConnection closederror. Newconfig-missingdiff category, non-fatal underfail_on_error(only genuine both-sides probe failures still hard-error). NewcompareConfigResults()exported fromrunner.tsfor unit testability. Reporter classifies into passing / changed / config-missing / error and renders a dedicated 🚫 callout in both the markdown report and PR summary. AddsconfigMissing?: { side, error }toTestResult. The 2.4.0 version bump from the original PR was dropped (3.0 already moved to 3.0.0-rc.0).Conflict resolution notes (for the #58 fold)
The only non-trivial conflict was in
src/reporter.ts: both PR #56 (this branch) and PR #58 added new pre-diff renderer blocks togenerateMarkdownReport. Resolved by keeping BOTH — the protocol-version banner renders first (cross-spec drift heads-up), then the config-missing callout (if applicable), then the existing diff section. Same change applied togeneratePRSummary. No semantic conflict; both features render independently into the same lines array.Follow-up (not in this PR)
@modelcontextprotocol/sdkv2 once released, switch the SDK call frominitializetoserver/discover, and re-evaluate whetherclient.getProtocolVersion()lands publicly so we can drop the transport-hook trick.Notes
sammorrowdrums/mcp-spec-update-check. Once chore: in-range dep updates + dist rebuild (supersedes #51) #55 merges intomain, retarget this PR tomain(or merge chore: in-range dep updates + dist rebuild (supersedes #51) #55 first, then rebase).Follow-up: MCP Apps + extension preservation (commit
8181d45)Caught a regression in my own
_metascrubber before merge: the original implementation matchedio.modelcontextprotocol/*by prefix, which would silently delete the entire MCP Apps surface (_meta.uiper 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/ResourceTemplatesResultelement types with index signatures soannotations,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.tsround-trips a tool withannotations+outputSchema+_meta.ui, a UI resource with the full MCP Apps_meta.uishape (CSPconnectDomains/resourceDomains/frameDomains/baseUriDomains, permissions), prompt arguments, and a resource template — every advertised field has to survive normalization. 74 tests pass.