Skip to content

Commit e86b183

Browse files
KKonstantinovLucaButBoringfelixweinberger
authored
[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>
1 parent cc9c9d1 commit e86b183

8 files changed

Lines changed: 108 additions & 18 deletions

File tree

.changeset/busy-rice-smoke.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
tasks - disallow requesting a null TTL

docs/migration-SKILL.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -433,11 +433,30 @@ const tool = await client.callTool({ name: 'my-tool', arguments: {} });
433433

434434
Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls.
435435

436-
## 12. Client Behavioral Changes
436+
## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null`
437+
438+
`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.
439+
440+
| v1 | v2 |
441+
|---|---|
442+
| `task: { ttl: null }` | `task: {}` (omit ttl) |
443+
| `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) |
444+
445+
Type changes in handler context:
446+
447+
| Type | v1 | v2 |
448+
|---|---|---|
449+
| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
450+
| `CreateTaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
451+
| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
452+
453+
> These task APIs are `@experimental` and may change without notice.
454+
455+
## 13. Client Behavioral Changes
437456

438457
`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.
439458

440-
## 13. Runtime-Specific JSON Schema Validators (Enhancement)
459+
## 14. Runtime-Specific JSON Schema Validators (Enhancement)
441460

442461
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
443462

@@ -461,7 +480,7 @@ new McpServer({ name: 'server', version: '1.0.0' }, {});
461480

462481
Access validators via `_shims` export: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';`
463482

464-
## 14. Migration Steps (apply in this order)
483+
## 15. Migration Steps (apply in this order)
465484

466485
1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
467486
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport``NodeStreamableHTTPServerTransport`

docs/migration.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,46 @@ try {
757757
}
758758
```
759759

760+
### Experimental: `TaskCreationParams.ttl` no longer accepts `null`
761+
762+
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.
763+
764+
This also narrows the type of `requestedTtl` in `TaskContext`, `CreateTaskServerContext`, and `TaskServerContext` from `number | null | undefined` to `number | undefined`.
765+
766+
**Before (v1):**
767+
768+
```typescript
769+
// Requesting unlimited lifetime by passing null
770+
const result = await client.callTool({
771+
name: 'long-task',
772+
arguments: {},
773+
task: { ttl: null }
774+
});
775+
776+
// Handler context had number | null | undefined
777+
server.setRequestHandler('tools/call', async (request, ctx) => {
778+
const ttl: number | null | undefined = ctx.task?.requestedTtl;
779+
});
780+
```
781+
782+
**After (v2):**
783+
784+
```typescript
785+
// Omit ttl to let the server decide (server may return null for unlimited)
786+
const result = await client.callTool({
787+
name: 'long-task',
788+
arguments: {},
789+
task: {}
790+
});
791+
792+
// Handler context is now number | undefined
793+
server.setRequestHandler('tools/call', async (request, ctx) => {
794+
const ttl: number | undefined = ctx.task?.requestedTtl;
795+
});
796+
```
797+
798+
> **Note:** These task APIs are marked `@experimental` and may change without notice.
799+
760800
## Enhancements
761801

762802
### Automatic JSON Schema validator selection by runtime

packages/core/src/experimental/tasks/interfaces.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ import type {
2626
* @experimental
2727
*/
2828
export type CreateTaskServerContext = ServerContext & {
29-
task: { store: RequestTaskStore; requestedTtl?: number | null };
29+
task: { store: RequestTaskStore; requestedTtl?: number };
3030
};
3131

3232
/**
3333
* Server context with guaranteed task ID and store for task operations.
3434
* @experimental
3535
*/
3636
export type TaskServerContext = ServerContext & {
37-
task: { id: string; store: RequestTaskStore; requestedTtl?: number | null };
37+
task: { id: string; store: RequestTaskStore; requestedTtl?: number };
3838
};
3939

4040
/**
@@ -137,7 +137,7 @@ export interface TaskMessageQueue {
137137
*/
138138
export interface CreateTaskOptions {
139139
/**
140-
* Time in milliseconds to keep task results available after completion.
140+
* Duration in milliseconds to retain task from creation.
141141
* If `null`, the task has unlimited lifetime until manually cleaned up.
142142
*/
143143
ttl?: number | null;

packages/core/src/shared/taskManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export interface RequestTaskStore {
151151
export type TaskContext = {
152152
id?: string;
153153
store: RequestTaskStore;
154-
requestedTtl?: number | null;
154+
requestedTtl?: number;
155155
};
156156

157157
export type TaskManagerOptions = {

packages/core/src/types/schemas.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ export const CursorSchema = z.string();
3232
*/
3333
export const TaskCreationParamsSchema = z.looseObject({
3434
/**
35-
* Time in milliseconds to keep task results available after completion.
36-
* If `null`, the task has unlimited lifetime until manually cleaned up.
35+
* Requested duration in milliseconds to retain task from creation.
3736
*/
38-
ttl: z.union([z.number(), z.null()]).optional(),
37+
ttl: z.number().optional(),
3938

4039
/**
4140
* Time in milliseconds to wait between task status requests.

packages/core/test/experimental/inMemory.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -488,17 +488,16 @@ describe('InMemoryTaskStore', () => {
488488
expect(task).toBeNull();
489489
});
490490

491-
it('should support null TTL for unlimited lifetime', async () => {
492-
// Test that null TTL means unlimited lifetime
493-
const taskParams: TaskCreationParams = {
494-
ttl: null
495-
};
491+
it('should support omitted TTL for unlimited lifetime', async () => {
492+
// Test that omitting TTL means unlimited lifetime (server returns null)
493+
// Per spec: clients omit ttl to let server decide, server returns null for unlimited
494+
const taskParams: TaskCreationParams = {};
496495
const createdTask = await store.createTask(taskParams, 2222, {
497496
method: 'tools/call',
498497
params: {}
499498
});
500499

501-
// The returned task should have null TTL
500+
// The returned task should have null TTL (unlimited)
502501
expect(createdTask.ttl).toBeNull();
503502

504503
// Task should not be cleaned up even after a long time

test/integration/test/experimental/tasks/task.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { isTerminal } from '@modelcontextprotocol/core';
2-
import type { Task } from '@modelcontextprotocol/server';
1+
import type { Task } from '@modelcontextprotocol/core';
2+
import { isTerminal, TaskCreationParamsSchema } from '@modelcontextprotocol/core';
33
import { describe, expect, it } from 'vitest';
44

55
describe('Task utility functions', () => {
@@ -115,3 +115,30 @@ describe('Task Schema Validation', () => {
115115
}
116116
});
117117
});
118+
119+
describe('TaskCreationParams Schema Validation', () => {
120+
it('should accept ttl as a number', () => {
121+
const result = TaskCreationParamsSchema.safeParse({ ttl: 60_000 });
122+
expect(result.success).toBe(true);
123+
});
124+
125+
it('should accept missing ttl (optional)', () => {
126+
const result = TaskCreationParamsSchema.safeParse({});
127+
expect(result.success).toBe(true);
128+
});
129+
130+
it('should reject null ttl (not allowed in request, only response)', () => {
131+
const result = TaskCreationParamsSchema.safeParse({ ttl: null });
132+
expect(result.success).toBe(false);
133+
});
134+
135+
it('should accept pollInterval as a number', () => {
136+
const result = TaskCreationParamsSchema.safeParse({ pollInterval: 1000 });
137+
expect(result.success).toBe(true);
138+
});
139+
140+
it('should accept both ttl and pollInterval', () => {
141+
const result = TaskCreationParamsSchema.safeParse({ ttl: 60_000, pollInterval: 1000 });
142+
expect(result.success).toBe(true);
143+
});
144+
});

0 commit comments

Comments
 (0)