fix(everything): use relatedTask option to enable elicitation over HTTP (#3228)
This commit is contained in:
parent
4503e2d12b
commit
67a71f1b60
2 changed files with 99 additions and 20 deletions
|
|
@ -15,6 +15,7 @@ import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-re
|
||||||
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
|
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
|
||||||
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
|
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
|
||||||
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
|
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
|
||||||
|
import { registerSimulateResearchQueryTool } from '../tools/simulate-research-query.js';
|
||||||
|
|
||||||
// Helper to capture registered tool handlers
|
// Helper to capture registered tool handlers
|
||||||
function createMockServer() {
|
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', () => {
|
describe('gzip-file-as-resource', () => {
|
||||||
it('should compress data URI and return resource link', async () => {
|
it('should compress data URI and return resource link', async () => {
|
||||||
const registeredResources: any[] = [];
|
const registeredResources: any[] = [];
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,10 @@ const researchStates = new Map<string, ResearchState>();
|
||||||
/**
|
/**
|
||||||
* Runs the background research process.
|
* Runs the background research process.
|
||||||
* Updates task status as it progresses through stages.
|
* Updates task status as it progresses through stages.
|
||||||
* If clarification is needed, attempts elicitation via sendRequest.
|
* If clarification is needed, sends elicitation via sendRequest with relatedTask,
|
||||||
*
|
* which queues the request in the task message queue. The SDK delivers it through
|
||||||
* Note: Elicitation only works on STDIO transport. On HTTP transport,
|
* the tasks/result stream when the client calls tasks/result (per spec input_required flow).
|
||||||
* sendRequest will fail and the task will use a default interpretation.
|
* This works on all transports (STDIO, SSE, Streamable HTTP).
|
||||||
* Full HTTP support requires SDK PR #1210's elicitInputStream API.
|
|
||||||
*/
|
*/
|
||||||
async function runResearchProcess(
|
async function runResearchProcess(
|
||||||
taskId: string,
|
taskId: string,
|
||||||
|
|
@ -94,7 +93,7 @@ async function runResearchProcess(
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
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(
|
const elicitResult: ElicitResult = await sendRequest(
|
||||||
{
|
{
|
||||||
method: "elicitation/create",
|
method: "elicitation/create",
|
||||||
|
|
@ -115,7 +114,8 @@ async function runResearchProcess(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ElicitResultSchema
|
ElicitResultSchema,
|
||||||
|
{ relatedTask: { taskId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process elicitation response
|
// Process elicitation response
|
||||||
|
|
@ -129,14 +129,12 @@ async function runResearchProcess(
|
||||||
state.clarification = "User cancelled - using default interpretation";
|
state.clarification = "User cancelled - using default interpretation";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Elicitation failed (likely HTTP transport without streaming support)
|
// Elicitation failed - use default interpretation and continue
|
||||||
// Use default interpretation and continue - task should still complete
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`Elicitation failed for task ${taskId} (HTTP transport?):`,
|
`Elicitation failed for task ${taskId}:`,
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
);
|
);
|
||||||
state.clarification =
|
state.clarification = "technical (default - elicitation unavailable)";
|
||||||
"technical (default - elicitation unavailable on HTTP)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume with working status (spec SHOULD)
|
// Resume with working status (spec SHOULD)
|
||||||
|
|
@ -199,12 +197,8 @@ ${
|
||||||
When the query was ambiguous, the server sent an \`elicitation/create\` request
|
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.
|
to the client. The task status changed to \`input_required\` while awaiting user input.
|
||||||
${
|
${
|
||||||
state.clarification.includes("unavailable on HTTP")
|
state.clarification.includes("unavailable")
|
||||||
? `
|
? `**Note:** Elicitation failed and a default interpretation was used.`
|
||||||
**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.
|
|
||||||
`
|
|
||||||
: `After receiving clarification ("${state.clarification}"), the task resumed processing and completed.`
|
: `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
|
- \`statusMessage\` provides human-readable progress updates
|
||||||
- Tasks have TTL (time-to-live) for automatic cleanup
|
- Tasks have TTL (time-to-live) for automatic cleanup
|
||||||
- \`pollInterval\` suggests how often to check status
|
- \`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.*
|
*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);
|
researchStates.set(task.taskId, state);
|
||||||
|
|
||||||
// Start background research (don't await - runs asynchronously)
|
// 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(
|
runResearchProcess(
|
||||||
task.taskId,
|
task.taskId,
|
||||||
validatedArgs,
|
validatedArgs,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue