feat(core): add isSpecType / specTypeSchemas Records for runtime validation of spec types (#1887)

This commit is contained in:
Felix Weinberger 2026-04-30 13:16:23 +01:00 • committed by GitHub
commit 96db044fe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 590 additions and 76 deletions

View file

@ -138,6 +138,8 @@ export { isTerminal } from '../../experimental/tasks/interfaces.js';
export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/tasks/stores/inMemory.js';
// Validator types and classes
export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js';
export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js';
export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js';
export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js';
export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js';

View file

@ -5,4 +5,5 @@ export * from './enums.js';
export * from './errors.js';
export * from './guards.js';
export * from './schemas.js';
export * from './specTypeSchema.js';
export * from './types.js';

View file

@ -12,11 +12,11 @@ import type {
ResultTypeMap
} from './types.js';
export const JSONValueSchema: z.ZodType<JSONValue> = z.lazy(() =>
export const JSONValueSchema: z.ZodType<JSONValue, JSONValue> = z.lazy(() =>
z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), JSONValueSchema), z.array(JSONValueSchema)])
);
export const JSONObjectSchema: z.ZodType<JSONObject> = z.record(z.string(), JSONValueSchema);
export const JSONArraySchema: z.ZodType<JSONArray> = z.array(JSONValueSchema);
export const JSONObjectSchema: z.ZodType<JSONObject, JSONObject> = z.record(z.string(), JSONValueSchema);
export const JSONArraySchema: z.ZodType<JSONArray, JSONArray> = z.array(JSONValueSchema);
/**
* A progress token, used to associate progress notifications with the original request.
*/

View file

@ -0,0 +1,40 @@
/**
* Type-checked examples for `specTypeSchema.ts`.
*
* These examples are synced into JSDoc comments via the sync-snippets script.
* Each function's region markers define the code snippet that appears in the docs.
*
* @module
*/
import { isSpecType, specTypeSchemas } from './specTypeSchema.js';
declare const untrusted: unknown;
declare const value: unknown;
declare const mixed: unknown[];
async function specTypeSchemas_basicUsage() {
//#region specTypeSchemas_basicUsage
const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
if (result.issues === undefined) {
// result.value is CallToolResult
}
//#endregion specTypeSchemas_basicUsage
void result;
}
function isSpecType_basicUsage() {
/* eslint-disable unicorn/no-array-callback-reference -- showcasing the guard-as-callback pattern */
//#region isSpecType_basicUsage
if (isSpecType.ContentBlock(value)) {
// value is ContentBlock
}
const blocks = mixed.filter(isSpecType.ContentBlock);
//#endregion isSpecType_basicUsage
/* eslint-enable unicorn/no-array-callback-reference */
void blocks;
}
void specTypeSchemas_basicUsage;
void isSpecType_basicUsage;

View file

