Skip to content

fix(mcp): serverInfo.version defaults to the package version, not a literal 1.0.0 - #21548

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21532-mcp-version-default
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21532-mcp-version-default

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21532

Clause-②: no

The MCP server's serverInfo.version is now the package version unless the caller sets one, as MCPServerPluginOptions.version is 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 explicit version still overrides it.

What changed

  • packages/mcp/src/package-version.ts (new): reads the package's own package.json once at load and exports two values. PACKAGE_VERSION is the manifest version, or undefined when the manifest cannot be read. DEFAULT_SERVER_VERSION is PACKAGE_VERSION or 'unknown'.
  • plugin.ts: the class field is version = PACKAGE_VERSION, and init() passes options.version ?? DEFAULT_SERVER_VERSION. The literal '1.0.0' is gone from both.
  • mcp-server-runtime.ts: the constructor default is DEFAULT_SERVER_VERSION, and the version TSDoc 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 the build script (tsup, was tsup --config ../../tsup.config.ts): the package builds with its own config, which is the shared one plus shims: true and a comment saying why. The shared tsup.config.ts is untouched.
  • mcp-server-info-version.test.ts (new) pins the behaviour. One assertion in __tests__/plugin.test.ts that pinned the deleted literal (plugin.version is '1.0.0') is replaced by a pointer to the new file.
  • .changeset/21532-mcp-server-info-version-default.md: @objectstack/mcp patch.

Precedent for the read (A2)

packages/runtime/src/runtime-version.ts and packages/metadata-protocol/src/discovery-version.ts already read their own manifest as createRequire(import.meta.url) against ../package.json, which resolves the same file from src/ and from the bundled dist/index.{js,cjs}. Both carry shims: true in a package-local tsup config for the CJS half, and scripts/check-dual-build-cjs-loads.mjs prescribes exactly that ("add shims: true to this package's tsup.config.ts"). No tsup config in the repo uses define, so there is no build-time-define precedent. This PR copies the shape and adds no build step.

Two measurements that shaped the diff

  1. The shared tsup config is not enough for the CJS build. Built with it, dist/index.cjs carries var import_meta = {}, createRequire(undefined) throws, and the reader's catch swallows it. Probe of the built dist (plugin built with no options, initialize over handleHttpRequest), manifest 17.6.0:

    shared config:      esm 17.6.0   cjs unknown
    package-local cfg:  esm 17.6.0   cjs 17.6.0   (serverInfo.version and plugin.version, both formats)
    

    The package-local config makes dist/index.cjs use importMetaUrl and zero verbatim import.meta; node --check parses it and require('./dist/index.cjs') loads.

  2. 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(...) answers PLUGIN_CONTRACT_VIOLATION at version for 'unknown'; ObjectKernel answers Invalid semantic version: unknown; undefined and 0.0.0 are accepted, and ObjectKernel supplies 0.0.0 itself for an absent version. So when the manifest is unreadable (a bundle with no package.json beside it) serverInfo.version, a free string on the wire, says unknown, and the plugin's own version is 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 (the Plugin contract), validated as SemVer 2.0.0 by both kernels. Nothing compares it to a literal: outside packages/mcp, the only consumers of the plugin's identity are os serve's capability table, which matches com.objectstack.mcp and MCPServerPlugin by name and never reads a version, and the one assertion that pinned the literal is the plugin.test.ts line 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.mjs on the committed fix (mutation proved on disk by anchor count and blob hash), then vitest run src/mcp-server-info-version.test.ts. The pins resolve their subjects from src/ through relative imports, so no rebuild stands between mutation and measurement. Direction observed: red, as expected.

restored literal red assertion
plugin init() default 3 of 7 expected '1.0.0' to be '17.6.0' (HTTP and long-lived), and expected '1.0.0' to be 'unknown' on the unreadable-manifest pin
runtime constructor default 2 of 7 expected '1.0.0' to be '17.6.0' (HTTP and long-lived)
plugin class field 1 of 7 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: OK with the test-layer ledger unchanged.
  • pnpm --filter @objectstack/mcp build: exit 0 (check-dts-emitted 2/2); dist proof above. The dependency closure was built first (@objectstack/mcp^... build, exit 0).
  • Lint: the full pnpm lint (eslint . --no-inline-config) exited 0 at 57aa3eee3e; 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/objectstack named 73 commands, run in full at 57aa3eee3e: 71 exit 0. --ran reconciles as 73 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-loads needs every workspace package built (77 dist/ absent). Its subject for this diff, mcp's dist/index.cjs, was probed above (parses, loads, answers the manifest version).
  • pnpm check:lean-entry-closure needs packages/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 is scripts/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.md Basic Usage (:41) and the runtime example (:240) set version: '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.
  • Pre-existing, measured: new MCPServerRuntime({ version: undefined }) answers initialize with serverInfo {"name":"objectstack"}, no version key, because the constructor spreads ...config over its defaults and an explicit undefined wins. MCPServerPlugin never passes undefined, and no in-repo caller does either, so no producer is named. Carrier: none.

Generated by Claude Code

claude added 2 commits October 3, 2026 05:00
…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>
@github-actions github-actions Bot added the size/m label Oct 3, 2026
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/mcp, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/mcp/tsup.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via MCPServerPlugin (symbol, a top-level class))
  • content/docs/ai/natural-language-queries.mdx (via MCPServerPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via MCPServerPlugin (symbol, a top-level class))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/mcp/tsup.config.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 24dc7c113460d1c8dfb4d0fb7b45542b86dd1ea6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 249f439836c006481f4ba00cbaea06539997f6af — the merge of head 1356d7acd554d7db3ec2483177d3fdcb4145ec17 into base 24dc7c113460d1c8dfb4d0fb7b45542b86dd1ea6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 24dc7c113460d1c8dfb4d0fb7b45542b86dd1ea6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants