chore: declare the MCP SDK packages only at the repo root (#1970) - #1971
Conversation
The four v2 SDK packages (`client`, `core`, `server`, `server-legacy`) plus `ext-apps` were declared in the root manifest *and* again in clients/web, cli, and tui, so each client installed its own copy. Node resolution walks up and the root install is on every client's chain — and the root manifest is already the documented source of truth for what ships — so the per-client entries were duplicates of a decision made at the root, and they had already drifted: before this change the tree carried ext-apps 1.7.4 at the root and 1.7.5 under clients/web, dragging in two copies of the v1 `@modelcontextprotocol/sdk` (1.29.0 and 1.30.0) through ext-apps' peer dependency. Nothing imports the v1 SDK; it is not in any manifest of ours, only a peer of ext-apps, so consolidating ext-apps is the only lever over it. After a clean install there is now exactly one copy of each, at the root. `npm update` then moves ext-apps to 1.7.5 and its SDK peer to 1.30.0, whose widened `@hono/node-server` range (`^1.19.9 || ^2.0.5`) also drops a nested duplicate of that package. `express` needed a real home to make this work. `test-servers/src` imports it, but no manifest declared it: it was reaching `clients/cli/node_modules` only as a peer of `express-rate-limit` under `@modelcontextprotocol/server-legacy`, so removing that entry took express with it and every cli test that spawns a test server failed to resolve it. It is now a root devDependency — test-servers is root-owned code with no manifest of its own — and `vitest.shared.mts` resolves it from the root, joining `yaml`, which was already the precedent for exactly this case. The clients/web duplicate is dropped; its only reference is a type-only import covered by `@types/express`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
There was a problem hiding this comment.
Pull request overview
Centralizes MCP SDK dependencies at the repository root to prevent duplicate installations and version drift across clients.
Changes:
- Removes duplicate MCP dependencies from Web, CLI, and TUI manifests.
- Declares Express at the root for test-server usage.
- Updates Vitest resolution and lockfiles for root-level dependencies.
Reviewed changes
Copilot reviewed 5 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
package.json |
Adds root Express development dependency. |
package-lock.json |
Updates consolidated dependency graph. |
vitest.shared.mts |
Resolves Express from root dependencies. |
clients/web/package.json |
Removes duplicated MCP and Express declarations. |
clients/web/package-lock.json |
Removes client-local dependency copies. |
clients/cli/package.json |
Removes duplicated MCP dependencies. |
clients/cli/package-lock.json |
Removes client-local dependency copies. |
clients/tui/package.json |
Removes duplicated MCP dependencies. |
clients/tui/package-lock.json |
Removes client-local dependency copies. |
Files not reviewed (3)
- clients/cli/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Per the maintenance rules, a change to the dependency layout has to land in the docs with it. Adds the placement rule in three places, at three altitudes: - README (Setup): the rule plus the concrete drift it prevents. - AGENTS.md: a "Dependency placement" section — the MCP SDK packages are root only, the v1 SDK is not ours and must not become a dependency, and anything reached solely through root-owned code with no manifest (`test-servers/src`, `core/`) is a root devDependency aliased to the repo root in `vitest.shared.mts`. - .github/copilot-instructions.md: the distilled, reviewer-citable form, so a diff re-adding one of these to a client manifest is flagged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
Round 1 review response (e132d55)Copilot returned no comments on this one, so nothing to address. Pushing one follow-up of my own: the maintenance rules require a change to the dependency layout to land with its documentation, which the original diff was missing.
Re-requesting review since the diff grew after the first pass. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 12 changed files in this pull request and generated no new comments.
Files not reviewed (3)
- clients/cli/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
Suppressed comments (4)
README.md:69
- This states that both packages are devDependencies, but
package.json:100declaresyamlin runtimedependencieswhile onlyexpressis indevDependencies. Avoid documenting the wrong install classification, since runtime dependencies are retained in published installs while devDependencies are not.
**Where a dependency is declared.** The MCP SDK packages (`@modelcontextprotocol/client`, `core`, `server`, `server-legacy`, `ext-apps`) live in the **root** `package.json` only — never in a client's. Node resolution walks up, so the root install is on every client's chain, and the root manifest is already what the published tarball resolves against. Declaring them per client installs a second copy that can drift from the root's, which is how two versions of `ext-apps` (and of the transitive v1 `@modelcontextprotocol/sdk`) ended up in the tree before [#1970](https://github.com/modelcontextprotocol/inspector/issues/1970) — and a second copy of `client`/`core` is the failure `vitest.shared.mts` carries a `dedupe` workaround for. The same holds for anything used only by root-owned code with no manifest of its own: `express` (imported by `test-servers/src`) and `yaml` are root devDependencies, resolved from the root by `vitest.shared.mts`.
AGENTS.md:120
- This guidance incorrectly says all dependencies used by
core/belong indevDependencies; however, the published client bundles externalize npm packages (README.md:284), so a runtime import from core must remain in rootdependencies. It also contradicts the current manifest, whereyamlis a runtime dependency. Distinguish runtime from test/build-only packages while retaining the root-only placement rule.
The same rule covers anything reached only through **root-owned code that has no manifest of its own**: `express` (imported by `test-servers/src`) and `yaml` are root devDependencies, and `vitest.shared.mts` aliases both to the **repo root** rather than to `<client>/node_modules` like its other pins. If you add a dependency to `test-servers/src` or `core/`, declare it at the root and alias it there too.
.github/copilot-instructions.md:79
- This mirrored rule would direct reviewers to require every new core dependency in
devDependencies, even though core's runtime npm imports are externalized and must be installed for published consumers. It also callsyamla devDependency despitepackage.json:100. Mirror the corrected runtime-versus-development distinction fromAGENTS.md.
- Dependencies used only by **root-owned code with no manifest** (`test-servers/src`, `core/`) go in the root `devDependencies` and are aliased to the **repo root** in `vitest.shared.mts` — as `express` and `yaml` are — not to `<client>/node_modules` like the other pins there.
vitest.shared.mts:88
- The ownership explanation is inaccurate: the only
yamlsource import istest-servers/src/load-config.ts, notcoreor root tooling. Correct this so the alias rationale points to the actual root-owned consumer.
// of its own — `test-servers/src` imports express, and `core`/the root
// tooling uses yaml — so the root is where they are declared and the only
Copilot review round 2, four findings, all factual errors in the docs added by the previous commit: - `yaml` is in the root `dependencies`, not `devDependencies`, and its only importer is `test-servers/src/load-config.ts` — not `core/` or the root tooling, as the `vitest.shared.mts` comment claimed. - The rule as written told a reader to put anything reached through root-owned code — `core/` included — in `devDependencies`. That is wrong and would break the published package silently: the client builds externalize npm packages, so a published install resolves them from the root manifest, where devDependencies are absent. Placement (which manifest) and classification (which section) are now stated as separate questions, the second following from who consumes the package at runtime. `yaml`'s runtime classification is left alone and called out instead: moving it changes what ships, which is not a docs change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
Round 2 review response (5a3a343)No new inline comments; four suppressed ones, and all four were right — every one a factual error in the docs I added in 1 & 2. 3. The rule as written was actively dangerous for
The copilot-instructions mirror now explicitly asks reviewers to flag a runtime 4. The One thing I deliberately did not do:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 12 changed files in this pull request and generated no new comments.
Files not reviewed (3)
- clients/cli/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
|
Cross-reference for whoever merges: #1962 ( They are complementary rather than competing, and in the same direction:
No functional conflict: #1962's guard reads top-level lockfile entries and ignores a package absent from an install ("a package absent from an install can't skew … don't add a dependency to a client just to satisfy this"), which is exactly the state this PR produces. One tidy-up for afterwards: |
Closes #1970
Declares
@modelcontextprotocol/client,core,server,server-legacy— andext-apps— in exactly one place: the repo-rootpackage.json. Every client resolves them from there, which is how the root manifest already describes itself ("Runtime dependencies are declared on the rootpackage.json; client builds … externalize npm packages resolved from the root install").The drift was already real
Not a hypothetical. Before this change, a
node_modulescensus across the repo:@modelcontextprotocol/sdkFive manifests pinning the same packages have to be bumped in lockstep or a client builds against a different copy than the one the tarball resolves — and a second copy of
client/coreis precisely the failurevitest.shared.mtsalready carries adedupe+server.deps.inlineworkaround for (avi.mockthat misses because mock and import resolved to different copies).After a clean install there is exactly one copy of each, at the root.
On
@modelcontextprotocol/sdk(the v1 SDK)Confirmed: nothing in this repo needs it directly. It appears in no
package.jsonof ours and no source file imports it — including the composable test servers, which build on the v2server/server-legacypackages. It is installed solely as a peer dependency of@modelcontextprotocol/ext-apps(^1.29.0), which npm auto-installs; that is why it shows up only as"peer": truein the lock files.So there is nothing to remove — but consolidating
ext-appsis the only lever we have over it, and it collapses two copies at two versions into one.npm updatethen takes ext-apps to 1.7.5 (latest) and its SDK peer to 1.30.0 (latest). A bonus from that bump: 1.30.0 widens its@hono/node-serverpeer to^1.19.9 || ^2.0.5, which matches our^2and lets npm drop a nested duplicate copy of that package.express— a latent bug this surfacedtest-servers/srcimports express, but no manifest declared it. It was arriving inclients/cli/node_modulesas a peer ofexpress-rate-limit, a dependency of@modelcontextprotocol/server-legacy. Removing server-legacy from cli's manifest took express with it, and every cli test that spawns a test server failed to resolve it (14 files,Cannot find package 'express').Fixed at the root of the problem rather than by putting the pin back: express is now a root devDependency (test-servers is root-owned code with no manifest of its own), and
vitest.shared.mtsresolves it from the repo root — joiningyaml, which was already the precedent for exactly this shape. Theclients/webduplicate is dropped; its only reference there is a type-only import, covered by@types/express.clients/launcherdeclared none of these and is untouched.Gate
Full
npm run ciafter a from-scratch reinstall (allnode_modulesdeleted):validate,verify:build-gate,smoke, and Storybook (466) pass, plus 4881 unit+integration and 304 cli tests.The
coveragestep exits 1 locally on two pre-existingConnection closedunhandled rejections frominspectorClient.test.ts, reproduced identically onv2/mainwith this branch stashed — unrelated to this change.No screenshots: dependency plumbing, no user-facing surface.