refactor(specTypeSchema): drop as unknown as casts; add allowlist drift guard (#1993)

This commit is contained in:
Felix Weinberger 2026-04-30 14:03:09 +01:00 • committed by GitHub
commit 4fbcfcd176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 4 deletions

View file

@ -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);

View file

@ -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);
});
});