From efb53081553ab22c816003ec3fc8c557f146ca49 Mon Sep 17 00:00:00 2001
From: Felix Weinberger <fweinberger@anthropic.com>
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<string, z.ZodTypeAny>` to
`Record<string, StandardSchemaV1>` 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<SpecTypeInputs[K], SpecTypes[K]> };
 type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
 
-const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
+const _specTypeSchemas: Record<string, StandardSchemaV1> = {};
 const _isSpecType: Record<string, (value: unknown) => 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 <fweinberger@anthropic.com>
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<SpecTypes['ResourceTemplate']>().toEqualTypeOf<ResourceTemplateType>();
     });
 });
+
+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);
+    });
+});
