perf: build era wire schemas lazily through memoized factories (#2476)
This commit is contained in:
parent
95d28cbae9
commit
e0a0ab74d9
11 changed files with 4467 additions and 3538 deletions
7
.changeset/lazy-era-wire-schemas.md
Normal file
7
.changeset/lazy-era-wire-schemas.md
Normal 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.
|
||||
2432
packages/core-internal/src/wire/rev2025-11-25/buildSchemas.ts
Normal file
2432
packages/core-internal/src/wire/rev2025-11-25/buildSchemas.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
1399
packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts
Normal file
1399
packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue