Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix(server): convert mid-poll task errors and align config-error hand…
…ling

Addresses review from @bhosmer-ant on #1769:

- handleAutomaticTaskPolling: wrap getTask in try/catch and convert
  ProtocolError(InvalidParams) to InternalError when a task vanishes
  mid-poll. A task going missing during automatic polling is a
  server-side issue — the client didn't request a task.
- handleAutomaticTaskPolling: change plain Error('No task store...') to
  ProtocolError(InternalError) for consistency with the analogous
  taskSupport config check at L177.
- Update three tests to assert error codes (not just messages) using
  toMatchObject, matching the pattern at L1824.

Follow-up (non-blocking): test coverage for the auto-polling output
validation path added in 3f29f35.
  • Loading branch information
felixweinberger committed Mar 31, 2026
commit 4633cb17f04dcaff49ce7201359831cde5429688
18 changes: 16 additions & 2 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export class McpServer {
ctx: ServerContext
): Promise<CallToolResult> {
if (!ctx.task?.store) {
throw new Error('No task store provided for task-capable tool.');
throw new ProtocolError(ProtocolErrorCode.InternalError, 'No task store provided for task-capable tool.');
}

// Validate input and create task using the executor
Expand All @@ -324,7 +324,21 @@ export class McpServer {

while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const updatedTask = await ctx.task.store.getTask(taskId);
let updatedTask;
try {
updatedTask = await ctx.task.store.getTask(taskId);
} catch (error) {
// RequestTaskStore.getTask throws InvalidParams when the task is
// missing, but a task vanishing mid-poll is a server-side issue
// (the client didn't even ask for a task) — surface as InternalError.
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams) {
throw new ProtocolError(
ProtocolErrorCode.InternalError,
`Task ${taskId} vanished during automatic polling`
);
}
throw error;
}
if (!updatedTask) {
throw new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} not found during polling`);
}
Expand Down
15 changes: 12 additions & 3 deletions test/integration/test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,10 @@ describe('Zod v4', () => {
input: 'hello'
}
})
).rejects.toThrow('Output validation error: Tool test has an output schema but no structured content was provided');
).rejects.toMatchObject({
code: ProtocolErrorCode.InternalError,
message: expect.stringContaining('has an output schema but no structured content')
});
});
/***
* Test: Tool with Output Schema Must Provide Structured Content
Expand Down Expand Up @@ -1548,7 +1551,10 @@ describe('Zod v4', () => {
input: 'hello'
}
})
).rejects.toThrow(/Output validation error: Invalid structured content for tool test/);
).rejects.toMatchObject({
code: ProtocolErrorCode.InternalError,
message: expect.stringMatching(/Invalid structured content for tool test/)
});
});

/***
Expand Down Expand Up @@ -6522,7 +6528,10 @@ describe('Zod v4', () => {
name: 'long-running-task',
arguments: { input: 'test data' }
})
).rejects.toThrow(/requires task augmentation/);
).rejects.toMatchObject({
code: ProtocolErrorCode.InvalidParams,
message: expect.stringMatching(/requires task augmentation/)
});

taskStore.cleanup();
});
Expand Down
Loading