[SEP-2663] refactor!: remove 2025-11 experimental tasks (#2128)

This commit is contained in:
Felix Weinberger 2026-06-02 13:04:18 +01:00 • committed by GitHub
commit c8d7401b46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 423 additions and 18280 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,144 +0,0 @@
import type { Task } from '@modelcontextprotocol/core';
import { isTerminal, TaskCreationParamsSchema } from '@modelcontextprotocol/core';
import { describe, expect, it } from 'vitest';
describe('Task utility functions', () => {
describe('isTerminal', () => {
it('should return true for completed status', () => {
expect(isTerminal('completed')).toBe(true);
});
it('should return true for failed status', () => {
expect(isTerminal('failed')).toBe(true);
});
it('should return true for cancelled status', () => {
expect(isTerminal('cancelled')).toBe(true);
});
it('should return false for working status', () => {
expect(isTerminal('working')).toBe(false);
});
it('should return false for input_required status', () => {
expect(isTerminal('input_required')).toBe(false);
});
});
});
describe('Task Schema Validation', () => {
it('should validate task with ttl field', () => {
const createdAt = new Date().toISOString();
const task: Task = {
taskId: 'test-123',
status: 'working',
ttl: 60_000,
createdAt,
lastUpdatedAt: createdAt,
pollInterval: 1000
};
expect(task.ttl).toBe(60_000);
expect(task.createdAt).toBeDefined();
expect(typeof task.createdAt).toBe('string');
});
it('should validate task with null ttl', () => {
const createdAt = new Date().toISOString();
const task: Task = {
taskId: 'test-456',
status: 'completed',
ttl: null,
createdAt,
lastUpdatedAt: createdAt
};
expect(task.ttl).toBeNull();
});
it('should validate task with statusMessage field', () => {
const createdAt = new Date().toISOString();
const task: Task = {
taskId: 'test-789',
status: 'failed',
ttl: null,
createdAt,
lastUpdatedAt: createdAt,
statusMessage: 'Operation failed due to timeout'
};
expect(task.statusMessage).toBe('Operation failed due to timeout');
});
it('should validate task with createdAt in ISO 8601 format', () => {
const now = new Date();
const createdAt = now.toISOString();
const task: Task = {
taskId: 'test-iso',
status: 'working',
ttl: 30_000,
createdAt,
lastUpdatedAt: createdAt
};
expect(task.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
expect(new Date(task.createdAt).getTime()).toBe(now.getTime());
});
it('should validate task with lastUpdatedAt in ISO 8601 format', () => {
const now = new Date();
const createdAt = now.toISOString();
const task: Task = {
taskId: 'test-iso',
status: 'working',
ttl: 30_000,
createdAt,
lastUpdatedAt: createdAt
};
expect(task.lastUpdatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
it('should validate all task statuses', () => {
const statuses: Task['status'][] = ['working', 'input_required', 'completed', 'failed', 'cancelled'];
const createdAt = new Date().toISOString();
for (const status of statuses) {
const task: Task = {
taskId: `test-${status}`,
status,
ttl: null,
createdAt,
lastUpdatedAt: createdAt
};
expect(task.status).toBe(status);
}
});
});
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);
});
});

View file

