Skip to content

Commit 4fbcfcd

Browse files
refactor(specTypeSchema): drop as unknown as casts; add allowlist drift guard (modelcontextprotocol#1993)
1 parent 96db044 commit 4fbcfcd

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

packages/core/src/types/specTypeSchema.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,9 @@ type SpecTypeInputs = {
238238
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
239239
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };
240240

241-
const _specTypeSchemas: Record<string, z.ZodTypeAny> = {};
241+
const _specTypeSchemas: Record<string, StandardSchemaV1> = {};
242242
const _isSpecType: Record<string, (value: unknown) => boolean> = {};
243-
function register(key: string, schema: z.ZodTypeAny): void {
243+
function register(key: string, schema: z.ZodType): void {
244244
const name = key.slice(0, -'Schema'.length);
245245
_specTypeSchemas[name] = schema;
246246
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
@@ -271,7 +271,7 @@ for (const [key, schema] of Object.entries(authSchemas)) {
271271
* }
272272
* ```
273273
*/
274-
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as unknown as SchemaRecord;
274+
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas as SchemaRecord);
275275

276276
/**
277277
* Type predicates for every MCP spec type, keyed by type name.
@@ -293,4 +293,4 @@ export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as
293293
* const blocks = mixed.filter(isSpecType.ContentBlock);
294294
* ```
295295
*/
296-
export const isSpecType: GuardRecord = Object.freeze(_isSpecType) as unknown as GuardRecord;
296+
export const isSpecType: GuardRecord = Object.freeze(_isSpecType as GuardRecord);

packages/core/test/types/specTypeSchema.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, expectTypeOf, it } from 'vitest';
22

33
import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth.js';
4+
import * as schemas from '../../src/types/schemas.js';
45
import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema.js';
56
import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema.js';
67
import type {
@@ -146,3 +147,30 @@ describe('SpecTypeName / SpecTypes (type-level)', () => {
146147
expectTypeOf<SpecTypes['ResourceTemplate']>().toEqualTypeOf<ResourceTemplateType>();
147148
});
148149
});
150+
151+
describe('SPEC_SCHEMA_KEYS allowlist', () => {
152+
// Mirrors the exclusion comment in specTypeSchema.ts. If this list grows, confirm the new
153+
// entry has no public type in types.ts before adding it here; otherwise add it to the allowlist.
154+
const INTERNAL_HELPER_SCHEMAS: readonly string[] = [
155+
'ListChangedOptionsBaseSchema',
156+
'BaseRequestParamsSchema',
157+
'NotificationsParamsSchema',
158+
'ClientTasksCapabilitySchema',
159+
'ServerTasksCapabilitySchema'
160+
];
161+
162+
it('covers every public protocol schema in schemas.ts (drift guard)', () => {
163+
// PascalCase filters out helper functions like getRequestSchema/getResultSchema.
164+
const allProtocolSchemas = Object.keys(schemas).filter(k => k.endsWith('Schema') && /^[A-Z]/.test(k));
165+
const expected = allProtocolSchemas
166+
.filter(k => !INTERNAL_HELPER_SCHEMAS.includes(k))
167+
.map(k => k.slice(0, -'Schema'.length))
168+
.sort();
169+
// Auth schemas are sourced from shared/auth.ts, not schemas.ts, so filter them out of the
170+
// observed side before comparing.
171+
const actual = Object.keys(isSpecType)
172+
.filter(k => !k.startsWith('OAuth') && !k.startsWith('OpenId'))
173+
.sort();
174+
expect(actual).toEqual(expected);
175+
});
176+
});

0 commit comments

Comments
 (0)