Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -482,14 +494,16 @@ 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` |
| `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.

**Before (v1):**

```typescript
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,13 @@ export { ProtocolError, UrlElicitationRequiredError } from '../../types/errors.j
export {
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate,
isCallToolResult,
isInitializedNotification,
isInitializeRequest,
isJSONRPCErrorResponse,
isJSONRPCNotification,
isJSONRPCRequest,
isJSONRPCResponse,
isJSONRPCResultResponse,
isTaskAugmentedRequestParams,
parseJSONRPCMessage
Expand Down
123 changes: 123 additions & 0 deletions packages/core/src/types/guards.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
23 changes: 23 additions & 0 deletions packages/core/src/types/guards.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {
CallToolResultSchema,
InitializedNotificationSchema,
InitializeRequestSchema,
JSONRPCErrorResponseSchema,
JSONRPCMessageSchema,
JSONRPCNotificationSchema,
JSONRPCRequestSchema,
JSONRPCResponseSchema,
JSONRPCResultResponseSchema,
TaskAugmentedRequestParamsSchema
} from './schemas.js';
import type {
CallToolResult,
CompleteRequest,
CompleteRequestPrompt,
CompleteRequestResourceTemplate,
Expand All @@ -18,6 +21,7 @@ import type {
JSONRPCMessage,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
JSONRPCResultResponse,
TaskAugmentedRequestParams
} from './types.js';
Expand Down Expand Up @@ -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.
Expand Down
Loading