From efb53081553ab22c816003ec3fc8c557f146ca49 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 30 Apr 2026 12:20:30 +0000 Subject: [PATCH 1/2] refactor(specTypeSchema): drop `as unknown as` from SchemaRecord/GuardRecord exports Retyping the staging record from `Record` to `Record` gives TS overlap with `SchemaRecord` (every `SchemaRecord[K]` is a `StandardSchemaV1`, so the cast direction is a permitted narrowing). The guard record already overlapped because `(v) => v is T` is a subtype of `(v) => boolean`. Both exports are now single `as` casts inside `Object.freeze()` instead of double-casts. --- packages/core/src/types/specTypeSchema.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/types/specTypeSchema.ts b/packages/core/src/types/specTypeSchema.ts index 21a45c77d8..cde3555d07 100644 --- a/packages/core/src/types/specTypeSchema.ts +++ b/packages/core/src/types/specTypeSchema.ts @@ -238,9 +238,9 @@ type SpecTypeInputs = { type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1 }; type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] }; -const _specTypeSchemas: Record = {}; +const _specTypeSchemas: Record = {}; const _isSpecType: Record boolean> = {}; -function register(key: string, schema: z.ZodTypeAny): void { +function register(key: string, schema: z.ZodType): void { const name = key.slice(0, -'Schema'.length); _specTypeSchemas[name] = schema; _isSpecType[name] = (v: unknown) => schema.safeParse(v).success; @@ -271,7 +271,7 @@ for (const [key, schema] of Object.entries(authSchemas)) { * } * ``` */ -export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as unknown as SchemaRecord; +export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas as SchemaRecord); /** * Type predicates for every MCP spec type, keyed by type name. @@ -293,4 +293,4 @@ export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as * const blocks = mixed.filter(isSpecType.ContentBlock); * ``` */ -export const isSpecType: GuardRecord = Object.freeze(_isSpecType) as unknown as GuardRecord; +export const isSpecType: GuardRecord = Object.freeze(_isSpecType as GuardRecord); From 65ab998792e5b58618aaef8f64e0a6a6a422987c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 30 Apr 2026 12:20:31 +0000 Subject: [PATCH 2/2] test(specTypeSchema): drift guard for SPEC_SCHEMA_KEYS vs schemas.ts exports Compares `Object.keys(isSpecType)` (protocol entries only) against the PascalCase `*Schema` exports of schemas.ts minus the documented internal helpers. A new schema added to schemas.ts now fails this test until it is either added to SPEC_SCHEMA_KEYS or to INTERNAL_HELPER_SCHEMAS. --- .../core/test/types/specTypeSchema.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/core/test/types/specTypeSchema.test.ts b/packages/core/test/types/specTypeSchema.test.ts index 9c9d0f1863..be8c419225 100644 --- a/packages/core/test/types/specTypeSchema.test.ts +++ b/packages/core/test/types/specTypeSchema.test.ts @@ -1,6 +1,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth.js'; +import * as schemas from '../../src/types/schemas.js'; import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema.js'; import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema.js'; import type { @@ -146,3 +147,30 @@ describe('SpecTypeName / SpecTypes (type-level)', () => { expectTypeOf().toEqualTypeOf(); }); }); + +describe('SPEC_SCHEMA_KEYS allowlist', () => { + // Mirrors the exclusion comment in specTypeSchema.ts. If this list grows, confirm the new + // entry has no public type in types.ts before adding it here; otherwise add it to the allowlist. + const INTERNAL_HELPER_SCHEMAS: readonly string[] = [ + 'ListChangedOptionsBaseSchema', + 'BaseRequestParamsSchema', + 'NotificationsParamsSchema', + 'ClientTasksCapabilitySchema', + 'ServerTasksCapabilitySchema' + ]; + + it('covers every public protocol schema in schemas.ts (drift guard)', () => { + // PascalCase filters out helper functions like getRequestSchema/getResultSchema. + const allProtocolSchemas = Object.keys(schemas).filter(k => k.endsWith('Schema') && /^[A-Z]/.test(k)); + const expected = allProtocolSchemas + .filter(k => !INTERNAL_HELPER_SCHEMAS.includes(k)) + .map(k => k.slice(0, -'Schema'.length)) + .sort(); + // Auth schemas are sourced from shared/auth.ts, not schemas.ts, so filter them out of the + // observed side before comparing. + const actual = Object.keys(isSpecType) + .filter(k => !k.startsWith('OAuth') && !k.startsWith('OpenId')) + .sort(); + expect(actual).toEqual(expected); + }); +});