fix(everything): use relatedTask option to enable elicitation over HTTP (#3228)

This commit is contained in:
dhalahayeu 2026-04-04 09:12:45 +02:00 • committed by Dzmitryi Halahayeu
commit 67a71f1b60
2 changed files with 99 additions and 20 deletions

View file

@ -15,6 +15,7 @@ import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-re
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
import { registerSimulateResearchQueryTool } from '../tools/simulate-research-query.js';
// Helper to capture registered tool handlers
function createMockServer() {
@ -738,6 +739,90 @@ describe('Tools', () => {
});
});
describe('simulate-research-query', () => {
function createMockServerWithTasks() {
const taskHandlers: Record<string, any> = {};
const mockServer = {
experimental: {
tasks: {
registerToolTask: vi.fn((_name: string, _config: any, handler: any) => {
Object.assign(taskHandlers, handler);
}),
},
},
server: { getClientCapabilities: vi.fn(() => ({ elicitation: {} })) },
} as unknown as McpServer;
return { mockServer, taskHandlers };
}
function createMockTaskStore(taskId: string) {
return {
createTask: vi.fn().mockResolvedValue({
taskId,
status: 'working',
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
ttl: 300000,
pollInterval: 1000,
}),
updateTaskStatus: vi.fn().mockResolvedValue(undefined),
storeTaskResult: vi.fn().mockResolvedValue(undefined),
getTask: vi.fn(),
getTaskResult: vi.fn(),
};
}
it('should pass relatedTask to sendRequest when elicitation is triggered', async () => {
vi.useFakeTimers();
const { mockServer, taskHandlers } = createMockServerWithTasks();
registerSimulateResearchQueryTool(mockServer);
const mockTaskStore = createMockTaskStore('task-abc');
const mockSendRequest = vi.fn().mockResolvedValue({
action: 'accept',
content: { interpretation: 'technical' },
});
await taskHandlers.createTask(
{ topic: 'python', ambiguous: true },
{ taskStore: mockTaskStore, sendRequest: mockSendRequest }
);
await vi.runAllTimersAsync();
vi.useRealTimers();
expect(mockSendRequest).toHaveBeenCalledWith(
expect.objectContaining({ method: 'elicitation/create' }),
expect.anything(),
expect.objectContaining({ relatedTask: { taskId: 'task-abc' } })
);
});
it('should complete without elicitation for non-ambiguous query', async () => {
vi.useFakeTimers();
const { mockServer, taskHandlers } = createMockServerWithTasks();
registerSimulateResearchQueryTool(mockServer);
const mockTaskStore = createMockTaskStore('task-def');
const mockSendRequest = vi.fn();
await taskHandlers.createTask(
{ topic: 'python', ambiguous: false },
{ taskStore: mockTaskStore, sendRequest: mockSendRequest }
);
await vi.runAllTimersAsync();
vi.useRealTimers();
expect(mockSendRequest).not.toHaveBeenCalled();
expect(mockTaskStore.storeTaskResult).toHaveBeenCalledWith(
'task-def', 'completed', expect.anything()
);
});
});
describe('gzip-file-as-resource', () => {
it('should compress data URI and return resource link', async () => {
const registeredResources: any[] = [];

View file

@ -47,11 +47,10 @@ const researchStates = new Map<string, ResearchState>();
/**
* Runs the background research process.
* Updates task status as it progresses through stages.
* If clarification is needed, attempts elicitation via sendRequest.
*
* Note: Elicitation only works on STDIO transport. On HTTP transport,
* sendRequest will fail and the task will use a default interpretation.
* Full HTTP support requires SDK PR #1210's elicitInputStream API.
* If clarification is needed, sends elicitation via sendRequest with relatedTask,
* which queues the request in the task message queue. The SDK delivers it through
* the tasks/result stream when the client calls tasks/result (per spec input_required flow).
* This works on all transports (STDIO, SSE, Streamable HTTP).
*/
async function runResearchProcess(
taskId: string,
@ -94,7 +93,7 @@ async function runResearchProcess(
);
try {
// Try elicitation via sendRequest (works on STDIO, fails on HTTP)
// relatedTask queues elicitation via task message queue → delivered through tasks/result on all transports
const elicitResult: ElicitResult = await sendRequest(
{
method: "elicitation/create",
@ -115,7 +114,8 @@ async function runResearchProcess(
},
},
},
ElicitResultSchema
ElicitResultSchema,
{ relatedTask: { taskId } }
);
// Process elicitation response
@ -129,14 +129,12 @@ async function runResearchProcess(
state.clarification = "User cancelled - using default interpretation";
}
} catch (error) {
// Elicitation failed (likely HTTP transport without streaming support)
// Use default interpretation and continue - task should still complete
// Elicitation failed - use default interpretation and continue
console.warn(
`Elicitation failed for task ${taskId} (HTTP transport?):`,
`Elicitation failed for task ${taskId}:`,
error instanceof Error ? error.message : String(error)
);
state.clarification =
"technical (default - elicitation unavailable on HTTP)";
state.clarification = "technical (default - elicitation unavailable)";
}
// Resume with working status (spec SHOULD)
@ -199,12 +197,8 @@ ${
When the query was ambiguous, the server sent an \`elicitation/create\` request
to the client. The task status changed to \`input_required\` while awaiting user input.
${
state.clarification.includes("unavailable on HTTP")
? `
**Note:** Elicitation was skipped because this server is running over HTTP transport.
The current SDK's \`sendRequest\` only works over STDIO. Full HTTP elicitation support
requires SDK PR #1210's streaming \`elicitInputStream\` API.
`
state.clarification.includes("unavailable")
? `**Note:** Elicitation failed and a default interpretation was used.`
: `After receiving clarification ("${state.clarification}"), the task resumed processing and completed.`
}
`
@ -215,7 +209,7 @@ requires SDK PR #1210's streaming \`elicitInputStream\` API.
- \`statusMessage\` provides human-readable progress updates
- Tasks have TTL (time-to-live) for automatic cleanup
- \`pollInterval\` suggests how often to check status
- Elicitation requests can be sent directly during task execution
- Elicitation requests use \`relatedTask\` to queue via tasks/result (works on all transports)
*This is a simulated research report from the Everything MCP Server.*
`;
@ -279,7 +273,7 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => {
researchStates.set(task.taskId, state);
// Start background research (don't await - runs asynchronously)
// Pass sendRequest for elicitation (works on STDIO, gracefully degrades on HTTP)
// Pass sendRequest for elicitation (queued via task message queue, works on all transports)
runResearchProcess(
task.taskId,
validatedArgs,