-
Notifications
You must be signed in to change notification settings - Fork 2k
perf: resolve schema modules from @modelcontextprotocol/core instead of inlining them #2459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,6 +133,7 @@ | |
| "test:watch": "vitest" | ||
| }, | ||
| "dependencies": { | ||
| "@modelcontextprotocol/core": "workspace:^", | ||
| "cross-spawn": "catalog:runtimeClientOnly", | ||
|
Comment on lines
+136
to
137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 This PR adds @modelcontextprotocol/core as a runtime dependency of @modelcontextprotocol/client, @modelcontextprotocol/server, and @modelcontextprotocol/server-legacy and changes their published dist output (schema modules resolved from core instead of inlined), but ships no .changeset/*.md entry — changeset-bot confirms "No Changeset found". Consider adding a patch changeset for the three affected packages so the perf improvement and the new dependency edge get a version bump and a changelog entry. Extended reasoning...What's missing The PR changes the published runtime output of three packages — Why it matters The repo's release flow is changesets-driven ( Concrete walk-through
Why nothing breaks at merge time The How to fix Add a small patch changeset, e.g. ---
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/server-legacy': patch
---
perf: resolve the spec + OAuth schema modules from @modelcontextprotocol/core at runtime instead of inlining them, so applications using multiple SDK packages evaluate one shared schema-graph copy; core is now a regular runtime dependency of these packages.(The changeset-bot comment even links a pre-filled template for this.) |
||
| "eventsource": "catalog:runtimeClientOnly", | ||
| "eventsource-parser": "catalog:runtimeClientOnly", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import * as z from 'zod/v4'; | ||
|
|
||
| /** | ||
| * Base schema for list changed subscription options (without callback). | ||
| * Used internally for Zod validation of `autoRefresh` and `debounceMs`. | ||
| * | ||
| * Lives outside `schemas.ts` deliberately: it is an SDK option schema, not a spec | ||
| * schema, so it is not part of `@modelcontextprotocol/core`'s public surface — | ||
| * and `schemas.ts` is resolved from that package at runtime by the client/server | ||
| * bundles, which can only see the names core actually exports. | ||
| */ | ||
| export const ListChangedOptionsBaseSchema = z.object({ | ||
| /** | ||
| * If `true`, the list will be refreshed automatically when a list changed notification is received. | ||
| * The callback will be called with the updated list. | ||
| * | ||
| * If `false`, the callback will be called with `null` items, allowing manual refresh. | ||
| * | ||
| * @default true | ||
| */ | ||
| autoRefresh: z.boolean().default(true), | ||
| /** | ||
| * Debounce time in milliseconds for list changed notification processing. | ||
| * | ||
| * Multiple notifications received within this timeframe will only trigger one refresh. | ||
| * Set to `0` to disable debouncing. | ||
| * | ||
| * @default 300 | ||
| */ | ||
| debounceMs: z.number().int().nonnegative().default(300) | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ import * as schemas from './schemas'; | |
| * Explicit allowlist of protocol Zod schemas that correspond to a public spec type in `types.ts`. | ||
| * | ||
| * This intentionally excludes internal helper schemas exported from `schemas.ts` that have no | ||
| * matching public type (e.g. `ListChangedOptionsBaseSchema`, `BaseRequestParamsSchema`, | ||
| * matching public type (e.g. `BaseRequestParamsSchema`, | ||
| * `NotificationsParamsSchema`, `ClientTasksCapabilitySchema`, `ServerTasksCapabilitySchema`). | ||
|
Comment on lines
+24
to
25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The stale 'ListChangedOptionsBaseSchema' entry remains in the INTERNAL_HELPER_SCHEMAS array in packages/core-internal/test/types/specTypeSchema.test.ts (line 169), even though this PR moved that schema out of types/schemas.ts and removed it from the exclusion comment in specTypeSchema.ts and from the parallel list in packages/core/test/coreSchemas.test.ts. The entry is now dead (it can never match Object.keys(schemas)) and the array's "mirrors the exclusion comment" claim is no longer accurate — removing that one line keeps the three lists consistent. Extended reasoning...What the issue is: The PR relocates |
||
| * Keeping the list explicit means new public spec types must be added here deliberately, and | ||
| * internals never leak into `SpecTypeName`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { existsSync } from 'node:fs'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { beforeAll, describe, expect, test } from 'vitest'; | ||
|
|
||
| const pkgDir = join(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const distEntry = join(pkgDir, 'dist', 'index.mjs'); | ||
|
|
||
| // client, server, and server-legacy resolve their schema modules from this | ||
| // package's dist entry at runtime, so a missing or incomplete build breaks | ||
| // every consumer of a built package (the repo's CI test job never runs a | ||
| // standalone build — dist only exists if a test builds it, the same way | ||
| // client's barrelClean.test.ts builds client). Build on demand and pin the | ||
| // runtime surface the boundary depends on. | ||
| describe('@modelcontextprotocol/core dist entry', () => { | ||
| beforeAll(() => { | ||
| if (!existsSync(distEntry)) { | ||
| execFileSync('pnpm', ['build'], { cwd: pkgDir, stdio: 'inherit' }); | ||
| } | ||
| }, 120_000); | ||
|
|
||
| test('root entry resolves and exposes the runtime schema surface', async () => { | ||
| const mod = await import(distEntry); | ||
| for (const name of [ | ||
| 'CallToolResultSchema', | ||
| 'InitializeRequestSchema', | ||
| 'JSONRPCMessageSchema', | ||
| 'OAuthMetadataSchema', | ||
| 'OAuthProtectedResourceMetadataSchema' | ||
| ]) { | ||
| expect(mod[name], `core dist entry must export ${name}`).toBeDefined(); | ||
| } | ||
| // The externalized boundary re-exports the whole spec schema surface; | ||
| // a partial build would show up as a collapsed export count. | ||
| expect(Object.keys(mod).length).toBeGreaterThan(150); | ||
| }); | ||
| }); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The user-facing docs still describe the pre-PR relationship between @modelcontextprotocol/core and the client/server packages: docs/advanced/wire-schemas.md:59 says server and client "never depend on it" and docs/get-started/packages.md:69 says "you never install it", but after this PR core is a runtime dependency of client/server/server-legacy and their bundles import the schema modules from it. These two doc sentences should be updated in this PR alongside the CLAUDE.md change that already reflects the new relationship.
Extended reasoning...
What's stale. This PR turns
@modelcontextprotocol/coreinto a real runtime dependency ofclient,server, andserver-legacy: each package.json adds"@modelcontextprotocol/core": "workspace:^"todependencies, and the newexternalize-core-schemasplugin in the three tsdown configs rewrites imports ofcore-internal/src/types/schemas.tsandcore-internal/src/shared/auth.tsto@modelcontextprotocol/coreas an external, so the published bundles import the schema modules from that package at runtime. CLAUDE.md line 73 was updated to describe exactly this ("Consumed by the sibling packages at runtime…"), but the user-facing docs were not.\n\nWhere the docs now contradict the implementation.\n\n-docs/advanced/wire-schemas.md:59: "Install it separately (npm install @modelcontextprotocol/core) —@modelcontextprotocol/serverand@modelcontextprotocol/clientkeep a Zod-free public surface and never depend on it." — the "never depend on it" clause is now flatly false, and "install it separately" is misleading since core now arrives transitively with client/server.\n-docs/get-started/packages.md:69: "…so if you only callregisterToolandcallToolyou never install it." — after this PR core is always installed transitively whenever client or server is installed.\n\nConcrete walk-through. A user runsnpm install @modelcontextprotocol/server. npm resolvesdependenciesand pulls@modelcontextprotocol/coreintonode_modules(the pnpm-lock.yaml diff in this PR shows the same for the workspace). The server bundle'sdist/index.mjsnow containsimport … from '@modelcontextprotocol/core'where the schema graph used to be inlined, so the package is loaded at runtime, not just present on disk. Meanwhile the docs the user reads say the package is something they'd "install separately" and that server/client "never depend on it" — the opposite of whatnpm ls @modelcontextprotocol/corewill show them.\n\nWhy nothing else prevents this. The parts of those sentences about the public surface remain true — client/server still export no Zod schemas and their public API stays Zod-free — so the pages don't fail any snippet-sync or example CI check; only the prose about the dependency graph is wrong, and nothing automated validates prose against package.json.\n\nImpact and fix. No runtime behavior is affected; this is documentation drift only, but it's drift introduced by this PR (which already fixed the equivalent sentence in CLAUDE.md), so it's cheapest to fix here. Suggested edits: in wire-schemas.md, replace "Install it separately … never depend on it" with wording like "It is installed automatically as a dependency of@modelcontextprotocol/serverand@modelcontextprotocol/client(their bundles resolve the schema modules from it), but their public surfaces stay Zod-free — import from@modelcontextprotocol/coredirectly when you need the raw schemas." In packages.md, change "you never install it" to something like "you never need to import it directly (it's installed transitively withclient/server)."