preview: combined import-cost changes (#2458 + #2476 + #2477)#2482
Closed
felixweinberger wants to merge 14 commits into
Closed
preview: combined import-cost changes (#2458 + #2476 + #2477)#2482felixweinberger wants to merge 14 commits into
felixweinberger wants to merge 14 commits into
Conversation
AjvJsonSchemaValidator built its Ajv2020 instance (plus ajv-formats registration) in the constructor. Client and Server construct the default validator unconditionally, so every embedding application paid several milliseconds of AJV instantiation at startup even when no JSON Schema validation ever runs - a tax on CLI cold start in particular. Defer engine creation to the first getValidator() call via a private lazy getter. A caller-supplied engine is still used as-is from construction, the 2020-12 dialect check still fires before any engine is built, and the public API is unchanged.
No-Verification-Needed: changeset-only commit
Importing the client or server package used to construct both frozen era wire-schema graphs (2025-11-25 and 2026-07-28) at module evaluation, before any message was validated. Move each era's schema definitions into a buildSchemas() factory memoized at module level: - schema definitions are verbatim from the old era schemas.ts (and the CallToolResultWireSchema wire seam verbatim from the 2025 registry, which now serves it through the same memo) - registries keep method membership and the exported method lists as static null-valued key objects mapped over the same wire unions, so the both-direction drift guards remain compile errors and importing a registry constructs nothing - codecs and inputRequired pull schemas through the memo at first use - each era schemas.ts becomes an eager named-export shim over the memo for tests and tooling; the shim is not on any runtime import path and tree-shakes out of the published bundles Reference identity is preserved: registry lookups, codecs, and the shim all serve objects from one memo, so the by-reference registry pins hold (the pin test now warms the memo via the shim import). Importing client+server gets roughly a third cheaper in a scratch consumer; all package suites, the examples e2e matrix, typecheck, and lint pass unchanged.
The shim restated the SEP-1686 task deprecations but dropped the SEP-2577 tags on the logging, sampling, and roots families; restore each block verbatim from the pre-factoring module so importers keep IDE strike-through and deprecation lint.
The MCP spec schemas (types/schemas.ts), the OAuth/OpenID schemas (shared/auth.ts), the protocol constants (types/constants.ts), and the three JSON value types move verbatim from core-internal into packages/core/src — core now owns the schema sources instead of bundling them out of core-internal through build-only subpath aliases. Mechanism: - core's curated root entry re-exports the same public surface as before (unchanged 172 exports), now from its own local modules. - A new ./internal subpath re-exports the moved modules wholesale for the sibling SDK packages (internal helper schemas, auth types, constants, JSON types — names that are deliberately not public on the root entry). - core-internal keeps the old module paths as one-to-one named re-export shims, so no importer or test changes anywhere. - client/server/server-legacy declare @modelcontextprotocol/core as a real dependency; their bundles keep @modelcontextprotocol/core/internal as an external runtime import (explicit tsdown external entry) instead of carrying their own bundled schema copies. - The build-only core-internal/schemas + core-internal/auth tsconfig/tsdown aliases are deleted everywhere; the per-package tsconfigs gain a single source-first alias for @modelcontextprotocol/core/internal so typecheck and vitest stay build-order independent. Layering stays acyclic: core depends only on zod; core-internal depends on core; the wire era modules are untouched (their frozen copies and their runtime constants imports resolve through the shims unchanged).
…home Test-only adaptations to the schema-source move; each guard keeps pinning the same invariant, only pointed at the new canonical location: - packageTopologyPins: pin @modelcontextprotocol/core's export map as ['.', './internal'] — the internal seam is deliberate, not public API. - coreSchemas: read the spec-schema source from core's own src/schemas.ts (the auth group still reads core-internal's authSchemas registry). - wireOnlyHiding: read the @deprecated task-schema and constants sources from packages/core/src (the old paths are now re-export shims with no doc comments to scan). - codemod authSchemaNames: core's barrel now ends its auth block with "} from './auth'" instead of the deleted build-only alias specifier. - cloudflareWorkers: generalize packServerPackage to packWorkspacePackage and install the workspace core tarball alongside the server tarball — the packed server resolves @modelcontextprotocol/core/internal at runtime, which the registry copy of core does not carry yet.
The schema sources moved into @modelcontextprotocol/core with the sibling packages resolving @modelcontextprotocol/core/internal at runtime. That seam can rot in ways no existing test catches; this locks it down: - Exact sibling pins: client/server/server-legacy (and core-internal, for consistency) depend on core via workspace:* so pnpm publishes an exact version pin instead of a caret range. The ./internal surface is only guaranteed for the core version each sibling was built against, so a caret range would let installs mix skewed versions. - Changesets fixed group for core + client + server + server-legacy, keeping the pinned versions releasable in lockstep. - ./internal is labeled as an SDK-internal contract in its header, with @internal JSDoc on the re-exports (source-level only: the dts bundler flattens the re-exports and drops statement-level comments, so the built declarations do not carry the tag). - A client boundary test parses the built dists in both directions: every name the client bundle imports or re-exports from core must resolve against core's built entry export lists (skew), and sentinel schemas that exist only in core's modules must never appear as definitions in the client bundle (re-inlining via lost external config or eager aliases). - A core-internal shim-purity test pins the old schema/auth/constants module paths as pure re-export forwards: no zod import, no local definitions, imports only from @modelcontextprotocol/core/internal — so new schemas can't accrete at the old paths and silently miss core's published entries. The shim headers now state that rule, and the type-only JSON value re-export in types.ts is pinned as erasable. No-Verification-Needed: tests, manifest pins, release config, and comments only — no runtime surface change
No-Verification-Needed: release-metadata-only change
barrelClean and coreBoundary each rebuilt a missing dist from their own beforeAll; vitest runs test files in separate workers, so a cold checkout raced two pnpm builds in the same package and the clean step deleted files under the other worker's reader. Route both through a shared single-flight helper: an atomic mkdir lock with one canonical sentinel set per package, a stale-lock steal for holders that died without cleanup, an async build bounded by a timeout so the worker's event loop and vitest's hook timer stay live, and only EEXIST treated as contention. No-Verification-Needed: test-only change, no runtime surface
The builds take seconds; the previous bounds were sized by stacking worst cases on worst cases. Keep the ordering invariant (build kill < waiter deadline < stale-lock steal < hook timeout) at realistic magnitudes: 60s/90s/120s with 180s and 240s hooks. No-Verification-Needed: test-only timeout constants
… blind spots - Add typesVersions for the ./internal subpath so moduleResolution:node10 consumers resolve its declarations (exports maps are invisible there) - Update wire-schemas and packages docs: core now arrives transitively as the shared runtime schema graph of client/server/server-legacy - Boundary test: scan dist recursively (validators/ chunks were excluded) and reject bare side-effect imports of core, which have no from-clause
…to scratch/es2-measure
…o scratch/es2-measure
🦋 Changeset detectedLatest commit: 60d7f1f The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
The wire schemas are built lazily since the memoized-factory change, which is the right default on process-per-invocation runtimes where module evaluation is boot latency. On isolate-based serverless platforms the accounting inverts: module scope evaluates during deployment/isolate warm-up outside any request's billed CPU, so lazy construction lands inside the first request each fresh isolate serves - observable in production as a per-request CPU regression with a fresh-isolate-first-request signature. - preloadSchemas() (core-internal wire/preload.ts): synchronous, idempotent; forces both era schema factories and every memoized lookup layer above them (the 2025 registry maps, the 2026 in-band input-request maps, the 2026 wire-result wrappers), so post-preload validation constructs nothing. Exported as public API from the client and server package roots for platforms that bill request CPU but not module evaluation. - The workerd shims call it at module scope, so Cloudflare Workers deployments get eager construction automatically. Node and browser builds stay lazy; the server package gains a dedicated browser shim (its browser export condition previously reused the workerd shim, which would have leaked the eager call into browser bundles). - Dist-level pins in both packages: the workerd entries must carry the module-scope call, node/browser entries must not, and the shim must import preloadSchemas from the same shared chunk as the root entry (a duplicated definition would warm a twin module graph and leave the real one cold). - The server dist suites now share a single-flight build helper so two test files cannot race tsdown's clean step on a cold checkout; the client helper's dist sentinels now cover the shim entries. Verified against packed tarballs in a scratch consumer: a bare import constructs nothing (first preloadSchemas call does the work, second is free); a wrangler bundle resolves shimsWorkerd and contains exactly one module-scope call; an esbuild browser bundle resolves shimsBrowser and contains none; the Cloudflare Workers integration test stays green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft aggregation of the three open import-cost PRs at their current heads, merged onto main — not for merge; it exists so the combined build can be installed and integration-tested from the Continuous Releases preview packages below.
Review happens on the individual PRs; this branch is their clean merge (no additional changes). Full workspace gates pass on the combined tree.