@ -0,0 +1,296 @@
import type * as z from 'zod/v4';
import {
OAuthClientInformationFullSchema,
OAuthClientInformationSchema,
OAuthClientMetadataSchema,
OAuthClientRegistrationErrorSchema,
OAuthErrorResponseSchema,
OAuthMetadataSchema,
OAuthProtectedResourceMetadataSchema,
OAuthTokenRevocationRequestSchema,
OAuthTokensSchema,
OpenIdProviderDiscoveryMetadataSchema,
OpenIdProviderMetadataSchema
} from '../shared/auth.js';
import type { StandardSchemaV1 } from '../util/standardSchema.js';
import * as schemas from './schemas.js';
/**
* Explicit allowlist of protocol Zod schemas that correspond to a public spec type in `types.ts`.
*
* This intentionally excludes internal helper schemas exported from `schemas.ts` that have no
* matching public type (e.g. `ListChangedOptionsBaseSchema`, `BaseRequestParamsSchema`,
* `NotificationsParamsSchema`, `ClientTasksCapabilitySchema`, `ServerTasksCapabilitySchema`).
* Keeping the list explicit means new public spec types must be added here deliberately, and
* internals never leak into `SpecTypeName`.
*
* `ResourceTemplateSchema` is included; its public type is exported as `ResourceTemplateType`
* (the bare name collides with the server package's `ResourceTemplate` class), so
* `SpecTypes['ResourceTemplate']` is structurally equal to `ResourceTemplateType` rather than to
* a type literally named `ResourceTemplate`.
*/
const SPEC_SCHEMA_KEYS = [
'AnnotationsSchema',
'AudioContentSchema',
'BaseMetadataSchema',
'BlobResourceContentsSchema',
'BooleanSchemaSchema',
'CallToolRequestSchema',
'CallToolRequestParamsSchema',
'CallToolResultSchema',
'CancelledNotificationSchema',
'CancelledNotificationParamsSchema',
'CancelTaskRequestSchema',
'CancelTaskResultSchema',
'ClientCapabilitiesSchema',
'ClientNotificationSchema',
'ClientRequestSchema',
'ClientResultSchema',
'CompatibilityCallToolResultSchema',
'CompleteRequestSchema',
'CompleteRequestParamsSchema',
'CompleteResultSchema',
'ContentBlockSchema',
'CreateMessageRequestSchema',
'CreateMessageRequestParamsSchema',
'CreateMessageResultSchema',
'CreateMessageResultWithToolsSchema',
'CreateTaskResultSchema',
'CursorSchema',
'ElicitationCompleteNotificationSchema',
'ElicitationCompleteNotificationParamsSchema',
'ElicitRequestSchema',
'ElicitRequestFormParamsSchema',
'ElicitRequestParamsSchema',
'ElicitRequestURLParamsSchema',
'ElicitResultSchema',
'EmbeddedResourceSchema',
'EmptyResultSchema',
'EnumSchemaSchema',
'GetPromptRequestSchema',
'GetPromptRequestParamsSchema',
'GetPromptResultSchema',
'GetTaskPayloadRequestSchema',
'GetTaskPayloadResultSchema',
'GetTaskRequestSchema',
'GetTaskResultSchema',
'IconSchema',
'IconsSchema',
'ImageContentSchema',
'ImplementationSchema',
'InitializedNotificationSchema',
'InitializeRequestSchema',
'InitializeRequestParamsSchema',
'InitializeResultSchema',
'JSONArraySchema',
'JSONObjectSchema',
'JSONRPCErrorResponseSchema',
'JSONRPCMessageSchema',
'JSONRPCNotificationSchema',
'JSONRPCRequestSchema',
'JSONRPCResponseSchema',
'JSONRPCResultResponseSchema',
'JSONValueSchema',
'LegacyTitledEnumSchemaSchema',
'ListPromptsRequestSchema',
'ListPromptsResultSchema',
'ListResourcesRequestSchema',
'ListResourcesResultSchema',
'ListResourceTemplatesRequestSchema',
'ListResourceTemplatesResultSchema',
'ListRootsRequestSchema',
'ListRootsResultSchema',
'ListTasksRequestSchema',
'ListTasksResultSchema',
'ListToolsRequestSchema',
'ListToolsResultSchema',
'LoggingLevelSchema',
'LoggingMessageNotificationSchema',
'LoggingMessageNotificationParamsSchema',
'ModelHintSchema',
'ModelPreferencesSchema',
'MultiSelectEnumSchemaSchema',
'NotificationSchema',
'NumberSchemaSchema',
'PaginatedRequestSchema',
'PaginatedRequestParamsSchema',
'PaginatedResultSchema',
'PingRequestSchema',
'PrimitiveSchemaDefinitionSchema',
'ProgressSchema',
'ProgressNotificationSchema',
'ProgressNotificationParamsSchema',
'ProgressTokenSchema',
'PromptSchema',
'PromptArgumentSchema',
'PromptListChangedNotificationSchema',
'PromptMessageSchema',
'PromptReferenceSchema',
'ReadResourceRequestSchema',
'ReadResourceRequestParamsSchema',
'ReadResourceResultSchema',
'RelatedTaskMetadataSchema',
'RequestSchema',
'RequestIdSchema',
'RequestMetaSchema',
'ResourceSchema',
'ResourceContentsSchema',
'ResourceLinkSchema',
'ResourceListChangedNotificationSchema',
'ResourceRequestParamsSchema',
'ResourceTemplateSchema',
'ResourceTemplateReferenceSchema',
'ResourceUpdatedNotificationSchema',
'ResourceUpdatedNotificationParamsSchema',
'ResultSchema',
'RoleSchema',
'RootSchema',
'RootsListChangedNotificationSchema',
'SamplingContentSchema',
'SamplingMessageSchema',
'SamplingMessageContentBlockSchema',
'ServerCapabilitiesSchema',
'ServerNotificationSchema',
'ServerRequestSchema',
'ServerResultSchema',
'SetLevelRequestSchema',
'SetLevelRequestParamsSchema',
'SingleSelectEnumSchemaSchema',
'StringSchemaSchema',
'SubscribeRequestSchema',
'SubscribeRequestParamsSchema',
'TaskSchema',
'TaskAugmentedRequestParamsSchema',
'TaskCreationParamsSchema',
'TaskMetadataSchema',
'TaskStatusSchema',
'TaskStatusNotificationSchema',
'TaskStatusNotificationParamsSchema',
'TextContentSchema',
'TextResourceContentsSchema',
'TitledMultiSelectEnumSchemaSchema',
'TitledSingleSelectEnumSchemaSchema',
'ToolSchema',
'ToolAnnotationsSchema',
'ToolChoiceSchema',
'ToolExecutionSchema',
'ToolListChangedNotificationSchema',
'ToolResultContentSchema',
'ToolUseContentSchema',
'UnsubscribeRequestSchema',
'UnsubscribeRequestParamsSchema',
'UntitledMultiSelectEnumSchemaSchema',
'UntitledSingleSelectEnumSchemaSchema'
] as const satisfies readonly (keyof typeof schemas)[];
const authSchemas = {
OAuthClientInformationFullSchema,
OAuthClientInformationSchema,
OAuthClientMetadataSchema,
OAuthClientRegistrationErrorSchema,
OAuthErrorResponseSchema,
OAuthMetadataSchema,
OAuthProtectedResourceMetadataSchema,
OAuthTokenRevocationRequestSchema,
OAuthTokensSchema,
OpenIdProviderDiscoveryMetadataSchema,
OpenIdProviderMetadataSchema
} as const;
type ProtocolSchemaKey = (typeof SPEC_SCHEMA_KEYS)[number];
type AuthSchemaKey = keyof typeof authSchemas;
type SchemaKey = ProtocolSchemaKey | AuthSchemaKey;
type SchemaFor<K extends SchemaKey> = K extends ProtocolSchemaKey
? (typeof schemas)[K]
: K extends AuthSchemaKey
? (typeof authSchemas)[K]
: never;
type StripSchemaSuffix<K> = K extends `${infer N}Schema` ? N : never;
/**
* Union of every named type in the SDK's protocol and OAuth schemas (e.g. `'CallToolResult'`,
* `'ContentBlock'`, `'Tool'`, `'OAuthTokens'`). Derived from the internal Zod schemas, so it stays
* in sync with the spec.
*/
export type SpecTypeName = StripSchemaSuffix<SchemaKey>;
/**
* Maps each {@linkcode SpecTypeName} to its TypeScript type.
*
* `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly.
*/
export type SpecTypes = {
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.output<SchemaFor<K>> : never;
};
/**
* Input shape for each {@linkcode SpecTypeName}. For most types this equals {@linkcode SpecTypes},
* but a few schemas apply defaults/preprocessing, so the accepted input may be looser than the
* resulting output type.
*/
type SpecTypeInputs = {
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.input<SchemaFor<K>> : never;
};
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 _isSpecType: Record<string, (value: unknown) => boolean> = {};
function register(key: string, schema: z.ZodTypeAny): void {
const name = key.slice(0, -'Schema'.length);
_specTypeSchemas[name] = schema;
_isSpecType[name] = (v: unknown) => schema.safeParse(v).success;
}
for (const key of SPEC_SCHEMA_KEYS) {
// eslint-disable-next-line import/namespace -- key is constrained to keyof typeof schemas via the satisfies clause above
register(key, schemas[key]);
}
for (const [key, schema] of Object.entries(authSchemas)) {
register(key, schema);
}
/**
* Runtime validators for every MCP spec type, keyed by type name.
*
* Use this when you need to validate a spec-defined shape at a boundary the SDK does not own, for
* example an extension's custom-method payload that embeds a `CallToolResult`, or a value read from
* storage that should be a `Tool`.
*
* Each entry implements the Standard Schema interface, so it composes with any
* Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead.
*
* @example
* ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage"
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
* if (result.issues === undefined) {
* // result.value is CallToolResult
* }
* ```
*/
export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas) as unknown as SchemaRecord;
/**
* Type predicates for every MCP spec type, keyed by type name.
*
* Returns `true` if the value satisfies the schema's input type (`z.input<>`, before defaults and
* transforms are applied), and narrows to that input type. For schemas with `.default()` or
* `.preprocess()`, this may accept values that do not structurally match the named output type;
* for example `isSpecType.CallToolResult({})` is `true` because `content` has a default. Use
* `specTypeSchemas.X['~standard'].validate(value)` when you need the validated output value.
*
* Each guard is a standalone function, so it can be passed directly as a callback.
*
* @example
* ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage"
* if (isSpecType.ContentBlock(value)) {
* // value is ContentBlock
* }
*
* const blocks = mixed.filter(isSpecType.ContentBlock);
* ```
*/
export const isSpecType: GuardRecord = Object.freeze(_isSpecType) as unknown as GuardRecord;

