fix(mcp): serverInfo.version defaults to the package version, not a literal 1.0.0 - #21548
Conversation
…iteral 1.0.0 Both literal defaults (the plugin's class field and init(), and MCPServerRuntime's constructor) now read one value taken from the package's own manifest; an explicit version option still overrides it. The CJS build needs shims for the read, so the package builds with its own tsup config. Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 249f439836c006481f4ba00cbaea06539997f6af && git checkout 249f439836c006481f4ba00cbaea06539997f6af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 24dc7c113460d1c8dfb4d0fb7b45542b86dd1ea6 1356d7acd554d7db3ec2483177d3fdcb4145ec17 && git checkout -B drift-repro 24dc7c113460d1c8dfb4d0fb7b45542b86dd1ea6 && git merge --no-ff 1356d7acd554d7db3ec2483177d3fdcb4145ec17
node scripts/docs-audit/affected-docs.mjs --json 24dc7c113460d1c8dfb4d0fb7b45542b86dd1ea6
|
…undefined Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
Fixes #21532
Clause-②: no
The MCP server's
serverInfo.versionis now the package version unless the caller sets one, asMCPServerPluginOptions.versionis documented ("Defaults to package version"). Triage's ruling on the card: declared means enforced, both literal defaults go, both read one value taken from the package manifest, an explicitversionstill overrides it.What changed
packages/mcp/src/package-version.ts(new): reads the package's ownpackage.jsononce at load and exports two values.PACKAGE_VERSIONis the manifest version, orundefinedwhen the manifest cannot be read.DEFAULT_SERVER_VERSIONisPACKAGE_VERSIONor'unknown'.plugin.ts: the class field isversion = PACKAGE_VERSION, andinit()passesoptions.version ?? DEFAULT_SERVER_VERSION. The literal'1.0.0'is gone from both.mcp-server-runtime.ts: the constructor default isDEFAULT_SERVER_VERSION, and theversionTSDoc now says what it defaults to. That one default feeds both the long-lived (stdio) server and the per-request HTTP server.packages/mcp/tsup.config.ts(new) and thebuildscript (tsup, wastsup --config ../../tsup.config.ts): the package builds with its own config, which is the shared one plusshims: trueand a comment saying why. The sharedtsup.config.tsis untouched.mcp-server-info-version.test.ts(new) pins the behaviour. One assertion in__tests__/plugin.test.tsthat pinned the deleted literal (plugin.versionis'1.0.0') is replaced by a pointer to the new file..changeset/21532-mcp-server-info-version-default.md:@objectstack/mcppatch.Precedent for the read (A2)
packages/runtime/src/runtime-version.tsandpackages/metadata-protocol/src/discovery-version.tsalready read their own manifest ascreateRequire(import.meta.url)against../package.json, which resolves the same file fromsrc/and from the bundleddist/index.{js,cjs}. Both carryshims: truein a package-local tsup config for the CJS half, andscripts/check-dual-build-cjs-loads.mjsprescribes exactly that ("addshims: trueto this package's tsup.config.ts"). No tsup config in the repo usesdefine, so there is no build-time-define precedent. This PR copies the shape and adds no build step.Two measurements that shaped the diff
The shared tsup config is not enough for the CJS build. Built with it,
dist/index.cjscarriesvar import_meta = {},createRequire(undefined)throws, and the reader'scatchswallows it. Probe of the built dist (plugin built with no options,initializeoverhandleHttpRequest), manifest17.6.0:The package-local config makes
dist/index.cjsuseimportMetaUrland zero verbatimimport.meta;node --checkparses it andrequire('./dist/index.cjs')loads.The kernel refuses a non-SemVer plugin
version, so one placeholder for both consumers would be a defect (this is the A3 check).new LiteKernel().use(...)answersPLUGIN_CONTRACT_VIOLATIONatversionfor'unknown';ObjectKernelanswersInvalid semantic version: unknown;undefinedand0.0.0are accepted, andObjectKernelsupplies0.0.0itself for an absent version. So when the manifest is unreadable (a bundle with nopackage.jsonbeside it)serverInfo.version, a free string on the wire, saysunknown, and the plugin's ownversionis left unset instead of becoming a plugin the kernel never loads. A pin covers that path.A3, the class field: it is the kernel plugin's own
version(thePlugincontract), validated as SemVer 2.0.0 by both kernels. Nothing compares it to a literal: outsidepackages/mcp, the only consumers of the plugin's identity areos serve's capability table, which matchescom.objectstack.mcpandMCPServerPluginby name and never reads a version, and the one assertion that pinned the literal is theplugin.test.tsline above. It reads the same one value, as triage names it.Verification
Reproduction (A4) and reverse verification in one: each literal restored through
scripts/ablation-replace.mjson the committed fix (mutation proved on disk by anchor count and blob hash), thenvitest run src/mcp-server-info-version.test.ts. The pins resolve their subjects fromsrc/through relative imports, so no rebuild stands between mutation and measurement. Direction observed: red, as expected.init()defaultexpected '1.0.0' to be '17.6.0'(HTTP and long-lived), andexpected '1.0.0' to be 'unknown'on the unreadable-manifest pinexpected '1.0.0' to be '17.6.0'(HTTP and long-lived)expected '1.0.0' to be '17.6.0'Each of the three restores was proven by a blob hash equal to HEAD's and an empty
git diff HEAD, not by an exit code.pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2: 35 files, 389 tests, all passed.pnpm --filter @objectstack/mcp typecheck: exit 0,check:test-typecheck: OKwith the test-layer ledger unchanged.pnpm --filter @objectstack/mcp build: exit 0 (check-dts-emitted2/2); dist proof above. The dependency closure was built first (@objectstack/mcp^...build, exit 0).pnpm lint(eslint . --no-inline-config) exited 0 at57aa3eee3e; the six touched JS/TS files also lint at 0 errors and 0 warnings on their own, none ignored, with no type-aware parser options in the resolved config.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstacknamed 73 commands, run in full at57aa3eee3e: 71 exit 0.--ranreconciles as73 derived, 71 run, 2 NOT-MEASURED, 0 UNRUN.NOT MEASURED, both exit 3
PREREQUISITE NOT MET, left to CI's Build Core:pnpm check:dual-build-cjs-loadsneeds every workspace package built (77dist/absent). Its subject for this diff, mcp'sdist/index.cjs, was probed above (parses, loads, answers the manifest version).pnpm check:lean-entry-closureneedspackages/objectql/dist. mcp is not among the 15 packages (objectql included) in objectql's dependency closure.The derivation ran on a tree 2 commits behind
origin/main; the one derivation input that changed upstream isscripts/codemod/view-to-viewitem.mjs, which this diff does not touch.Acceptance notes
Noted, not filed, not fixed here (outside the ruling's file surface or without a named producer):
packages/mcp/README.mdBasic Usage (:41) and the runtime example (:240) setversion: '1.0.0'explicitly, so copying them pins the old string. The README's option table already says "Defaults to package version", which is true now. Carrier: none.new MCPServerRuntime({ version: undefined })answersinitializewithserverInfo {"name":"objectstack"}, noversionkey, because the constructor spreads...configover its defaults and an explicitundefinedwins.MCPServerPluginnever passesundefined, and no in-repo caller does either, so no producer is named. Carrier: none.Generated by Claude Code