feat(core): add isSpecType / specTypeSchemas Records for runtime validation of spec types (#1887)
This commit is contained in:
parent
db83829c5b
commit
96db044fe9
11 changed files with 590 additions and 76 deletions
6
.changeset/spec-type-schema.md
Normal file
6
.changeset/spec-type-schema.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@modelcontextprotocol/client': minor
|
||||
'@modelcontextprotocol/server': minor
|
||||
---
|
||||
|
||||
Export `isSpecType` and `specTypeSchemas` records for runtime validation of any MCP spec type by name. `isSpecType.ContentBlock(value)` is a type predicate; `specTypeSchemas.ContentBlock` is a `StandardSchemaV1<ContentBlock>` validator. Guards are standalone functions, so `arr.filter(isSpecType.ContentBlock)` works. Also export the `SpecTypeName` and `SpecTypes` types.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -59,8 +59,8 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
|
|||
|
||||
### Types / shared imports
|
||||
|
||||
| v1 import path | v2 package |
|
||||
| ------------------------------------------------- | ---------------------------- |
|
||||
| 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` |
|
||||
|
|
@ -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`) |
|
||||
| `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()`.
|
||||
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 `specTypeSchemas.TypeName` for the `StandardSchemaV1` validator object. The keys are 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<T>` | `StandardSchemaWithJSON.InferInput<T>` |
|
||||
| Removed from `@modelcontextprotocol/core` | Replacement |
|
||||
| ------------------------------------------------------------------------------------ | ----------------------------------------- |
|
||||
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
|
||||
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
|
||||
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
|
||||
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | none (internal Zod introspection helpers) |
|
||||
|
||||
## 7. Headers API
|
||||
|
|
@ -460,29 +462,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` / `specTypeSchemas`:
|
||||
|
||||
| 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)` |
|
||||
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
|
||||
| `<TypeName>Schema.parse(value)` | `await specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result`, not the value) |
|
||||
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1<In, Out>`) |
|
||||
|
||||
`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 +519,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';`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -296,11 +297,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<T>` | `StandardSchemaWithJSON.InferInput<T>` |
|
||||
| Removed | Replacement |
|
||||
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
||||
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
|
||||
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
|
||||
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
|
||||
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | No replacement — these are now internal Zod introspection helpers |
|
||||
|
||||
### Host header validation moved
|
||||
|
|
@ -488,18 +489,29 @@ 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 `specTypeSchemas`:
|
||||
|
||||
```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: use the type guard
|
||||
import { isCallToolResult } from '@modelcontextprotocol/client';
|
||||
if (isCallToolResult(value)) { /* ... */ }
|
||||
// v2: keyed type predicate
|
||||
import { isSpecType } from '@modelcontextprotocol/client';
|
||||
if (isSpecType.CallToolResult(value)) {
|
||||
/* ... */
|
||||
}
|
||||
const blocks = mixed.filter(isSpecType.ContentBlock);
|
||||
|
||||
// v2: or get the StandardSchemaV1 validator object directly
|
||||
import { specTypeSchemas } from '@modelcontextprotocol/client';
|
||||
const result = await specTypeSchemas.CallToolResult['~standard'].validate(value);
|
||||
```
|
||||
|
||||
`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1<In, Out>`, 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 +545,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 +587,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` |
|
||||
|
|
@ -842,7 +855,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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
40
packages/core/src/types/specTypeSchema.examples.ts
Normal file
40
packages/core/src/types/specTypeSchema.examples.ts
Normal 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;
|
||||
296
packages/core/src/types/specTypeSchema.ts
Normal file
296
packages/core/src/types/specTypeSchema.ts
Normal 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;
|
||||
123
packages/core/test/types/guards.test.ts
Normal file
123
packages/core/test/types/guards.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
148
packages/core/test/types/specTypeSchema.test.ts
Normal file
148
packages/core/test/types/specTypeSchema.test.ts
Normal 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>();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue