Skip to content
Open
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
18 changes: 9 additions & 9 deletions examples/server/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,16 +478,16 @@ const getServer = () => {
}
},
{
async createTask({ duration }, { taskStore, taskRequestedTtl }) {
async createTask({ duration }, { task }) {
// Create the task
const task = await taskStore.createTask({
ttl: taskRequestedTtl
const newTask = await task.store.createTask({
ttl: task.requestedTtl
});

// Simulate out-of-band work
(async () => {
await new Promise(resolve => setTimeout(resolve, duration));
await taskStore.storeTaskResult(task.taskId, 'completed', {
await task.store.storeTaskResult(newTask.taskId, 'completed', {
content: [
{
type: 'text',
Expand All @@ -499,14 +499,14 @@ const getServer = () => {

// Return CreateTaskResult with the created task
return {
task
task: newTask
};
},
async getTask(_args, { taskId, taskStore }) {
return await taskStore.getTask(taskId);
async getTask(_args, { task }) {
return await task.store.getTask(task.id);
},
async getTaskResult(_args, { taskId, taskStore }) {
const result = await taskStore.getTaskResult(taskId);
async getTaskResult(_args, { task }) {
const result = await task.store.getTaskResult(task.id);
return result as CallToolResult;
}
}
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/experimental/tasks/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* WARNING: These APIs are experimental and may change without notice.
*/

import type { RequestHandlerExtra, RequestTaskStore } from '../../shared/protocol.js';
import type { RequestHandlerExtra, TaskContext } from '../../shared/protocol.js';
import type {
JSONRPCErrorResponse,
JSONRPCNotification,
Expand All @@ -23,20 +23,19 @@ import type {
// ============================================================================

/**
* Extended handler extra with task store for task creation.
* Extended handler extra with task context for task creation.
* @experimental
*/
export interface CreateTaskRequestHandlerExtra extends RequestHandlerExtra<ServerRequest, ServerNotification> {
taskStore: RequestTaskStore;
task: TaskContext;
}

/**
* Extended handler extra with task ID and store for task operations.
* @experimental
*/
export interface TaskRequestHandlerExtra extends RequestHandlerExtra<ServerRequest, ServerNotification> {
taskId: string;
taskStore: RequestTaskStore;
task: TaskContext & { id: string };
}

/**
Expand Down
32 changes: 24 additions & 8 deletions packages/core/src/shared/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,18 @@ export interface RequestTaskStore {
listTasks(cursor?: string): Promise<{ tasks: Task[]; nextCursor?: string }>;
}

/**
* Context for task-related operations in request handlers.
*/
export interface TaskContext {
/** The related task identifier (present when operating on existing task) */
id?: string;
/** Task store for managing task state */
store: RequestTaskStore;
/** Requested TTL in milliseconds (from client's task creation params) */
requestedTtl?: number;
}

/**
* Extra data given to request handlers.
*/
Expand Down Expand Up @@ -265,11 +277,11 @@ export type RequestHandlerExtra<SendRequestT extends Request, SendNotificationT
*/
requestId: RequestId;

taskId?: string;

taskStore?: RequestTaskStore;

taskRequestedTtl?: number | null;
/**
* Task context for task-related operations.
* Present when the server has task storage enabled.
*/
task?: TaskContext;

/**
* The original HTTP request.
Expand Down Expand Up @@ -749,9 +761,13 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
authInfo: extra?.authInfo,
requestId: request.id,
requestInfo: extra?.requestInfo,
taskId: relatedTaskId,
taskStore: taskStore,
taskRequestedTtl: taskCreationParams?.ttl,
task: taskStore
? {
id: relatedTaskId,
store: taskStore,
requestedTtl: taskCreationParams?.ttl
}
: undefined,
closeSSEStream: extra?.closeSSEStream,
closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
};
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,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.
Expand Down
11 changes: 5 additions & 6 deletions packages/core/test/experimental/in-memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/shared/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2489,12 +2489,12 @@ describe('Progress notification support for tasks', () => {

// Set up a request handler that will complete the task
protocol.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
if (extra.taskStore) {
const task = await extra.taskStore.createTask({ ttl: 60000 });
if (extra.task?.store) {
const task = await extra.task?.store.createTask({ ttl: 60000 });

// Simulate async work then complete the task
setTimeout(async () => {
await extra.taskStore!.storeTaskResult(task.taskId, 'completed', {
await extra.task?.store!.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: 'Done' }]
});
}, 50);
Expand Down
10 changes: 5 additions & 5 deletions packages/server/src/experimental/tasks/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ export class ExperimentalMcpServerTasks {
* execution: { taskSupport: 'required' }
* }, {
* createTask: async (args, extra) => {
* const task = await extra.taskStore.createTask({ ttl: 300000 });
* startBackgroundWork(task.taskId, args);
* return { task };
* const newTask = await extra.task.store.createTask({ ttl: 300000 });
* startBackgroundWork(newTask.taskId, args);
* return { task: newTask };
* },
* getTask: async (args, extra) => {
* return extra.taskStore.getTask(extra.taskId);
* return extra.task.store.getTask(extra.task.id);
* },
* getTaskResult: async (args, extra) => {
* return extra.taskStore.getTaskResult(extra.taskId);
* return extra.task.store.getTaskResult(extra.task.id);
* }
* });
* ```
Expand Down
12 changes: 6 additions & 6 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,10 @@ export class McpServer {
const isTaskHandler = 'createTask' in handler;

if (isTaskHandler) {
if (!extra.taskStore) {
if (!extra.task?.store) {
throw new Error('No task store provided.');
}
const taskExtra = { ...extra, taskStore: extra.taskStore };
const taskExtra = { ...extra, task: extra.task };

if (tool.inputSchema) {
const typedHandler = handler as ToolTaskHandler<ZodRawShapeCompat>;
Expand Down Expand Up @@ -367,14 +367,14 @@ export class McpServer {
request: RequestT,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
): Promise<CallToolResult> {
if (!extra.taskStore) {
if (!extra.task?.store) {
throw new Error('No task store provided for task-capable tool.');
}

// Validate input and create task
const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
const handler = tool.handler as ToolTaskHandler<ZodRawShapeCompat | undefined>;
const taskExtra = { ...extra, taskStore: extra.taskStore };
const taskExtra = { ...extra, task: extra.task };

const createTaskResult: CreateTaskResult = args // undefined only if tool.inputSchema is undefined
? await Promise.resolve((handler as ToolTaskHandler<ZodRawShapeCompat>).createTask(args, taskExtra))
Expand All @@ -388,15 +388,15 @@ export class McpServer {

while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const updatedTask = await extra.taskStore.getTask(taskId);
const updatedTask = await extra.task.store.getTask(taskId);
if (!updatedTask) {
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
}
task = updatedTask;
}

// Return the final result
return (await extra.taskStore.getTaskResult(taskId)) as CallToolResult;
return (await extra.task.store.getTaskResult(taskId)) as CallToolResult;
}

private _completionHandlerInitialized = false;
Expand Down
Loading
Loading