[v2] Minor task-related refactors (#1758)
Co-authored-by: Luca Chang <lucalc@amazon.com> Co-authored-by: Luca Chang <131398524+LucaButBoring@users.noreply.github.com> Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
parent
cc9c9d19a1
commit
e86b1835cc
8 changed files with 108 additions and 18 deletions
6
.changeset/busy-rice-smoke.md
Normal file
6
.changeset/busy-rice-smoke.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@modelcontextprotocol/client': patch
|
||||
'@modelcontextprotocol/server': patch
|
||||
---
|
||||
|
||||
tasks - disallow requesting a null TTL
|
||||
|
|
@ -433,11 +433,30 @@ 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.
|
||||
|
||||
## 12. Client Behavioral Changes
|
||||
## 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) |
|
||||
| `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) |
|
||||
|
||||
Type changes in handler context:
|
||||
|
||||
| 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` |
|
||||
|
||||
> These task APIs are `@experimental` and may change without notice.
|
||||
|
||||
## 13. Client Behavioral Changes
|
||||
|
||||
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.
|
||||
|
||||
## 13. Runtime-Specific JSON Schema Validators (Enhancement)
|
||||
## 14. Runtime-Specific JSON Schema Validators (Enhancement)
|
||||
|
||||
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
|
||||
|
||||
|
|
@ -461,7 +480,7 @@ new McpServer({ name: 'server', version: '1.0.0' }, {});
|
|||
|
||||
Access validators via `_shims` export: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';`
|
||||
|
||||
## 14. Migration Steps (apply in this order)
|
||||
## 15. Migration Steps (apply in this order)
|
||||
|
||||
1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
|
||||
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`
|
||||
|
|
|
|||
|
|
@ -757,6 +757,46 @@ 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.
|
||||
|
||||
This also narrows the type of `requestedTtl` in `TaskContext`, `CreateTaskServerContext`, and `TaskServerContext` from `number | null | undefined` to `number | undefined`.
|
||||
|
||||
**Before (v1):**
|
||||
|
||||
```typescript
|
||||
// Requesting unlimited lifetime by passing null
|
||||
const result = await client.callTool({
|
||||
name: 'long-task',
|
||||
arguments: {},
|
||||
task: { ttl: null }
|
||||
});
|
||||
|
||||
// Handler context had number | null | undefined
|
||||
server.setRequestHandler('tools/call', async (request, ctx) => {
|
||||
const ttl: number | null | undefined = ctx.task?.requestedTtl;
|
||||
});
|
||||
```
|
||||
|
||||
**After (v2):**
|
||||
|
||||
```typescript
|
||||
// Omit ttl to let the server decide (server may return null for unlimited)
|
||||
const result = await client.callTool({
|
||||
name: 'long-task',
|
||||
arguments: {},
|
||||
task: {}
|
||||
});
|
||||
|
||||
// Handler context is now number | undefined
|
||||
server.setRequestHandler('tools/call', async (request, ctx) => {
|
||||
const ttl: number | undefined = ctx.task?.requestedTtl;
|
||||
});
|
||||
```
|
||||
|
||||
> **Note:** These task APIs are marked `@experimental` and may change without notice.
|
||||
|
||||
## Enhancements
|
||||
|
||||
### Automatic JSON Schema validator selection by runtime
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import type {
|
|||
* @experimental
|
||||
*/
|
||||
export type CreateTaskServerContext = ServerContext & {
|
||||
task: { store: RequestTaskStore; requestedTtl?: number | null };
|
||||
task: { store: RequestTaskStore; requestedTtl?: number };
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +34,7 @@ export type CreateTaskServerContext = ServerContext & {
|
|||
* @experimental
|
||||
*/
|
||||
export type TaskServerContext = ServerContext & {
|
||||
task: { id: string; store: RequestTaskStore; requestedTtl?: number | null };
|
||||
task: { id: string; store: RequestTaskStore; requestedTtl?: number };
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -137,7 +137,7 @@ export interface TaskMessageQueue {
|
|||
*/
|
||||
export interface CreateTaskOptions {
|
||||
/**
|
||||
* Time in milliseconds to keep task results available after completion.
|
||||
* Duration in milliseconds to retain task from creation.
|
||||
* If `null`, the task has unlimited lifetime until manually cleaned up.
|
||||
*/
|
||||
ttl?: number | null;
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export interface RequestTaskStore {
|
|||
export type TaskContext = {
|
||||
id?: string;
|
||||
store: RequestTaskStore;
|
||||
requestedTtl?: number | null;
|
||||
requestedTtl?: number;
|
||||
};
|
||||
|
||||
export type TaskManagerOptions = {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,9 @@ export const CursorSchema = z.string();
|
|||
*/
|
||||
export const TaskCreationParamsSchema = z.looseObject({
|
||||
/**
|
||||
* Time in milliseconds to keep task results available after completion.
|
||||
* If `null`, the task has unlimited lifetime until manually cleaned up.
|
||||
* Requested duration in milliseconds to retain task from creation.
|
||||
*/
|
||||
ttl: z.union([z.number(), z.null()]).optional(),
|
||||
ttl: z.number().optional(),
|
||||
|
||||
/**
|
||||
* Time in milliseconds to wait between task status requests.
|
||||
|
|
|
|||
|
|
@ -488,17 +488,16 @@ describe('InMemoryTaskStore', () => {
|
|||
expect(task).toBeNull();
|
||||
});
|
||||
|
||||
it('should support null TTL for unlimited lifetime', async () => {
|
||||
// Test that null TTL means unlimited lifetime
|
||||
const taskParams: TaskCreationParams = {
|
||||
ttl: null
|
||||
};
|
||||
it('should support omitted TTL for unlimited lifetime', async () => {
|
||||
// Test that omitting TTL means unlimited lifetime (server returns null)
|
||||
// Per spec: clients omit ttl to let server decide, server returns null for unlimited
|
||||
const taskParams: TaskCreationParams = {};
|
||||
const createdTask = await store.createTask(taskParams, 2222, {
|
||||
method: 'tools/call',
|
||||
params: {}
|
||||
});
|
||||
|
||||
// The returned task should have null TTL
|
||||
// The returned task should have null TTL (unlimited)
|
||||
expect(createdTask.ttl).toBeNull();
|
||||
|
||||
// Task should not be cleaned up even after a long time
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { isTerminal } from '@modelcontextprotocol/core';
|
||||
import type { Task } from '@modelcontextprotocol/server';
|
||||
import type { Task } from '@modelcontextprotocol/core';
|
||||
import { isTerminal, TaskCreationParamsSchema } from '@modelcontextprotocol/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Task utility functions', () => {
|
||||
|
|
@ -115,3 +115,30 @@ describe('Task Schema Validation', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskCreationParams Schema Validation', () => {
|
||||
it('should accept ttl as a number', () => {
|
||||
const result = TaskCreationParamsSchema.safeParse({ ttl: 60_000 });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept missing ttl (optional)', () => {
|
||||
const result = TaskCreationParamsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject null ttl (not allowed in request, only response)', () => {
|
||||
const result = TaskCreationParamsSchema.safeParse({ ttl: null });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept pollInterval as a number', () => {
|
||||
const result = TaskCreationParamsSchema.safeParse({ pollInterval: 1000 });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept both ttl and pollInterval', () => {
|
||||
const result = TaskCreationParamsSchema.safeParse({ ttl: 60_000, pollInterval: 1000 });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue