Skip to content

Fix: TS server version drift - #4608

Draft
Medsaad wants to merge 6 commits into
modelcontextprotocol:mainfrom
Medsaad:fix/issue-4575-ts-server-version-drift
Draft

Fix: TS server version drift#4608
Medsaad wants to merge 6 commits into
modelcontextprotocol:mainfrom
Medsaad:fix/issue-4575-ts-server-version-drift

Conversation

@Medsaad

@Medsaad Medsaad commented Aug 4, 2026

Copy link
Copy Markdown

Draft .. parked until #4576 lands. That PR establishes the pattern for sequentialthinking; this one applies it to the remaining three TypeScript servers. Opening it early so the work is visible and nobody duplicates it. I'll mark it ready once #4576 merges, and rebase if its implementation changes during review.

Description

Follow-up to #4576, as suggested by @KarlLeen there. filesystem, memory, and everything hardcode a version literal in their McpServer constructor .. the same defect #4575 reports for sequentialthinking. Those literals cannot be kept correct, because scripts/release.py rewrites package.json["version"] at release time, so every server eventually misreports its version over the wire.

This replaces each literal with SERVER_VERSION, read from the package's own package.json at runtime.

Server Literal in source package.json
filesystem 0.2.0 0.6.3 drifted today
memory 0.6.3 0.6.3 matches now, drifts at next release
everything 2.0.0 2.0.0 matches now, drifts at next release

memory and everything match at rest but drift the moment a release stamps a CalVer version into their manifests, so all three are fixed together. sequentialthinking is deliberately untouched here .. it belongs to #4576.

Server Details

  • Server: filesystem, memory, everything
  • Changes to: reported serverInfo.version (no tools, resources, or prompts changed)

Motivation and Context

Related to #4575.

scripts/release.py:70-76 (NpmPackage.update_version) rewrites package.json["version"] to a date-based version generated by gen_version() at release time. A version committed in TypeScript source is therefore stale by construction .. it is never the version that ships. That makes this one systemic defect rather than a series of independent oversights, and it is why the fix has to read the manifest at runtime rather than sync a literal.

This stays correct if #4463 (semver via changesets) changes how versions are stamped, since it reads whatever ends up in the shipped manifest.

Implementation

version.ts is duplicated at each package root rather than extracted into a shared package. A shared src/shared/ workspace package would be auto-detected as publishable by both find_changed_packages (release.py) and the CI detect-packages matrix, would need its own npm trusted-publisher registration per RELEASING.md, and would make each server's release depend on a separately-versioned package. That is disproportionate for ~30 lines, so the duplication is deliberate.

The implementation matches #4576 deliberately, so all four servers end up with the same file. Two details worth noting for review:

  • The search is bounded. The manifest is either alongside the module (running from source, e.g. under vitest) or one level up (running from dist/). Those two locations are checked and nothing else, it never walks up into the monorepo and cannot pick up a parent package.json.
  • version.ts must stay at the package root. Moving it into a subdirectory breaks the dist/ case. This matters for everything, where the surrounding code is organised into subfolders.

There is one intentional difference from #4576:

-    } catch {
-      // Try the next candidate when running from dist/ or source.
+    } catch (error) {
+      // Only a missing manifest is skippable; a corrupt or unreadable one is a real failure.
+      if ((error as NodeJS.ErrnoException)?.code !== 'MODULE_NOT_FOUND') {
+        throw error;
+      }
     }

A blanket catch swallows every failure, so a corrupt or permission-denied package.json reports Could not locate package.json for server version .. misleading for a file that was found and is unreadable for a different reason. Narrowing to MODULE_NOT_FOUND lets real failures surface. Happy to drop this if reviewers would rather the four files stay byte-identical, and equally happy to open it against #4576 instead.

How Has This Been Tested?

  • npm run build clean across all packages (everything type-checks its tests, and passes).
  • Suites pass: everything 113, filesystem 158, memory 56. 18 of those are new (6 per package). sequentialthinking unchanged at 14.
  • Ran a server in Claude Desktop against the local build: connects, negotiates, and its tools work normally.

Each package's version.ts gets six tests, in two groups:

  • Four drive the resolver directly, with createRequire mocked: the happy path against the real manifest, throwing when neither candidate exists (asserting the search stops at the package root), propagation of non-MODULE_NOT_FOUND errors (EACCES), and propagation of a malformed manifest. The mock is a pass-through by default, so the happy-path assertion runs against the real filesystem in the same file; only tests that need a failure inject one.
  • Two exercise the real build .. importing dist/version.js, and spawning node dist/index.js to read serverInfo.version back over a stdio handshake (everything 2.0.0, filesystem 0.6.3, memory 0.6.3; filesystem reported 0.2.0 before this change). These use it.skipIf so an unbuilt tree still passes, matching fix(sequentialthinking): read server version from package.json #4576. They do run in CI, since npm ci triggers prepare .. npm run build.

The two propagation tests and the bounded-search test were mutation-tested: reverting the catch to a blanket catch {} fails the first two, and adding a third candidate path beyond the package root fails the third.

Breaking Changes

None. No client configuration changes. Servers now report their actual published version instead of a stale literal, which is the intent of the fix.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly — n/a, no README documents the server version
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options — n/a, none added

Additional context

This overlaps existing work, and I'd rather flag that up front than have a reviewer find it:

If maintainers would rather have one combined change, I'm happy for this to be folded into #4576 instead. Credit to @KarlLeen for reporting #4575 and to the authors of #4576 and #4557 .. the direction here is the same as theirs.

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