View file

@ -0,0 +1,123 @@
import { describe, expect, it } from 'vitest';
import { JSONRPC_VERSION } from '../../src/types/constants.js';
import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from '../../src/types/guards.js';
describe('isJSONRPCResponse', () => {
it('returns true for a valid result response', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
id: 1,
result: {}
})
).toBe(true);
});
it('returns true for a valid error response', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
id: 1,
error: { code: -32_600, message: 'Invalid Request' }
})
).toBe(true);
});
it('returns false for a request', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
id: 1,
method: 'test'
})
).toBe(false);
});
it('returns false for a notification', () => {
expect(
isJSONRPCResponse({
jsonrpc: JSONRPC_VERSION,
method: 'test'
})
).toBe(false);
});
it('returns false for arbitrary objects', () => {
expect(isJSONRPCResponse({ foo: 'bar' })).toBe(false);
});
it('narrows the type correctly', () => {
const value: unknown = {
jsonrpc: JSONRPC_VERSION,
id: 1,
result: { content: [] }
};
if (isJSONRPCResponse(value)) {
// Type should be narrowed to JSONRPCResponse
expect(value.jsonrpc).toBe(JSONRPC_VERSION);
expect(value.id).toBe(1);
}
});
it('agrees with isJSONRPCResultResponse || isJSONRPCErrorResponse', () => {
const values = [
{ jsonrpc: JSONRPC_VERSION, id: 1, result: {} },
{ jsonrpc: JSONRPC_VERSION, id: 2, error: { code: -1, message: 'err' } },
{ jsonrpc: JSONRPC_VERSION, id: 3, method: 'test' },
{ jsonrpc: JSONRPC_VERSION, method: 'notify' },
{ foo: 'bar' },
null,
42
];
for (const v of values) {
expect(isJSONRPCResponse(v)).toBe(isJSONRPCResultResponse(v) || isJSONRPCErrorResponse(v));
}
});
});
describe('isCallToolResult', () => {
it('returns false for an empty object (content is required)', () => {
expect(isCallToolResult({})).toBe(false);
});
it('returns true for a result with content', () => {
expect(
isCallToolResult({
content: [{ type: 'text', text: 'hello' }]
})
).toBe(true);
});
it('returns true for a result with isError', () => {
expect(
isCallToolResult({
content: [{ type: 'text', text: 'fail' }],
isError: true
})
).toBe(true);
});
it('returns true for a result with structuredContent', () => {
expect(
isCallToolResult({
content: [],
structuredContent: { key: 'value' }
})
).toBe(true);
});
it('returns false for non-objects', () => {
expect(isCallToolResult(null)).toBe(false);
expect(isCallToolResult(42)).toBe(false);
expect(isCallToolResult('string')).toBe(false);
});
it('returns false for invalid content items', () => {
expect(
isCallToolResult({
content: [{ type: 'invalid' }]
})
).toBe(false);
});
});

