perf: build era wire schemas lazily through memoized factories (#2476)

This commit is contained in:
Felix Weinberger 2026-07-13 17:03:47 +01:00 • committed by GitHub
commit e0a0ab74d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 4467 additions and 3538 deletions

View file

@ -0,0 +1,7 @@
---
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/server-legacy': patch
---
Build protocol-revision wire schemas lazily on first validation instead of at import. Each revision's schema set is now constructed by a module-level memoized factory, so importing the client or server package no longer pays the construction cost of both frozen wire-schema graphs up front. Method membership in the revision registries stays static, the schemas themselves are unchanged, and registry lookups keep returning reference-identical schema objects.

File diff suppressed because it is too large Load diff

View file

@ -31,9 +31,9 @@ import type * as z from 'zod/v4';
import type { CallToolResult, Result } from '../../types/types';
import type { DecodedResult, EnvelopeIssue, LiftedWireMaterial, OutboundEnvelopeMaterial, ValidateOutcome, WireCodec } from '../codec';
import { appendTextFallbackForNonObject } from '../textFallback';
import { buildSchemas2025 } from './buildSchemas';
import { isNonObjectJsonSchemaRoot, wrapOutputSchemaForLegacy } from './legacyWrap';
import { getNotificationSchema, getRequestSchema, getResultSchema, hasNotificationMethod2025, hasRequestMethod2025 } from './registry';
import { CreateMessageResultSchema, CreateMessageResultWithToolsSchema } from './schemas';
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
@ -75,9 +75,13 @@ export const rev2025Codec: WireCodec = {
validateInputResponse: (): ValidateOutcome<never> => NOT_IN_ERA,
// Arrow literals can't carry overload signatures; the cast is sound (the
// boolean dispatches to exactly the schema each overload names).
samplingResultVariant: ((hasTools: boolean, raw: unknown) =>
triState(hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema, raw)) as WireCodec['samplingResultVariant'],
// boolean dispatches to exactly the schema each overload names). The
// schemas are pulled through the era's memo so the codec module itself
// stays construction-free at import time.
samplingResultVariant: ((hasTools: boolean, raw: unknown) => {
const s = buildSchemas2025();
return triState(hasTools ? s.CreateMessageResultWithToolsSchema : s.CreateMessageResultSchema, raw);
}) as WireCodec['samplingResultVariant'],
// The 2025 era carries no per-request `_meta` envelope — legacy wire
// bytes stay identical (the never-stamp guarantee, outbound-request half).

View file

@ -20,67 +20,29 @@
* 2026-only vocabulary (`server/discover`, `subscriptions/listen`, the MRTR
* shells, `resultType`, the `_meta` envelope) has NO entry and NO code path
* here — the inverse-leak guarantee is physical absence, not discipline.
*
* LAZY SCHEMA CONSTRUCTION: method membership and the exported method lists
* are static (null-valued key objects below — the mapped-type keying keeps
* the both-direction drift guard), while the schema VALUES are pulled through
* the era's memoized `buildSchemas2025()` factory on first lookup. Importing
* this module therefore constructs no zod schemas; the first validation does,
* once, and every consumer (registry, codec, the eager `schemas.ts` shim)
* sees the same objects, so the by-reference pins keep holding.
*/
import * as z from 'zod/v4';
import type * as z from 'zod/v4';
import type { NotificationMethod, NotificationTypeMap, RequestMethod, RequestTypeMap, ResultTypeMap } from '../../types/types';
import { normalizeContentlessToolResult, TOOL_RESULT_FOREIGN_FAMILY_KEYS } from '../resultFamilies';
import type { ClientNotificationSchema, ClientRequestSchema, ServerNotificationSchema, ServerRequestSchema } from './schemas';
import {
CallToolRequestSchema,
CallToolResultSchema,
CancelledNotificationSchema,
CancelTaskRequestSchema,
CompleteRequestSchema,
CompleteResultSchema,
CreateMessageRequestSchema,
CreateMessageResultWithToolsSchema,
ElicitationCompleteNotificationSchema,
ElicitRequestSchema,
ElicitResultSchema,
EmptyResultSchema,
GetPromptRequestSchema,
GetPromptResultSchema,
GetTaskPayloadRequestSchema,
GetTaskRequestSchema,
InitializedNotificationSchema,
InitializeRequestSchema,
InitializeResultSchema,
ListPromptsRequestSchema,
ListPromptsResultSchema,
ListResourcesRequestSchema,
ListResourcesResultSchema,
ListResourceTemplatesRequestSchema,
ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListRootsResultSchema,
ListTasksRequestSchema,
ListToolsRequestSchema,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
PingRequestSchema,
ProgressNotificationSchema,
PromptListChangedNotificationSchema,
ReadResourceRequestSchema,
ReadResourceResultSchema,
ResourceListChangedNotificationSchema,
ResourceUpdatedNotificationSchema,
RootsListChangedNotificationSchema,
SetLevelRequestSchema,
SubscribeRequestSchema,
TaskStatusNotificationSchema,
ToolListChangedNotificationSchema,
UnsubscribeRequestSchema
} from './schemas';
import type { Rev2025WireSchemas } from './buildSchemas';
import { buildSchemas2025 } from './buildSchemas';
/* The era's wire vocabulary, derived from the wire role unions in
* `./schemas.ts` (the same unions the registries used to be built from at
* runtime). Keying the maps by these derived unions makes drift a compile
* `./buildSchemas.ts` (the same unions the registries used to be built from
* at runtime). Keying the maps by these derived unions makes drift a compile
* error in BOTH directions: a union member without a map entry, a map entry
* the unions do not know, and an entry pointing at a different method's
* schema all fail to typecheck. */
type WireRequest = z.output<typeof ClientRequestSchema> | z.output<typeof ServerRequestSchema>;
type WireNotification = z.output<typeof ClientNotificationSchema> | z.output<typeof ServerNotificationSchema>;
type WireRequest = z.output<Rev2025WireSchemas['ClientRequestSchema']> | z.output<Rev2025WireSchemas['ServerRequestSchema']>;
type WireNotification = z.output<Rev2025WireSchemas['ClientNotificationSchema']> | z.output<Rev2025WireSchemas['ServerNotificationSchema']>;
/** Every request method in the 2025-era wire vocabulary (the typed `RequestMethod` surface plus the task family). */
export type Rev2025RequestMethod = WireRequest['method'];
@ -98,115 +60,171 @@ export type Rev2025NotificationMethod = WireNotification['method'];
*/
type Rev2025TypedRequestMethod = Extract<RequestMethod, Rev2025RequestMethod>;
/* Runtime schema lookup — result schemas by method */
// Keyed by the era's typed-method subset and valued by
// `z.ZodType<ResultTypeMap[M]>` so the runtime map and the typed
// `ResultTypeMap` cannot drift: a missing entry, an extra key, or an entry
// that does not parse to the typed map's result type is a compile error. No
// entry may be looser than the typed map (no task-result union members) and
// no key may fall outside it (no `tasks/*` entries — the task methods are
// 2025-11-25 wire vocabulary with no SDK runtime; callers needing task
// interop pass an explicit schema).
/**
* Wire seam: owns both halves of the v1-parity ruling — the guard (a content-less body
* carrying another result family's keys fails loudly; the era is frozen so the key list is
* complete) and the tolerance (`content` defaults to `[]`). The era file stays twin-conformant.
*/
export const CallToolResultWireSchema = z
.unknown()
.superRefine((value, ctx) => {
// content === undefined covers both an absent key and an explicit
// undefined from server-side authoring objects.
if (typeof value !== 'object' || value === null || Array.isArray(value) || (value as Record<string, unknown>).content !== undefined)
return;
for (const key of TOOL_RESULT_FOREIGN_FAMILY_KEYS) {
if (key in value) {
ctx.addIssue({
code: 'custom',
message: `content is required when the body carries '${key}' — another result family cannot default into an empty tools/call success`
});
return;
}
}
})
.transform(normalizeContentlessToolResult)
.pipe(CallToolResultSchema);
const resultSchemas: { readonly [M in Rev2025TypedRequestMethod]: z.ZodType<ResultTypeMap[M]> } = {
ping: EmptyResultSchema,
initialize: InitializeResultSchema,
'completion/complete': CompleteResultSchema,
'logging/setLevel': EmptyResultSchema,
'prompts/get': GetPromptResultSchema,
'prompts/list': ListPromptsResultSchema,
'resources/list': ListResourcesResultSchema,
'resources/templates/list': ListResourceTemplatesResultSchema,
'resources/read': ReadResourceResultSchema,
'resources/subscribe': EmptyResultSchema,
'resources/unsubscribe': EmptyResultSchema,
'tools/call': CallToolResultWireSchema,
'tools/list': ListToolsResultSchema,
'sampling/createMessage': CreateMessageResultWithToolsSchema,
'elicitation/create': ElicitResultSchema,
'roots/list': ListRootsResultSchema
};
/* Runtime schema lookup — request and notification schemas by method.
/* Static method membership — the schema-free half of the registry.
*
* The entries are the SAME schema objects the wire role unions are built
* from (reference identity is pinned by `test/types/registryPins.test.ts`),
* and the key order preserves the pre-split union iteration order so the
* exported method lists are byte-identical to the builder they replace. */
const requestSchemas: { readonly [M in Rev2025RequestMethod]: z.ZodType<Extract<WireRequest, { method: M }>> } = {
ping: PingRequestSchema,
initialize: InitializeRequestSchema,
'completion/complete': CompleteRequestSchema,
'logging/setLevel': SetLevelRequestSchema,
'prompts/get': GetPromptRequestSchema,
'prompts/list': ListPromptsRequestSchema,
'resources/list': ListResourcesRequestSchema,
'resources/templates/list': ListResourceTemplatesRequestSchema,
'resources/read': ReadResourceRequestSchema,
'resources/subscribe': SubscribeRequestSchema,
'resources/unsubscribe': UnsubscribeRequestSchema,
'tools/call': CallToolRequestSchema,
'tools/list': ListToolsRequestSchema,
'tasks/get': GetTaskRequestSchema,
'tasks/result': GetTaskPayloadRequestSchema,
'tasks/list': ListTasksRequestSchema,
'tasks/cancel': CancelTaskRequestSchema,
'sampling/createMessage': CreateMessageRequestSchema,
'elicitation/create': ElicitRequestSchema,
'roots/list': ListRootsRequestSchema
* These null-valued objects carry ONLY the method keys, in the same order the
* eager schema maps used to declare them (so the exported method lists stay
* byte-identical to the pre-lazy builder). The mapped-type keying preserves
* the both-direction drift guard: a union member without a key, or a key the
* union does not know, is a compile error. Membership checks and the exported
* lists read these objects and never touch the schema memo. */
const requestMethodKeys: { readonly [M in Rev2025RequestMethod]: null } = {
ping: null,
initialize: null,
'completion/complete': null,
'logging/setLevel': null,
'prompts/get': null,
'prompts/list': null,
'resources/list': null,
'resources/templates/list': null,
'resources/read': null,
'resources/subscribe': null,
'resources/unsubscribe': null,
'tools/call': null,
'tools/list': null,
'tasks/get': null,
'tasks/result': null,
'tasks/list': null,
'tasks/cancel': null,
'sampling/createMessage': null,
'elicitation/create': null,
'roots/list': null
};
const notificationSchemas: { readonly [M in Rev2025NotificationMethod]: z.ZodType<Extract<WireNotification, { method: M }>> } = {
'notifications/cancelled': CancelledNotificationSchema,
'notifications/progress': ProgressNotificationSchema,
'notifications/initialized': InitializedNotificationSchema,
'notifications/roots/list_changed': RootsListChangedNotificationSchema,
'notifications/tasks/status': TaskStatusNotificationSchema,
'notifications/message': LoggingMessageNotificationSchema,
'notifications/resources/updated': ResourceUpdatedNotificationSchema,
'notifications/resources/list_changed': ResourceListChangedNotificationSchema,
'notifications/tools/list_changed': ToolListChangedNotificationSchema,
'notifications/prompts/list_changed': PromptListChangedNotificationSchema,
'notifications/elicitation/complete': ElicitationCompleteNotificationSchema
const notificationMethodKeys: { readonly [M in Rev2025NotificationMethod]: null } = {
'notifications/cancelled': null,
'notifications/progress': null,
'notifications/initialized': null,
'notifications/roots/list_changed': null,
'notifications/tasks/status': null,
'notifications/message': null,
'notifications/resources/updated': null,
'notifications/resources/list_changed': null,
'notifications/tools/list_changed': null,
'notifications/prompts/list_changed': null,
'notifications/elicitation/complete': null
};
const resultMethodKeys: { readonly [M in Rev2025TypedRequestMethod]: null } = {
ping: null,
initialize: null,
'completion/complete': null,
'logging/setLevel': null,
'prompts/get': null,
'prompts/list': null,
'resources/list': null,
'resources/templates/list': null,
'resources/read': null,
'resources/subscribe': null,
'resources/unsubscribe': null,
'tools/call': null,
'tools/list': null,
'sampling/createMessage': null,
'elicitation/create': null,
'roots/list': null
};
/* Lazy schema maps — built once, on the first schema lookup, from the era's
* memoized schema factory. The entries are the SAME schema objects the wire
* role unions are built from (reference identity is pinned by
* `test/types/registryPins.test.ts`), and the key order preserves the
* pre-split union iteration order. The mapped types below are unchanged from
* the eager maps, so the drift guards still apply entry by entry. */
interface RegistryMaps {
/* Runtime schema lookup — request and notification schemas by method. */
readonly requestSchemas: { readonly [M in Rev2025RequestMethod]: z.ZodType<Extract<WireRequest, { method: M }>> };
readonly notificationSchemas: { readonly [M in Rev2025NotificationMethod]: z.ZodType<Extract<WireNotification, { method: M }>> };
/* Runtime schema lookup — result schemas by method.
*
* Keyed by the era's typed-method subset and valued by
* `z.ZodType<ResultTypeMap[M]>` so the runtime map and the typed
* `ResultTypeMap` cannot drift: a missing entry, an extra key, or an entry
* that does not parse to the typed map's result type is a compile error. No
* entry may be looser than the typed map (no task-result union members) and
* no key may fall outside it (no `tasks/*` entries — the task methods are
* 2025-11-25 wire vocabulary with no SDK runtime; callers needing task
* interop pass an explicit schema). The `tools/call` entry is the wire-seam
* wrapper `CallToolResultWireSchema` (content-default guard + tolerance),
* which lives with the era schemas in `./buildSchemas.ts`. */
readonly resultSchemas: { readonly [M in Rev2025TypedRequestMethod]: z.ZodType<ResultTypeMap[M]> };
}
let maps: RegistryMaps | undefined;
function registryMaps(): RegistryMaps {
if (maps) return maps;
const s = buildSchemas2025();
maps = {
requestSchemas: {
ping: s.PingRequestSchema,
initialize: s.InitializeRequestSchema,
'completion/complete': s.CompleteRequestSchema,
'logging/setLevel': s.SetLevelRequestSchema,
'prompts/get': s.GetPromptRequestSchema,
'prompts/list': s.ListPromptsRequestSchema,
'resources/list': s.ListResourcesRequestSchema,
'resources/templates/list': s.ListResourceTemplatesRequestSchema,
'resources/read': s.ReadResourceRequestSchema,
'resources/subscribe': s.SubscribeRequestSchema,
'resources/unsubscribe': s.UnsubscribeRequestSchema,
'tools/call': s.CallToolRequestSchema,
'tools/list': s.ListToolsRequestSchema,
'tasks/get': s.GetTaskRequestSchema,
'tasks/result': s.GetTaskPayloadRequestSchema,
'tasks/list': s.ListTasksRequestSchema,
'tasks/cancel': s.CancelTaskRequestSchema,
'sampling/createMessage': s.CreateMessageRequestSchema,
'elicitation/create': s.ElicitRequestSchema,
'roots/list': s.ListRootsRequestSchema
},
notificationSchemas: {
'notifications/cancelled': s.CancelledNotificationSchema,
'notifications/progress': s.ProgressNotificationSchema,
'notifications/initialized': s.InitializedNotificationSchema,
'notifications/roots/list_changed': s.RootsListChangedNotificationSchema,
'notifications/tasks/status': s.TaskStatusNotificationSchema,
'notifications/message': s.LoggingMessageNotificationSchema,
'notifications/resources/updated': s.ResourceUpdatedNotificationSchema,
'notifications/resources/list_changed': s.ResourceListChangedNotificationSchema,
'notifications/tools/list_changed': s.ToolListChangedNotificationSchema,
'notifications/prompts/list_changed': s.PromptListChangedNotificationSchema,
'notifications/elicitation/complete': s.ElicitationCompleteNotificationSchema
},
resultSchemas: {
ping: s.EmptyResultSchema,
initialize: s.InitializeResultSchema,
'completion/complete': s.CompleteResultSchema,
'logging/setLevel': s.EmptyResultSchema,
'prompts/get': s.GetPromptResultSchema,
'prompts/list': s.ListPromptsResultSchema,
'resources/list': s.ListResourcesResultSchema,
'resources/templates/list': s.ListResourceTemplatesResultSchema,
'resources/read': s.ReadResourceResultSchema,
'resources/subscribe': s.EmptyResultSchema,
'resources/unsubscribe': s.EmptyResultSchema,
'tools/call': s.CallToolResultWireSchema,
'tools/list': s.ListToolsResultSchema,
'sampling/createMessage': s.CreateMessageResultWithToolsSchema,
'elicitation/create': s.ElicitResultSchema,
'roots/list': s.ListRootsResultSchema
}
};
return maps;
}
/** The 2025-era request-method set (registry membership = the deletion story). */
export function hasRequestMethod2025(method: string): method is Rev2025RequestMethod {
return Object.prototype.hasOwnProperty.call(requestSchemas, method);
return Object.prototype.hasOwnProperty.call(requestMethodKeys, method);
}
/** The 2025-era notification-method set. */
export function hasNotificationMethod2025(method: string): method is Rev2025NotificationMethod {
return Object.prototype.hasOwnProperty.call(notificationSchemas, method);
return Object.prototype.hasOwnProperty.call(notificationMethodKeys, method);
}
/** Result-map membership: exactly the era's typed-method subset (no task entries, no 2026-only methods). */
function hasResultMethod(method: string): method is Rev2025TypedRequestMethod {
return Object.prototype.hasOwnProperty.call(resultSchemas, method);
return Object.prototype.hasOwnProperty.call(resultMethodKeys, method);
}
/**
@ -219,7 +237,7 @@ function hasResultMethod(method: string): method is Rev2025TypedRequestMethod {
export function getResultSchema<M extends Rev2025TypedRequestMethod>(method: M): z.ZodType<ResultTypeMap[M]>;
export function getResultSchema(method: string): z.ZodType | undefined;
export function getResultSchema(method: string): z.ZodType | undefined {
return hasResultMethod(method) ? resultSchemas[method] : undefined;
return hasResultMethod(method) ? registryMaps().resultSchemas[method] : undefined;
}
/**
@ -231,7 +249,7 @@ export function getResultSchema(method: string): z.ZodType | undefined {
export function getRequestSchema<M extends Rev2025TypedRequestMethod>(method: M): z.ZodType<RequestTypeMap[M]>;
export function getRequestSchema(method: string): z.ZodType | undefined;
export function getRequestSchema(method: string): z.ZodType | undefined {
return hasRequestMethod2025(method) ? requestSchemas[method] : undefined;
return hasRequestMethod2025(method) ? registryMaps().requestSchemas[method] : undefined;
}
/**
@ -242,9 +260,9 @@ export function getRequestSchema(method: string): z.ZodType | undefined {
export function getNotificationSchema<M extends NotificationMethod>(method: M): z.ZodType<NotificationTypeMap[M]>;
export function getNotificationSchema(method: string): z.ZodType | undefined;
export function getNotificationSchema(method: string): z.ZodType | undefined {
return hasNotificationMethod2025(method) ? notificationSchemas[method] : undefined;
return hasNotificationMethod2025(method) ? registryMaps().notificationSchemas[method] : undefined;
}
/** Registry method lists (for the spec-method universe and the CI registry-diff oracle). */
export const rev2025RequestMethods: readonly string[] = Object.keys(requestSchemas);
export const rev2025NotificationMethods: readonly string[] = Object.keys(notificationSchemas);
export const rev2025RequestMethods: readonly string[] = Object.keys(requestMethodKeys);
export const rev2025NotificationMethods: readonly string[] = Object.keys(notificationMethodKeys);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -32,6 +32,7 @@ import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, LOG_LEVEL_META_KEY,
import type { CallToolResult, Result } from '../../types/types';
import type { DecodedResult, EnvelopeIssue, LiftedWireMaterial, OutboundEnvelopeMaterial, ValidateOutcome, WireCodec } from '../codec';
import { appendTextFallbackForNonObject } from '../textFallback';
import { buildSchemas2026 } from './buildSchemas';
import { fillCacheFields, stampResultType } from './encodeContract';
import { getInputRequestSchema2026, getInputResponseSchema2026 } from './inputRequired';
import {
@ -41,18 +42,6 @@ import {
hasNotificationMethod2026,
hasRequestMethod2026
} from './registry';
import {
CallToolResultSchema,
CompleteResultSchema,
DiscoverResultSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListToolsResultSchema,
ReadResourceResultSchema,
RequestMetaEnvelopeSchema
} from './schemas';
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
@ -146,7 +135,7 @@ export const rev2026Codec: WireCodec & {
for (const key of REQUIRED_ENVELOPE_KEYS) {
if (!(key in meta)) issues.push({ key, problem: 'missing' });
}
const parsed = RequestMetaEnvelopeSchema.safeParse(meta);
const parsed = buildSchemas2026().RequestMetaEnvelopeSchema.safeParse(meta);
if (!parsed.success) {
for (const issue of parsed.error.issues) {
const path = issue.path.map(String);
@ -244,7 +233,8 @@ export const rev2026Codec: WireCodec & {
// Own-key lookup: `method` is peer-influenced on related-request
// paths, and a prototype-chain hit (e.g. 'constructor') must not
// masquerade as a schema and throw out of the decode hop.
const wireSchema = Object.hasOwn(WIRE_RESULT_SCHEMAS, method) ? WIRE_RESULT_SCHEMAS[method] : undefined;
const wireResultSchemas = getWireResultSchemas();
const wireSchema = Object.hasOwn(wireResultSchemas, method) ? wireResultSchemas[method] : undefined;
if (wireSchema !== undefined) {
const parsed = wireSchema.safeParse(raw);
if (!parsed.success) {
@ -280,7 +270,7 @@ export const rev2026Codec: WireCodec & {
'(io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo, io.modelcontextprotocol/clientCapabilities)'
);
}
const parsed = RequestMetaEnvelopeSchema.safeParse(material.envelope);
const parsed = buildSchemas2026().RequestMetaEnvelopeSchema.safeParse(material.envelope);
if (!parsed.success) {
return `Invalid _meta envelope for protocol revision 2026-07-28: ${parsed.error.issues.map(issue => issue.message).join('; ')}`;
}
@ -288,15 +278,23 @@ export const rev2026Codec: WireCodec & {
}
};
/** Wire-true result wrappers consulted by decode step 2, keyed by method. */
const WIRE_RESULT_SCHEMAS: Record<string, z.ZodType> = {
'tools/call': CallToolResultSchema,
'tools/list': ListToolsResultSchema,
'prompts/get': GetPromptResultSchema,
'prompts/list': ListPromptsResultSchema,
'resources/list': ListResourcesResultSchema,
'resources/templates/list': ListResourceTemplatesResultSchema,
'resources/read': ReadResourceResultSchema,
'completion/complete': CompleteResultSchema,
'server/discover': DiscoverResultSchema
};
/** Wire-true result wrappers consulted by decode step 2, keyed by method —
* built once through the era's schema memo on the first decode. */
let wireResultSchemasMemo: Record<string, z.ZodType> | undefined;
function getWireResultSchemas(): Record<string, z.ZodType> {
if (wireResultSchemasMemo) return wireResultSchemasMemo;
const s = buildSchemas2026();
wireResultSchemasMemo = {
'tools/call': s.CallToolResultSchema,
'tools/list': s.ListToolsResultSchema,
'prompts/get': s.GetPromptResultSchema,
'prompts/list': s.ListPromptsResultSchema,
'resources/list': s.ListResourcesResultSchema,
'resources/templates/list': s.ListResourceTemplatesResultSchema,
'resources/read': s.ReadResourceResultSchema,
'completion/complete': s.CompleteResultSchema,
'server/discover': s.DiscoverResultSchema
};
return wireResultSchemasMemo;
}

View file

@ -21,41 +21,51 @@
import * as z from 'zod/v4';
import type { RequestMethod, RequestTypeMap, ResultTypeMap } from '../../types/types';
import {
CreateMessageRequestParamsSchema,
CreateMessageResultSchema,
ElicitRequestParamsSchema,
ElicitResultSchema,
ListRootsResultSchema
} from './schemas';
import { buildSchemas2026 } from './buildSchemas';
/** The embedded input-request methods of the 2026-07-28 revision. */
export const INPUT_REQUEST_METHODS_2026 = ['elicitation/create', 'sampling/createMessage', 'roots/list'] as const;
export type InputRequestMethod2026 = (typeof INPUT_REQUEST_METHODS_2026)[number];
/** Dispatch-time (lenient) embedded request schemas, keyed by method. */
const inputRequestSchemas2026: Record<InputRequestMethod2026, z.ZodType> = {
'elicitation/create': z.object({
method: z.literal('elicitation/create'),
params: ElicitRequestParamsSchema
}),
'sampling/createMessage': z.object({
method: z.literal('sampling/createMessage'),
params: CreateMessageRequestParamsSchema
}),
'roots/list': z.object({
method: z.literal('roots/list'),
params: z.looseObject({}).optional()
})
};
/* The embedded-request maps are built lazily through the era's memoized
* schema factory (`buildSchemas2026`) so importing this module constructs no
* zod schemas; the maps themselves are memoized alongside. */
interface InputSchemaMaps {
/** Dispatch-time (lenient) embedded request schemas, keyed by method. */
readonly request: Record<InputRequestMethod2026, z.ZodType>;
/** Embedded (bare) response schemas, keyed by the request method they answer. */
readonly response: Record<InputRequestMethod2026, z.ZodType>;
}
/** Embedded (bare) response schemas, keyed by the request method they answer. */
const inputResponseSchemas2026: Record<InputRequestMethod2026, z.ZodType> = {
'elicitation/create': ElicitResultSchema,
'sampling/createMessage': CreateMessageResultSchema,
'roots/list': ListRootsResultSchema
};
let maps: InputSchemaMaps | undefined;
function inputSchemaMaps(): InputSchemaMaps {
if (maps) return maps;
const s = buildSchemas2026();
maps = {
request: {
'elicitation/create': z.object({
method: z.literal('elicitation/create'),
params: s.ElicitRequestParamsSchema
}),
'sampling/createMessage': z.object({
method: z.literal('sampling/createMessage'),
params: s.CreateMessageRequestParamsSchema
}),
'roots/list': z.object({
method: z.literal('roots/list'),
params: z.looseObject({}).optional()
})
},
response: {
'elicitation/create': s.ElicitResultSchema,
'sampling/createMessage': s.CreateMessageResultSchema,
'roots/list': s.ListRootsResultSchema
}
};
return maps;
}
export function isInputRequestMethod2026(method: string): method is InputRequestMethod2026 {
return (INPUT_REQUEST_METHODS_2026 as readonly string[]).includes(method);
@ -69,7 +79,7 @@ export function isInputRequestMethod2026(method: string): method is InputRequest
export function getInputRequestSchema2026<M extends RequestMethod>(method: M): z.ZodType<RequestTypeMap[M]> | undefined;
export function getInputRequestSchema2026(method: string): z.ZodType | undefined;
export function getInputRequestSchema2026(method: string): z.ZodType | undefined {
return isInputRequestMethod2026(method) ? inputRequestSchemas2026[method] : undefined;
return isInputRequestMethod2026(method) ? inputSchemaMaps().request[method] : undefined;
}
/**
@ -79,5 +89,5 @@ export function getInputRequestSchema2026(method: string): z.ZodType | undefined
export function getInputResponseSchema2026<M extends RequestMethod>(method: M): z.ZodType<ResultTypeMap[M]> | undefined;
export function getInputResponseSchema2026(method: string): z.ZodType | undefined;
export function getInputResponseSchema2026(method: string): z.ZodType | undefined {
return isInputRequestMethod2026(method) ? inputResponseSchemas2026[method] : undefined;
return isInputRequestMethod2026(method) ? inputSchemaMaps().response[method] : undefined;
}

View file

@ -26,26 +26,59 @@
* and own ack/filter/stamp/teardown themselves; on the client side
* `Client.listen()` sends directly on the transport (string-typed
* request id, transport-level demux) rather than via `request()`.
*
* LAZY SCHEMA CONSTRUCTION: method membership and the exported method lists
* are static (null-valued key objects below, mapped over the hand-registry
* unions so drift is a compile error in both directions), while the dispatch
* schema maps are pulled through the era's memoized `buildSchemas2026()`
* factory on first lookup — importing this module constructs no zod schemas.
*/
import type * as z from 'zod/v4';
import type { NotificationMethod, NotificationTypeMap, RequestMethod, RequestTypeMap, ResultTypeMap } from '../../types/types';
import type { Rev2026NotificationMethod, Rev2026RequestMethod } from './schemas';
import { dispatchRequestSchemas, dispatchResultSchemas, notificationSchemas2026 } from './schemas';
import type { Rev2026NotificationMethod, Rev2026RequestMethod } from './buildSchemas';
import { buildSchemas2026 } from './buildSchemas';
/* Static method membership — the schema-free half of the registry. Key order
* matches the dispatch maps in `./buildSchemas.ts` so the exported method
* lists are byte-identical to the pre-lazy `Object.keys(map)` derivation. */
const requestMethodKeys: { readonly [M in Rev2026RequestMethod]: null } = {
'tools/call': null,
'tools/list': null,
'prompts/get': null,
'prompts/list': null,
'resources/list': null,
'resources/templates/list': null,
'resources/read': null,
'completion/complete': null,
'server/discover': null,
'subscriptions/listen': null
};
const notificationMethodKeys: { readonly [M in Rev2026NotificationMethod]: null } = {
'notifications/cancelled': null,
'notifications/progress': null,
'notifications/message': null,
'notifications/resources/updated': null,
'notifications/resources/list_changed': null,
'notifications/tools/list_changed': null,
'notifications/prompts/list_changed': null,
'notifications/subscriptions/acknowledged': null
};
/** The 2026-era request-method set (registry membership = the deletion story). */
export function hasRequestMethod2026(method: string): method is Rev2026RequestMethod {
return Object.prototype.hasOwnProperty.call(dispatchRequestSchemas, method);
return Object.prototype.hasOwnProperty.call(requestMethodKeys, method);
}
/** The 2026-era notification-method set. */
export function hasNotificationMethod2026(method: string): method is Rev2026NotificationMethod {
return Object.prototype.hasOwnProperty.call(notificationSchemas2026, method);
return Object.prototype.hasOwnProperty.call(notificationMethodKeys, method);
}
/** Result-map membership (same key set as the request map on this era). */
function hasResultMethod2026(method: string): method is Rev2026RequestMethod {
return Object.prototype.hasOwnProperty.call(dispatchResultSchemas, method);
return Object.prototype.hasOwnProperty.call(requestMethodKeys, method);
}
/**
@ -57,7 +90,7 @@ function hasResultMethod2026(method: string): method is Rev2026RequestMethod {
export function getRequestSchema2026<M extends RequestMethod>(method: M): z.ZodType<RequestTypeMap[M]> | undefined;
export function getRequestSchema2026(method: string): z.ZodType | undefined;
export function getRequestSchema2026(method: string): z.ZodType | undefined {
return hasRequestMethod2026(method) ? dispatchRequestSchemas[method] : undefined;
return hasRequestMethod2026(method) ? buildSchemas2026().dispatchRequestSchemas[method] : undefined;
}
/**
@ -69,7 +102,7 @@ export function getRequestSchema2026(method: string): z.ZodType | undefined {
export function getResultSchema2026<M extends RequestMethod>(method: M): z.ZodType<ResultTypeMap[M]> | undefined;
export function getResultSchema2026(method: string): z.ZodType | undefined;
export function getResultSchema2026(method: string): z.ZodType | undefined {
return hasResultMethod2026(method) ? dispatchResultSchemas[method] : undefined;
return hasResultMethod2026(method) ? buildSchemas2026().dispatchResultSchemas[method] : undefined;
}
/**
@ -80,9 +113,9 @@ export function getResultSchema2026(method: string): z.ZodType | undefined {
export function getNotificationSchema2026<M extends NotificationMethod>(method: M): z.ZodType<NotificationTypeMap[M]> | undefined;
export function getNotificationSchema2026(method: string): z.ZodType | undefined;
export function getNotificationSchema2026(method: string): z.ZodType | undefined {
return hasNotificationMethod2026(method) ? notificationSchemas2026[method] : undefined;
return hasNotificationMethod2026(method) ? buildSchemas2026().notificationSchemas2026[method] : undefined;
}
/** Registry method lists (for the spec-method universe and the CI registry-diff oracle). */
export const rev2026RequestMethods: readonly string[] = Object.keys(dispatchRequestSchemas);
export const rev2026NotificationMethods: readonly string[] = Object.keys(notificationSchemas2026);
export const rev2026RequestMethods: readonly string[] = Object.keys(requestMethodKeys);
export const rev2026NotificationMethods: readonly string[] = Object.keys(notificationMethodKeys);

File diff suppressed because it is too large Load diff

View file

@ -22,15 +22,21 @@ import { describe, expect, it } from 'vitest';
// Post-relocation home (Q1 increment-2 step 1): the pinned contents are
// unchanged — only the module housing the registries moved.
import { getNotificationSchema, getRequestSchema, CallToolResultWireSchema, getResultSchema } from '../../src/wire/rev2025-11-25/registry';
import { getNotificationSchema, getRequestSchema, getResultSchema } from '../../src/wire/rev2025-11-25/registry';
// The 2025 wire schemas are fully self-contained in the era's schema module:
// every per-method schema the registry serves is a FROZEN 2025-11-25 copy so
// the public/neutral layer can evolve (e.g. SEP-2106 widening) without
// changing the 2025 wire-parse contract. The registry serves the FROZEN
// copies, so the by-reference pins target this module.
// copies, so the by-reference pins target this module. Since the lazy-
// construction change, importing this module also WARMS the era's schema
// memo (`buildSchemas2025`) at module scope — the registry pulls its maps
// through the same memo, so the by-reference pins hold under laziness. The
// wire-seam wrapper `CallToolResultWireSchema` moved here from the registry
// with that change (same object either way, via the shared memo).
import {
CallToolRequestSchema,
CallToolResultSchema,
CallToolResultWireSchema,
CancelledNotificationSchema,
CancelTaskRequestSchema,
CompleteRequestSchema,