@ -1,129 +0,0 @@
import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createInMemoryTaskEnvironment } from '../../helpers/mcp.js';
describe('Task Listing with Pagination', () => {
let client: Awaited<ReturnType<typeof createInMemoryTaskEnvironment>>['client'];
let server: Awaited<ReturnType<typeof createInMemoryTaskEnvironment>>['server'];
let taskStore: Awaited<ReturnType<typeof createInMemoryTaskEnvironment>>['taskStore'];
beforeEach(async () => {
const env = await createInMemoryTaskEnvironment();
client = env.client;
server = env.server;
taskStore = env.taskStore;
});
afterEach(async () => {
taskStore.cleanup();
await client.close();
await server.close();
});
it('should return empty list when no tasks exist', async () => {
const result = await client.experimental.tasks.listTasks();
expect(result.tasks).toEqual([]);
expect(result.nextCursor).toBeUndefined();
});
it('should return all tasks when less than page size', async () => {
// Create 3 tasks
for (let i = 0; i < 3; i++) {
await taskStore.createTask({}, i, {
method: 'tools/call',
params: { name: 'test-tool' }
});
}
const result = await client.experimental.tasks.listTasks();
expect(result.tasks).toHaveLength(3);
expect(result.nextCursor).toBeUndefined();
});
it('should paginate when more than page size exists', async () => {
// Create 15 tasks (page size is 10 in InMemoryTaskStore)
for (let i = 0; i < 15; i++) {
await taskStore.createTask({}, i, {
method: 'tools/call',
params: { name: 'test-tool' }
});
}
// Get first page
const page1 = await client.experimental.tasks.listTasks();
expect(page1.tasks).toHaveLength(10);
expect(page1.nextCursor).toBeDefined();
// Get second page using cursor
const page2 = await client.experimental.tasks.listTasks(page1.nextCursor);
expect(page2.tasks).toHaveLength(5);
expect(page2.nextCursor).toBeUndefined();
});
it('should treat cursor as opaque token', async () => {
// Create 5 tasks
for (let i = 0; i < 5; i++) {
await taskStore.createTask({}, i, {
method: 'tools/call',
params: { name: 'test-tool' }
});
}
// Get all tasks to get a valid cursor
const allTasks = taskStore.getAllTasks();
const validCursor = allTasks[2]!.taskId;
// Use the cursor - should work even though we don't know its internal structure
const result = await client.experimental.tasks.listTasks(validCursor);
expect(result.tasks).toHaveLength(2);
});
it('should return error code -32602 for invalid cursor', async () => {
await taskStore.createTask({}, 1, {
method: 'tools/call',
params: { name: 'test-tool' }
});
// Try to use an invalid cursor - should return -32602 (Invalid params) per MCP spec
await expect(client.experimental.tasks.listTasks('invalid-cursor')).rejects.toSatisfy((error: ProtocolError) => {
expect(error).toBeInstanceOf(ProtocolError);
expect(error.code).toBe(ProtocolErrorCode.InvalidParams);
expect(error.message).toContain('Invalid cursor');
return true;
});
});
it('should ensure tasks accessible via tasks/get are also accessible via tasks/list', async () => {
// Create a task
const task = await taskStore.createTask({}, 1, {
method: 'tools/call',
params: { name: 'test-tool' }
});
// Verify it's accessible via tasks/get
const getResult = await client.experimental.tasks.getTask(task.taskId);
expect(getResult.taskId).toBe(task.taskId);
// Verify it's also accessible via tasks/list
const listResult = await client.experimental.tasks.listTasks();
expect(listResult.tasks).toHaveLength(1);
expect(listResult.tasks[0]!.taskId).toBe(task.taskId);
});
it('should not include related-task metadata in list response', async () => {
// Create a task
await taskStore.createTask({}, 1, {
method: 'tools/call',
params: { name: 'test-tool' }
});
const result = await client.experimental.tasks.listTasks();
// The response should have _meta but not include related-task metadata
expect(result._meta).toBeDefined();
expect(result._meta?.['io.modelcontextprotocol/related-task']).toBeUndefined();
});
});

View file

