v2: add guard methods (#1842)

This commit is contained in:
Konstantin Konstantinov 2026-04-11 16:29:13 +03:00 • committed by GitHub
commit 1eb80c4d08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 172 additions and 3 deletions

View file

@ -86,7 +86,7 @@ Notes:
| `JSONRPCError` | `JSONRPCErrorResponse` |
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
| `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`) |
@ -98,7 +98,7 @@ Notes:
| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) |
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |
All other symbols from `@modelcontextprotocol/sdk/types.js` retain their original names (e.g., `CallToolResultSchema`, `ListToolsResultSchema`, etc.).
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()`.
### Error class changes
@ -435,6 +435,13 @@ const tool = await client.callTool({ name: 'my-tool', arguments: {} });
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:
| v1 pattern | v2 replacement |
| --------------------------------------------------- | -------------------------- |
| `CallToolResultSchema.safeParse(value).success` | `isCallToolResult(value)` |
| `CallToolResultSchema.parse(value)` | Use `isCallToolResult(value)` then cast, or use `CallToolResult` type |
## 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.

View file

@ -442,6 +442,18 @@ const result = await client.callTool({ name: 'my-tool', arguments: {} });
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult | CreateTaskResult>`.
If you were using `CallToolResultSchema` for **runtime validation** (not just in `request()`/`callTool()` calls), use the new `isCallToolResult` type guard instead:
```typescript
// v1: runtime validation with Zod schema
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
if (CallToolResultSchema.safeParse(value).success) { /* ... */ }
// v2: use the type guard
import { isCallToolResult } from '@modelcontextprotocol/client';
if (isCallToolResult(value)) { /* ... */ }
```
### 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.
@ -482,7 +494,7 @@ The following deprecated type aliases have been removed from `@modelcontextproto
| `JSONRPCError` | `JSONRPCErrorResponse` |
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) |
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
| `ResourceReference` | `ResourceTemplateReference` |
| `IsomorphicHeaders` | Use Web Standard `Headers` |
@ -490,6 +502,8 @@ The following deprecated type aliases have been removed from `@modelcontextproto
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.
**Before (v1):**
```typescript

View file

@ -104,11 +104,13 @@ export { ProtocolError, UrlElicitationRequiredError } from '../../types/errors.j
export {
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate,
isCallToolResult,
isInitializedNotification,
isInitializeRequest,
isJSONRPCErrorResponse,
isJSONRPCNotification,
isJSONRPCRequest,
isJSONRPCResponse,
isJSONRPCResultResponse,
isTaskAugmentedRequestParams,
parseJSONRPCMessage

View file

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

View file

@ -1,14 +1,17 @@
import {
CallToolResultSchema,
InitializedNotificationSchema,
InitializeRequestSchema,
JSONRPCErrorResponseSchema,
JSONRPCMessageSchema,
JSONRPCNotificationSchema,
JSONRPCRequestSchema,
JSONRPCResponseSchema,
JSONRPCResultResponseSchema,
TaskAugmentedRequestParamsSchema
} from './schemas.js';
import type {
CallToolResult,
CompleteRequest,
CompleteRequestPrompt,
CompleteRequestResourceTemplate,
@ -18,6 +21,7 @@ import type {
JSONRPCMessage,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
JSONRPCResultResponse,
TaskAugmentedRequestParams
} from './types.js';
@ -58,6 +62,25 @@ export const isJSONRPCResultResponse = (value: unknown): value is JSONRPCResultR
export const isJSONRPCErrorResponse = (value: unknown): value is JSONRPCErrorResponse =>
JSONRPCErrorResponseSchema.safeParse(value).success;
/**
* Checks if a value is a valid {@linkcode JSONRPCResponse} (either a result or error response).
* @param value - The value to check.
*
* @returns True if the value is a valid {@linkcode JSONRPCResponse}, false otherwise.
*/
export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => JSONRPCResponseSchema.safeParse(value).success;
/**
* Checks if a value is a valid {@linkcode CallToolResult}.
* @param value - The value to check.
*
* @returns True if the value is a valid {@linkcode CallToolResult}, false otherwise.
*/
export const isCallToolResult = (value: unknown): value is CallToolResult => {
if (typeof value !== 'object' || value === null || !('content' in value)) return false;
return CallToolResultSchema.safeParse(value).success;
};
/**
* Checks if a value is a valid {@linkcode TaskAugmentedRequestParams}.
* @param value - The value to check.