diff --git a/.changeset/spec-type-schema.md b/.changeset/spec-type-schema.md new file mode 100644 index 000000000..1e216d474 --- /dev/null +++ b/.changeset/spec-type-schema.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/client': minor +'@modelcontextprotocol/server': minor +--- + +Export `isSpecType` and `specTypeSchema` for runtime validation of any MCP spec type by name. `isSpecType('ContentBlock', value)` is a type predicate; `specTypeSchema('ContentBlock')` returns a `StandardSchemaV1` validator. Also export the `StandardSchemaV1`, +`SpecTypeName`, and `SpecTypes` types. diff --git a/CLAUDE.md b/CLAUDE.md index 28ccbb55e..cbbf95027 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Include what changed, why, and how to migrate. Search for related sections and g - **Files**: Lowercase with hyphens, test files with `.test.ts` suffix - **Imports**: ES module style, include `.js` extension, group imports logically - **Formatting**: 2-space indentation, semicolons required, single quotes preferred -- **Testing**: Co-locate tests with source files, use descriptive test names +- **Testing**: Place tests under each package's `test/` directory (vitest only includes `test/**/*.test.ts`), use descriptive test names - **Comments**: JSDoc for public APIs, inline comments for complex logic ### JSDoc `@example` Code Snippets diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 7f27356ee..4f5bb42f3 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -36,13 +36,13 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. ### Client imports -| v1 import path | v2 package | -| ---------------------------------------------------- | ------------------------------ | -| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/streamableHttp.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` | -| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client/stdio` | +| v1 import path | v2 package | +| ---------------------------------------------------- | ------------------------------------------------------------------------------ | +| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/streamableHttp.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client/stdio` | | `@modelcontextprotocol/sdk/client/websocket.js` | REMOVED (use Streamable HTTP or stdio; implement `Transport` for custom needs) | ### Server imports @@ -51,7 +51,7 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` | +| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` | | `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) OR `@modelcontextprotocol/server` (web-standard `WebStandardStreamableHTTPServerTransport` for Cloudflare Workers, Deno, etc.) | | `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) | | `@modelcontextprotocol/sdk/server/auth/*` | RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers removed (use external IdP/OAuth library) | @@ -59,13 +59,13 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. ### Types / shared imports -| v1 import path | v2 package | -| ------------------------------------------------- | ---------------------------- | -| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| v1 import path | v2 package | +| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) | Notes: @@ -81,24 +81,25 @@ Notes: ## 5. Removed / Renamed Type Aliases and Symbols -| v1 (removed) | v2 (replacement) | -| ---------------------------------------- | -------------------------------------------------------- | -| `JSONRPCError` | `JSONRPCErrorResponse` | -| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | -| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| v1 (removed) | v2 (replacement) | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | | `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | -| `ResourceReference` | `ResourceTemplateReference` | -| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | -| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | -| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | -| `McpError` | `ProtocolError` | -| `ErrorCode` | `ProtocolErrorCode` | -| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` | -| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` | -| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) | -| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | - -All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use type guard functions like `isCallToolResult` instead of `CallToolResultSchema.safeParse()`. +| `ResourceReference` | `ResourceTemplateReference` | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | +| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | +| `McpError` | `ProtocolError` | +| `ErrorCode` | `ProtocolErrorCode` | +| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` | +| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` | +| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) | +| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | + +All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use +`isSpecType('TypeName', value)` (e.g., `isSpecType('CallToolResult', v)`) or `specTypeSchema('TypeName')` for the `StandardSchemaV1` validator object. The first argument is typed as `SpecTypeName`, a literal union of all spec type names. ### Error class changes @@ -212,7 +213,8 @@ Zod schemas, all callback return types. Note: `callTool()` and `request()` signa The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the `register*` methods with a config object. -**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. For raw JSON Schema, wrap with `fromJsonSchema(schema)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`. +**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. For raw JSON Schema, wrap with +`fromJsonSchema(schema)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`. ### Tools @@ -282,20 +284,20 @@ Note: the third argument (`metadata`) is required — pass `{}` if no metadata. ### Schema Migration Quick Reference -| v1 (raw shape) | v2 (Standard Schema object) | -|----------------|-----------------| -| `{ name: z.string() }` | `z.object({ name: z.string() })` | +| v1 (raw shape) | v2 (Standard Schema object) | +| ---------------------------------- | -------------------------------------------- | +| `{ name: z.string() }` | `z.object({ name: z.string() })` | | `{ count: z.number().optional() }` | `z.object({ count: z.number().optional() })` | | `{}` (empty) | `z.object({})` | | `undefined` (no schema) | `undefined` or omit the field | ### Removed core exports -| Removed from `@modelcontextprotocol/core` | Replacement | -|---|---| -| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | -| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | -| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | +| Removed from `@modelcontextprotocol/core` | Replacement | +| ------------------------------------------------------------------------------------ | ----------------------------------------- | +| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | +| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | +| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | | `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | none (internal Zod introspection helpers) | ## 7. Headers API @@ -321,7 +323,8 @@ new URL(ctx.http?.req?.url).searchParams.get('debug') ### Server-side auth -Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) are removed from the core SDK; use an external IdP/OAuth library. See `examples/server/src/` for demos. +Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, +`ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) are removed from the core SDK; use an external IdP/OAuth library. See `examples/server/src/` for demos. ### Host header validation (Express) @@ -460,29 +463,33 @@ For **custom (non-spec)** methods, keep the result-schema argument — see §9. Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls. -If `CallToolResultSchema` was used for **runtime validation** (not just as a `request()` argument), replace with the `isCallToolResult` type guard: +If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchema`: -| v1 pattern | v2 replacement | -| --------------------------------------------------- | -------------------------- | -| `CallToolResultSchema.safeParse(value).success` | `isCallToolResult(value)` | -| `CallToolResultSchema.parse(value)` | Use `isCallToolResult(value)` then cast, or use `CallToolResult` type | +| v1 pattern | v2 replacement | +| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `CallToolResultSchema.safeParse(value).success` | `isSpecType('CallToolResult', value)` | +| `Schema.safeParse(value).success` | `isSpecType('', value)` | +| `Schema.parse(value)` | `await specTypeSchema('')['~standard'].validate(value)` (returns a `Result`, not the value) | +| Passing `Schema` as a validator argument | `specTypeSchema('')` (a `StandardSchemaV1`) | + +`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name. ## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null` `TaskCreationParams.ttl` changed from `z.union([z.number(), z.null()]).optional()` to `z.number().optional()`. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Omit `ttl` to let the server decide. -| v1 | v2 | -|---|---| -| `task: { ttl: null }` | `task: {}` (omit ttl) | +| v1 | v2 | +| ---------------------- | ---------------------------------- | +| `task: { ttl: null }` | `task: {}` (omit ttl) | | `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) | Type changes in handler context: -| Type | v1 | v2 | -|---|---|---| -| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` | +| Type | v1 | v2 | +| ------------------------------------------- | ----------------------------- | --------------------- | +| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` | | `CreateTaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` | -| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` | +| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` | > These task APIs are `@experimental` and may change without notice. @@ -513,6 +520,7 @@ new McpServer({ name: 'server', version: '1.0.0' }, {}); ``` Access validators explicitly: + - Runtime-aware default: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';` - AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';` - CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';` diff --git a/docs/migration.md b/docs/migration.md index 02f673068..16c27de1f 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -59,7 +59,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; ``` -Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so you can import types and error classes from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly — it is an internal package. +Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so you can import types and error classes from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly +— it is an internal package. ### Dropped Node.js 18 and CommonJS @@ -136,7 +137,8 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30 Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are now first-class in `@modelcontextprotocol/express`. -Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for a working demo with `better-auth`. +Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for +a working demo with `better-auth`. Note: `AuthInfo` has moved from `server/auth/types.ts` to the core types and is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`. @@ -296,11 +298,11 @@ This applies to: **Removed Zod-specific helpers** from `@modelcontextprotocol/core` (use Standard Schema equivalents): -| Removed | Replacement | -|---|---| -| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | -| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | -| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | +| Removed | Replacement | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | +| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | +| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | +| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | | `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | No replacement — these are now internal Zod introspection helpers | ### Host header validation moved @@ -378,7 +380,11 @@ const AcmeSearch = z.object({ params: z.object({ query: z.string(), limit: z.number().int() }) }); server.setRequestHandler(AcmeSearch, async request => { - return { items: [/* ... */] }; + return { + items: [ + /* ... */ + ] + }; }); ``` @@ -389,7 +395,11 @@ const SearchParams = z.object({ query: z.string(), limit: z.number().int() }); const SearchResult = z.object({ items: z.array(z.string()) }); server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => { - return { items: [/* ... */] }; + return { + items: [ + /* ... */ + ] + }; }); ``` @@ -428,8 +438,8 @@ Common method string replacements: ### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer require a schema parameter for spec methods -For **spec** methods, the public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer require a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas -like `CallToolResultSchema` or `ElicitResultSchema` when making spec-method requests. +For **spec** methods, the public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer require a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to +import result schemas like `CallToolResultSchema` or `ElicitResultSchema` when making spec-method requests. **`client.request()` — Before (v1):** @@ -488,18 +498,30 @@ The return type is now inferred from the method name via `ResultTypeMap`. For ex For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method. -If you were using `CallToolResultSchema` for **runtime validation** (not just in `request()`/`callTool()` calls), use the new `isCallToolResult` type guard instead: +If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), use `isSpecType` or `specTypeSchema`: ```typescript // v1: runtime validation with Zod schema import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; -if (CallToolResultSchema.safeParse(value).success) { /* ... */ } +if (CallToolResultSchema.safeParse(value).success) { + /* ... */ +} + +// v2: type predicate by name +import { isSpecType } from '@modelcontextprotocol/client'; +if (isSpecType('CallToolResult', value)) { + /* ... */ +} +const blocks = mixed.filter(v => isSpecType('ContentBlock', v)); -// v2: use the type guard -import { isCallToolResult } from '@modelcontextprotocol/client'; -if (isCallToolResult(value)) { /* ... */ } +// v2: or get the StandardSchemaV1 validator object directly +import { specTypeSchema } from '@modelcontextprotocol/client'; +const result = await specTypeSchema('CallToolResult')['~standard'].validate(value); ``` +The first argument to `isSpecType` and `specTypeSchema` is a `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchema(name)` returns a `StandardSchemaV1`, which composes with any +Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works. + ### Client list methods return empty results for missing capabilities `Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, and `listTools()` now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation. @@ -533,20 +555,21 @@ import { InMemoryTransport } from '@modelcontextprotocol/client'; The following deprecated type aliases have been removed from `@modelcontextprotocol/core`: -| Removed | Replacement | -| ---------------------------------------- | ------------------------------------------------ | -| `JSONRPCError` | `JSONRPCErrorResponse` | -| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | -| `isJSONRPCError` | `isJSONRPCErrorResponse` | -| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) | -| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | -| `ResourceReference` | `ResourceTemplateReference` | -| `IsomorphicHeaders` | Use Web Standard `Headers` | +| Removed | Replacement | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `ResourceReference` | `ResourceTemplateReference` | +| `IsomorphicHeaders` | Use Web Standard `Headers` | | `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | All other types and schemas exported from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. -> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for *result* responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it checks for *any* response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. +> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for _result_ responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it +> checks for _any_ response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. **Before (v1):** @@ -574,7 +597,7 @@ The `RequestHandlerExtra` type has been replaced with a structured context type | `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` | | `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` | | `extra.authInfo` | `ctx.http?.authInfo` | -| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) | +| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) | | `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) | | `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) | | `extra.sessionId` | `ctx.sessionId` | @@ -693,22 +716,22 @@ try { The new `SdkErrorCode` enum contains string-valued codes for local SDK errors: -| Code | Description | -| ------------------------------------------------- | ------------------------------------------- | -| `SdkErrorCode.NotConnected` | Transport is not connected | -| `SdkErrorCode.AlreadyConnected` | Transport is already connected | -| `SdkErrorCode.NotInitialized` | Protocol is not initialized | -| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported | -| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response | -| `SdkErrorCode.ConnectionClosed` | Connection was closed | -| `SdkErrorCode.SendFailed` | Failed to send message | +| Code | Description | +| ------------------------------------------------- | ---------------------------------------------- | +| `SdkErrorCode.NotConnected` | Transport is not connected | +| `SdkErrorCode.AlreadyConnected` | Transport is already connected | +| `SdkErrorCode.NotInitialized` | Protocol is not initialized | +| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported | +| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response | +| `SdkErrorCode.ConnectionClosed` | Connection was closed | +| `SdkErrorCode.SendFailed` | Failed to send message | | `SdkErrorCode.InvalidResult` | Response result failed local schema validation | -| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed | -| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after re-authentication | -| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping | -| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response | -| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream | -| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session | +| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed | +| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after re-authentication | +| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping | +| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response | +| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream | +| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session | #### `StreamableHTTPError` removed @@ -842,7 +865,8 @@ try { ### Experimental: `TaskCreationParams.ttl` no longer accepts `null` -The `ttl` field in `TaskCreationParams` (used when requesting the server to create a task) no longer accepts `null`. Per the MCP spec, `null` TTL (meaning unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Clients should omit `ttl` to let the server decide the lifetime. +The `ttl` field in `TaskCreationParams` (used when requesting the server to create a task) no longer accepts `null`. Per the MCP spec, `null` TTL (meaning unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Clients should omit `ttl` to let +the server decide the lifetime. This also narrows the type of `requestedTtl` in `TaskContext`, `CreateTaskServerContext`, and `TaskServerContext` from `number | null | undefined` to `number | undefined`. diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts index 51a3f5618..82f01b680 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -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, specTypeSchema } 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'; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 8349a7496..c150aea73 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -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'; diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts index 246d36976..a243c1b82 100644 --- a/packages/core/src/types/schemas.ts +++ b/packages/core/src/types/schemas.ts @@ -12,11 +12,11 @@ import type { ResultTypeMap } from './types.js'; -export const JSONValueSchema: z.ZodType = z.lazy(() => +export const JSONValueSchema: z.ZodType = 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 = z.record(z.string(), JSONValueSchema); -export const JSONArraySchema: z.ZodType = z.array(JSONValueSchema); +export const JSONObjectSchema: z.ZodType = z.record(z.string(), JSONValueSchema); +export const JSONArraySchema: z.ZodType = z.array(JSONValueSchema); /** * A progress token, used to associate progress notifications with the original request. */ diff --git a/packages/core/src/types/specTypeSchema.examples.ts b/packages/core/src/types/specTypeSchema.examples.ts new file mode 100644 index 000000000..df3e8cb95 --- /dev/null +++ b/packages/core/src/types/specTypeSchema.examples.ts @@ -0,0 +1,38 @@ +/** + * 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, specTypeSchema } from './specTypeSchema.js'; + +declare const untrusted: unknown; +declare const value: unknown; +declare const mixed: unknown[]; + +async function specTypeSchema_basicUsage() { + //#region specTypeSchema_basicUsage + const result = await specTypeSchema('CallToolResult')['~standard'].validate(untrusted); + if (result.issues === undefined) { + // result.value is CallToolResult + } + //#endregion specTypeSchema_basicUsage + void result; +} + +function isSpecType_basicUsage() { + //#region isSpecType_basicUsage + if (isSpecType('ContentBlock', value)) { + // value is ContentBlock + } + + const blocks = mixed.filter(v => isSpecType('ContentBlock', v)); + //#endregion isSpecType_basicUsage + void blocks; +} + +void specTypeSchema_basicUsage; +void isSpecType_basicUsage; diff --git a/packages/core/src/types/specTypeSchema.ts b/packages/core/src/types/specTypeSchema.ts new file mode 100644 index 000000000..f8800b17e --- /dev/null +++ b/packages/core/src/types/specTypeSchema.ts @@ -0,0 +1,293 @@ +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 ProtocolSchemaKey + ? (typeof schemas)[K] + : K extends AuthSchemaKey + ? (typeof authSchemas)[K] + : never; + +type StripSchemaSuffix = 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; + +/** + * 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]: SchemaFor extends z.ZodType ? z.output> : 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]: SchemaFor extends z.ZodType ? z.input> : never; +}; + +// Populated for every SpecTypeName by the loops below; the cast lets `allSchemas[name]` be +// non-undefined under `noUncheckedIndexedAccess` when `name` is a SpecTypeName. +const allSchemas = {} as Record; +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 + allSchemas[key.slice(0, -'Schema'.length) as SpecTypeName] = schemas[key]; +} +for (const [key, schema] of Object.entries(authSchemas)) { + allSchemas[key.slice(0, -'Schema'.length) as SpecTypeName] = schema; +} + +/** + * Returns the runtime validator for the named MCP spec type. + * + * 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`. + * + * The returned validator 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#specTypeSchema_basicUsage" + * const result = await specTypeSchema('CallToolResult')['~standard'].validate(untrusted); + * if (result.issues === undefined) { + * // result.value is CallToolResult + * } + * ``` + */ +export function specTypeSchema(name: K): StandardSchemaV1; +export function specTypeSchema(name: SpecTypeName): StandardSchemaV1 { + return allSchemas[name]; +} + +/** + * Type predicate for the named MCP spec type. + * + * 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 + * `specTypeSchema(name)['~standard'].validate(value)` when you need the validated output value. + * + * @example + * ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage" + * if (isSpecType('ContentBlock', value)) { + * // value is ContentBlock + * } + * + * const blocks = mixed.filter(v => isSpecType('ContentBlock', v)); + * ``` + */ +export function isSpecType(name: K, value: unknown): value is SpecTypeInputs[K]; +export function isSpecType(name: SpecTypeName, value: unknown): boolean { + return allSchemas[name].safeParse(value).success; +} diff --git a/packages/core/src/types/guards.test.ts b/packages/core/test/types/guards.test.ts similarity index 96% rename from packages/core/src/types/guards.test.ts rename to packages/core/test/types/guards.test.ts index d80f9474a..117e9ecda 100644 --- a/packages/core/src/types/guards.test.ts +++ b/packages/core/test/types/guards.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { JSONRPC_VERSION } from './constants.js'; -import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from './guards.js'; +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', () => { diff --git a/packages/core/test/types/specTypeSchema.test.ts b/packages/core/test/types/specTypeSchema.test.ts new file mode 100644 index 000000000..a502cd852 --- /dev/null +++ b/packages/core/test/types/specTypeSchema.test.ts @@ -0,0 +1,145 @@ +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, specTypeSchema } from '../../src/types/specTypeSchema.js'; +import type { + CallToolResult, + ContentBlock, + Implementation, + JSONObject, + JSONRPCRequest, + JSONValue, + ResourceTemplateType, + Tool +} from '../../src/types/types.js'; + +describe('specTypeSchema', () => { + it('returns a StandardSchemaV1 validator that accepts valid values', () => { + const result = specTypeSchema('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 = specTypeSchema('Implementation')['~standard'].validate({ name: 'x' }); + expect((result as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0); + }); + + it('rejects unknown names at compile time', () => { + // @ts-expect-error - 'NotASpecType' is not a SpecTypeName; the literal type constraint rejects it. + expect(specTypeSchema('NotASpecType')).toBeUndefined(); + }); + + it('covers JSON-RPC envelope types', () => { + const ok = specTypeSchema('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 = specTypeSchema('OAuthTokens')['~standard'].validate({ access_token: 'x', token_type: 'Bearer' }); + expect((ok as { issues?: unknown }).issues).toBeUndefined(); + const bad = specTypeSchema('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 and internal-only names at compile time', () => { + // @ts-expect-error - 'NotASpecType' is not a SpecTypeName; the literal type constraint rejects it. + void ((v: unknown) => isSpecType('NotASpecType', v)); + // @ts-expect-error - ListChangedOptionsBase is an internal helper, not in SpecTypeName. + void ((v: unknown) => isSpecType('ListChangedOptionsBase', v)); + // @ts-expect-error - BaseRequestParams is an internal helper, not in SpecTypeName. + void specTypeSchema('BaseRequestParams'); + // @ts-expect-error - NotificationsParams is an internal helper, not in SpecTypeName. + void ((v: unknown) => isSpecType('NotificationsParams', v)); + }); + + 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(); + } + }); + + 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(); + expectTypeOf(v).not.toEqualTypeOf(); + } + }); + + it('JSONValue / JSONObject — narrows to the JSON type, not unknown', () => { + // These schemas use an explicit z.ZodType 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(); + } + if (isSpecType('JSONObject', v)) { + expectTypeOf(v).toEqualTypeOf(); + } + }); + + it('works as a filter callback via an arrow wrapper and narrows the element type', () => { + const mixed: unknown[] = [{ type: 'text', text: 'hi' }, 42, { type: 'text' }]; + const blocks = mixed.filter(v => isSpecType('ContentBlock', v)); + expect(blocks).toHaveLength(1); + expectTypeOf(blocks).toEqualTypeOf(); + }); +}); + +describe('SpecTypeName / SpecTypes (type-level)', () => { + it('SpecTypeName includes representative names', () => { + expectTypeOf<'CallToolResult'>().toMatchTypeOf(); + expectTypeOf<'ContentBlock'>().toMatchTypeOf(); + expectTypeOf<'Tool'>().toMatchTypeOf(); + expectTypeOf<'Implementation'>().toMatchTypeOf(); + expectTypeOf<'JSONRPCRequest'>().toMatchTypeOf(); + expectTypeOf<'OAuthTokens'>().toMatchTypeOf(); + expectTypeOf<'OAuthMetadata'>().toMatchTypeOf(); + expectTypeOf<'ResourceTemplate'>().toMatchTypeOf(); + }); + + it('SpecTypes[K] matches the named export type', () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + // 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().toEqualTypeOf(); + }); +});