View file

@ -0,0 +1,148 @@
import { describe, expect, expectTypeOf, it } from 'vitest';
import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth.js';
import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema.js';
import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema.js';
import type {
CallToolResult,
ContentBlock,
Implementation,
JSONObject,
JSONRPCRequest,
JSONValue,
ResourceTemplateType,
Tool
} from '../../src/types/types.js';
describe('specTypeSchemas', () => {
it('returns a StandardSchemaV1 validator that accepts valid values', () => {
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' });
expect((result as { issues?: unknown }).issues).toBeUndefined();
});
it('returns a validator that rejects invalid values with issues', () => {
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' });
expect((result as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
});
it('rejects unknown names at compile time and is undefined at runtime', () => {
// @ts-expect-error - 'NotASpecType' is not a SpecTypeName
expect(specTypeSchemas['NotASpecType']).toBeUndefined();
});
it('covers JSON-RPC envelope types', () => {
const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' });
expect((ok as { issues?: unknown }).issues).toBeUndefined();
});
it('covers OAuth types from shared/auth.ts', () => {
const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' });
expect((ok as { issues?: unknown }).issues).toBeUndefined();
const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' });
expect((bad as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
});
});
describe('isSpecType', () => {
it('CallToolResult — accepts valid, rejects invalid/null/primitive', () => {
expect(isSpecType.CallToolResult({ content: [{ type: 'text', text: 'hi' }] })).toBe(true);
expect(isSpecType.CallToolResult({ content: 'not-an-array' })).toBe(false);
expect(isSpecType.CallToolResult(null)).toBe(false);
expect(isSpecType.CallToolResult('string')).toBe(false);
});
it('ContentBlock — accepts text block, rejects wrong shape', () => {
expect(isSpecType.ContentBlock({ type: 'text', text: 'hi' })).toBe(true);
expect(isSpecType.ContentBlock({ type: 'text' })).toBe(false);
expect(isSpecType.ContentBlock({})).toBe(false);
});
it('Tool — accepts valid, rejects missing inputSchema', () => {
expect(isSpecType.Tool({ name: 'echo', inputSchema: { type: 'object' } })).toBe(true);
expect(isSpecType.Tool({ name: 'echo' })).toBe(false);
});
it('ResourceTemplate — accepts valid, rejects missing uriTemplate', () => {
expect(isSpecType.ResourceTemplate({ name: 'r', uriTemplate: 'file:///{path}' })).toBe(true);
expect(isSpecType.ResourceTemplate({ name: 'r' })).toBe(false);
});
it('rejects unknown names at compile time and is undefined at runtime', () => {
// @ts-expect-error - 'NotASpecType' is not a SpecTypeName
expect(isSpecType['NotASpecType']).toBeUndefined();
});
it('excludes internal helper schemas (no matching public type)', () => {
// @ts-expect-error - ListChangedOptionsBase is internal-only
expect(isSpecType['ListChangedOptionsBase']).toBeUndefined();
// @ts-expect-error - BaseRequestParams is internal-only
expect(specTypeSchemas['BaseRequestParams']).toBeUndefined();
// @ts-expect-error - NotificationsParams is internal-only
expect(isSpecType['NotificationsParams']).toBeUndefined();
});
it('narrows the value type to the schema input type', () => {
const v: unknown = { name: 'x', version: '1.0.0' };
if (isSpecType.Implementation(v)) {
// ImplementationSchema has no defaults/transforms, so its input type equals Implementation.
expectTypeOf(v).toEqualTypeOf<Implementation>();
}
});
it('narrows to the input type, not the output type, for schemas with defaults', () => {
const v: unknown = {};
expect(isSpecType.CallToolResult(v)).toBe(true);
if (isSpecType.CallToolResult(v)) {
// CallToolResultSchema has `content: z.array(...).default([])`, so the input type
// permits `content` to be absent. The guard narrows to that input shape.
expectTypeOf(v.content).toEqualTypeOf<ContentBlock[] | undefined>();
expectTypeOf(v).not.toEqualTypeOf<CallToolResult>();
}
});
it('JSONValue / JSONObject — narrows to the JSON type, not unknown', () => {
// These schemas use an explicit z.ZodType<T, T> annotation for recursion; without the
// second param Zod's Input defaults to `unknown` and the predicate would not narrow.
const v: unknown = { a: 1 };
if (isSpecType.JSONValue(v)) {
expectTypeOf(v).toEqualTypeOf<JSONValue>();
}
if (isSpecType.JSONObject(v)) {
expectTypeOf(v).toEqualTypeOf<JSONObject>();
}
});
it('guards work as filter callbacks and narrow the element type', () => {
const mixed: unknown[] = [{ type: 'text', text: 'hi' }, 42, { type: 'text' }];
const blocks = mixed.filter(isSpecType.ContentBlock);
expect(blocks).toHaveLength(1);
expectTypeOf(blocks).toEqualTypeOf<ContentBlock[]>();
});
});
describe('SpecTypeName / SpecTypes (type-level)', () => {
it('SpecTypeName includes representative names', () => {
expectTypeOf<'CallToolResult'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'ContentBlock'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'Tool'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'Implementation'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'JSONRPCRequest'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'OAuthTokens'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'OAuthMetadata'>().toMatchTypeOf<SpecTypeName>();
expectTypeOf<'ResourceTemplate'>().toMatchTypeOf<SpecTypeName>();
});
it('SpecTypes[K] matches the named export type', () => {
expectTypeOf<SpecTypes['CallToolResult']>().toEqualTypeOf<CallToolResult>();
expectTypeOf<SpecTypes['ContentBlock']>().toEqualTypeOf<ContentBlock>();
expectTypeOf<SpecTypes['Tool']>().toEqualTypeOf<Tool>();
expectTypeOf<SpecTypes['Implementation']>().toEqualTypeOf<Implementation>();
expectTypeOf<SpecTypes['JSONRPCRequest']>().toEqualTypeOf<JSONRPCRequest>();
expectTypeOf<SpecTypes['OAuthTokens']>().toEqualTypeOf<OAuthTokens>();
expectTypeOf<SpecTypes['OAuthMetadata']>().toEqualTypeOf<OAuthMetadata>();
// The public type is exported as ResourceTemplateType (the bare name collides with the
// server package's ResourceTemplate class), so this is the one entry where the key and
// the public type name differ.
expectTypeOf<SpecTypes['ResourceTemplate']>().toEqualTypeOf<ResourceTemplateType>();
});
});