@ -1,70 +0,0 @@
import { Client } from '@modelcontextprotocol/client';
import { InMemoryTransport } from '@modelcontextprotocol/core';
import type { ClientCapabilities, ServerCapabilities } from '@modelcontextprotocol/server';
import { InMemoryTaskMessageQueue, InMemoryTaskStore, Server } from '@modelcontextprotocol/server';
export interface InMemoryTaskEnvironment {
client: Client;
server: Server;
taskStore: InMemoryTaskStore;
clientTransport: InMemoryTransport;
serverTransport: InMemoryTransport;
}
export async function createInMemoryTaskEnvironment(options?: {
clientCapabilities?: ClientCapabilities;
serverCapabilities?: ServerCapabilities;
}): Promise<InMemoryTaskEnvironment> {
const taskStore = new InMemoryTaskStore();
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: options?.clientCapabilities ?? {
tasks: {
list: {},
requests: {
tools: {
call: {}
}
}
}
}
}
);
const server = new Server(
{
name: 'test-server',
version: '1.0.0'
},
{
capabilities: options?.serverCapabilities ?? {
tasks: {
list: {},
requests: {
tools: {
call: {}
}
},
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
}
}
);
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
return {
client,
server,
taskStore,
clientTransport,
serverTransport
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,10 @@
import { Client } from '@modelcontextprotocol/client';
import type { CallToolResult, Notification, TextContent } from '@modelcontextprotocol/core';
import {
getDisplayName,
InMemoryTaskStore,
InMemoryTransport,
ProtocolErrorCode,
UriTemplate,
UrlElicitationRequiredError
} from '@modelcontextprotocol/core';
import type { Notification, TextContent } from '@modelcontextprotocol/core';
import { getDisplayName, InMemoryTransport, ProtocolErrorCode, UriTemplate, UrlElicitationRequiredError } from '@modelcontextprotocol/core';
import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import * as z from 'zod/v4';
function createLatch() {
let latch = false;
const waitForLatch = async () => {
while (!latch) {
await new Promise(resolve => setTimeout(resolve, 0));
}
};
return {
releaseLatch: () => {
latch = true;
},
waitForLatch
};
}
describe('Zod v4', () => {
describe('McpServer', () => {
/***
@ -2019,146 +1996,6 @@ describe('Zod v4', () => {
expect(result.tools[0]!._meta).toBeUndefined();
});
test('should include execution field in listTools response when tool has execution settings', async () => {
const taskStore = new InMemoryTaskStore();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client({
name: 'test client',
version: '1.0'
});
// Register a tool with execution.taskSupport
mcpServer.experimental.tasks.registerToolTask(
'task-tool',
{
description: 'A tool with task support',
inputSchema: z.object({ input: z.string() }),
execution: {
taskSupport: 'required'
}
},
{
createTask: async (_args, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000 });
return { task };
},
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) throw new Error('Task not found');
return task;
},
getTaskResult: async (_args, ctx) => {
return (await ctx.task.store.getTaskResult(ctx.task.id)) as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
const result = await client.request({ method: 'tools/list' });
expect(result.tools).toHaveLength(1);
expect(result.tools[0]!.name).toBe('task-tool');
expect(result.tools[0]!.execution).toEqual({
taskSupport: 'required'
});
taskStore.cleanup();
});
test('should include execution field with taskSupport optional in listTools response', async () => {
const taskStore = new InMemoryTaskStore();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client({
name: 'test client',
version: '1.0'
});
// Register a tool with execution.taskSupport optional
mcpServer.experimental.tasks.registerToolTask(
'optional-task-tool',
{
description: 'A tool with optional task support',
inputSchema: z.object({ input: z.string() }),
execution: {
taskSupport: 'optional'
}
},
{
createTask: async (_args, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000 });
return { task };
},
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) throw new Error('Task not found');
return task;
},
getTaskResult: async (_args, ctx) => {
return (await ctx.task.store.getTaskResult(ctx.task.id)) as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
const result = await client.request({ method: 'tools/list' });
expect(result.tools).toHaveLength(1);
expect(result.tools[0]!.name).toBe('optional-task-tool');
expect(result.tools[0]!.execution).toEqual({
taskSupport: 'optional'
});
taskStore.cleanup();
});
test('should validate tool names according to SEP specification', () => {
// Create a new server instance for this test
const testServer = new McpServer({
@ -6445,598 +6282,10 @@ describe('Zod v4', () => {
});
});
describe('Tool-level task hints with automatic polling wrapper', () => {
test('should return error for tool with taskSupport "required" called without task augmentation', async () => {
const taskStore = new InMemoryTaskStore();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client(
{
name: 'test client',
version: '1.0'
},
{
capabilities: {
tasks: {
requests: {
tools: {
call: {}
}
}
}
}
}
);
// Register a task-based tool with taskSupport "required"
mcpServer.experimental.tasks.registerToolTask(
'long-running-task',
{
description: 'A long running task',
inputSchema: z.object({
input: z.string()
}),
execution: {
taskSupport: 'required'
}
},
{
createTask: async ({ input }, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = ctx.task.store;
// Simulate async work
setTimeout(async () => {
await store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text' as const, text: `Processed: ${input}` }]
});
}, 200);
return { task };
},
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_input, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
// Call the tool WITHOUT task augmentation - should return error
const result = await client.callTool({
name: 'long-running-task',
arguments: { input: 'test data' }
});
// Should receive error result
expect(result.isError).toBe(true);
const content = result.content as TextContent[];
expect(content[0]!.text).toContain('requires task augmentation');
taskStore.cleanup();
});
test('should automatically poll and return CallToolResult for tool with taskSupport "optional" called without task augmentation', async () => {
const taskStore = new InMemoryTaskStore();
const { releaseLatch, waitForLatch } = createLatch();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client(
{
name: 'test client',
version: '1.0'
},
{
capabilities: {
tasks: {
requests: {
tools: {
call: {}
}
}
}
}
}
);
// Register a task-based tool with taskSupport "optional"
mcpServer.experimental.tasks.registerToolTask(
'optional-task',
{
description: 'An optional task',
inputSchema: z.object({
value: z.number()
}),
execution: {
taskSupport: 'optional'
}
},
{
createTask: async ({ value }, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = ctx.task.store;
// Simulate async work
setTimeout(async () => {
await store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text' as const, text: `Result: ${value * 2}` }]
});
releaseLatch();
}, 150);
return { task };
},
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_value, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
// Call the tool WITHOUT task augmentation
const result = await client.callTool({
name: 'optional-task',
arguments: { value: 21 }
});
// Should receive CallToolResult directly, not CreateTaskResult
expect(result).toHaveProperty('content');
expect(result.content).toEqual([{ type: 'text' as const, text: 'Result: 42' }]);
expect(result).not.toHaveProperty('task');
// Wait for async operations to complete
await waitForLatch();
taskStore.cleanup();
});
test('should return CreateTaskResult when tool with taskSupport "required" is called WITH task augmentation', async () => {
const taskStore = new InMemoryTaskStore();
const { releaseLatch, waitForLatch } = createLatch();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client(
{
name: 'test client',
version: '1.0'
},
{
capabilities: {
tasks: {
requests: {
tools: {
call: {}
}
}
}
}
}
);
// Register a task-based tool with taskSupport "required"
mcpServer.experimental.tasks.registerToolTask(
'task-tool',
{
description: 'A task tool',
inputSchema: z.object({
data: z.string()
}),
execution: {
taskSupport: 'required'
}
},
{
createTask: async ({ data }, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = ctx.task.store;
// Simulate async work
setTimeout(async () => {
await store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text' as const, text: `Completed: ${data}` }]
});
releaseLatch();
}, 200);
return { task };
},
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_data, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
// Call the tool WITH task augmentation
const result = await client.request(
{
method: 'tools/call',
params: {
name: 'task-tool',
arguments: { data: 'test' },
task: { ttl: 60_000 }
}
},
z.object({
task: z.object({
taskId: z.string(),
status: z.string(),
ttl: z.union([z.number(), z.null()]),
createdAt: z.string(),
pollInterval: z.number().optional()
})
})
);
// Should receive CreateTaskResult with task field
expect(result).toHaveProperty('task');
expect(result.task).toHaveProperty('taskId');
expect(result.task.status).toBe('working');
// Wait for async operations to complete
await waitForLatch();
taskStore.cleanup();
});
test('should handle task failures during automatic polling', async () => {
const taskStore = new InMemoryTaskStore();
const { releaseLatch, waitForLatch } = createLatch();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client(
{
name: 'test client',
version: '1.0'
},
{
capabilities: {
tasks: {
requests: {
tools: {
call: {}
}
}
}
}
}
);
// Register a task-based tool that fails
mcpServer.experimental.tasks.registerToolTask(
'failing-task',
{
description: 'A failing task',
execution: {
taskSupport: 'optional'
}
},
{
createTask: async ctx => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = ctx.task.store;
// Simulate async failure
setTimeout(async () => {
await store.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text' as const, text: 'Error occurred' }],
isError: true
});
releaseLatch();
}, 150);
return { task };
},
getTask: async ctx => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async ctx => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
// Call the tool WITHOUT task augmentation
const result = await client.callTool({
name: 'failing-task',
arguments: {}
});
// Should receive the error result
expect(result).toHaveProperty('content');
expect(result.content).toEqual([{ type: 'text' as const, text: 'Error occurred' }]);
expect(result.isError).toBe(true);
// Wait for async operations to complete
await waitForLatch();
taskStore.cleanup();
});
test('should handle task cancellation during automatic polling', async () => {
const taskStore = new InMemoryTaskStore();
const { releaseLatch, waitForLatch } = createLatch();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
const client = new Client(
{
name: 'test client',
version: '1.0'
},
{
capabilities: {
tasks: {
requests: {
tools: {
call: {}
}
}
}
}
}
);
// Register a task-based tool that gets cancelled
mcpServer.experimental.tasks.registerToolTask(
'cancelled-task',
{
description: 'A task that gets cancelled',
execution: {
taskSupport: 'optional'
}
},
{
createTask: async ctx => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = ctx.task.store;
// Simulate async cancellation
setTimeout(async () => {
await store.updateTaskStatus(task.taskId, 'cancelled', 'Task was cancelled');
releaseLatch();
}, 150);
return { task };
},
getTask: async ctx => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async ctx => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
// Call the tool WITHOUT task augmentation
const result = await client.callTool({
name: 'cancelled-task',
arguments: {}
});
// Should receive an error since cancelled tasks don't have results
expect(result).toHaveProperty('content');
expect(result.content).toEqual([{ type: 'text' as const, text: expect.stringContaining('has no result stored') }]);
// Wait for async operations to complete
await waitForLatch();
taskStore.cleanup();
});
test('should raise error when registerToolTask is called with taskSupport "forbidden"', () => {
const taskStore = new InMemoryTaskStore();
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: {
call: {}
}
},
taskStore
}
}
}
);
// Attempt to register a task-based tool with taskSupport "forbidden" (cast to bypass type checking)
expect(() => {
mcpServer.experimental.tasks.registerToolTask(
'invalid-task',
{
description: 'A task with forbidden support',
inputSchema: z.object({
input: z.string()
}),
execution: {
taskSupport: 'forbidden' as unknown as 'required'
}
},
{
createTask: async (_args, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
return { task };
},
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_args, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
}).toThrow();
taskStore.cleanup();
});
});
// SEP-2663: `taskSupport: 'required'` enforcement and the automatic-polling wrapper
// depended on the client sending `params.task`. Under the server-directed model the
// tool handler decides to return `{resultType:'task', task}`; there is no per-tool
// augmentation to enforce and no wrapper. The deleted "should include execution field"
// tests registered tools via `experimental.tasks.registerToolTask`, which is removed;
// `Tool.execution` remains a spec field but no SDK registration path currently sets it.
});

File diff suppressed because it is too large Load diff