From c53a0f3799c8fe0bb2004d15fbd3214f1f8451e0 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 17 Jan 2026 17:42:26 -0700 Subject: [PATCH] fix(everything): send elicitation directly from background task Instead of waiting for the client to call tasks/result to trigger elicitation, the server now sends elicitation/create directly from the background process using sendRequest. This simplifies the flow: - Server sends elicitation proactively when clarification is needed - Client receives and handles it via existing elicitation handler - Task resumes and completes after receiving the response - Client's polling sees completed status This approach avoids requiring the client to detect input_required status and call tasks/result as a side-channel. Co-Authored-By: Claude Opus 4.5 --- .../tools/simulate-research-query.ts | 170 +++++++++--------- 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/src/everything/tools/simulate-research-query.ts b/src/everything/tools/simulate-research-query.ts index 3afb8a5e..7853b7c4 100644 --- a/src/everything/tools/simulate-research-query.ts +++ b/src/everything/tools/simulate-research-query.ts @@ -5,8 +5,10 @@ import { GetTaskResult, Task, ElicitResultSchema, + ServerRequest, } from "@modelcontextprotocol/sdk/types.js"; import { CreateTaskResult } from "@modelcontextprotocol/sdk/experimental"; +import type { AnySchema, SchemaOutput } from "@modelcontextprotocol/sdk/server/zod-compat.js"; // Tool input schema const SimulateResearchQuerySchema = z.object({ @@ -36,7 +38,6 @@ interface ResearchState { ambiguous: boolean; currentStage: number; clarification?: string; - waitingForClarification: boolean; completed: boolean; result?: CallToolResult; } @@ -47,6 +48,7 @@ const researchStates = new Map(); /** * Runs the background research process. * Updates task status as it progresses through stages. + * If clarification is needed, sends elicitation request directly. */ async function runResearchProcess( taskId: string, @@ -62,7 +64,12 @@ async function runResearchProcess( status: "completed" | "failed", result: CallToolResult ) => Promise; - } + }, + sendRequest: ( + request: ServerRequest, + resultSchema: U, + options?: { timeout?: number } + ) => Promise> ): Promise { const state = researchStates.get(taskId); if (!state) return; @@ -79,14 +86,59 @@ async function runResearchProcess( // At synthesis stage (index 2), check if clarification is needed if (i === 2 && state.ambiguous && !state.clarification) { - state.waitingForClarification = true; + // Update status to show we're requesting input await taskStore.updateTaskStatus( taskId, "input_required", - `Found multiple interpretations for "${state.topic}". Please clarify your intent.` + `Found multiple interpretations for "${state.topic}". Requesting clarification...` ); - // Wait for clarification - the getTaskResult handler will resume this - return; + + // Send elicitation directly and await response + const elicitationResult = await sendRequest( + { + method: "elicitation/create", + params: { + message: `The research query "${state.topic}" could have multiple interpretations. Please clarify what you're looking for:`, + requestedSchema: { + type: "object", + properties: { + interpretation: { + type: "string", + title: "Clarification", + description: "Which interpretation of the topic do you mean?", + oneOf: getInterpretationsForTopic(state.topic), + }, + }, + required: ["interpretation"], + }, + }, + }, + ElicitResultSchema, + { timeout: 5 * 60 * 1000 /* 5 minutes */ } + ); + + // Process elicitation response + if ( + elicitationResult.action === "accept" && + elicitationResult.content + ) { + state.clarification = + (elicitationResult.content as { interpretation?: string }) + .interpretation || "User accepted without selection"; + } else if (elicitationResult.action === "decline") { + state.clarification = "User declined - using default interpretation"; + } else { + state.clarification = "User cancelled - using default interpretation"; + } + + // Resume with working status + await taskStore.updateTaskStatus( + taskId, + "working", + `Received clarification: "${state.clarification}". Continuing...` + ); + + // Continue processing (no return - just keep going through the loop) } // Simulate work for this stage @@ -131,17 +183,18 @@ This tool demonstrates MCP's task-based execution pattern for long-running opera 3. Status progressed: \`working\` → ${state.clarification ? `\`input_required\` → \`working\` → ` : ""}\`completed\` 4. Client calls \`tasks/result\` → Server returns this final result -${state.clarification ? `**input_required Flow:** -When the query was ambiguous, the task paused with \`input_required\` status. -The client called \`tasks/result\` prematurely, which triggered an elicitation -request via the side-channel. After receiving clarification ("${state.clarification}"), -the task resumed processing. +${state.clarification ? `**Elicitation Flow:** +When the query was ambiguous, the server sent an \`elicitation/create\` request +directly to the client. The task status changed to \`input_required\` while +awaiting user input. After receiving clarification ("${state.clarification}"), +the task resumed processing and completed. ` : ""} **Key Concepts:** - Tasks enable "call now, fetch later" patterns - \`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 *This is a simulated research report from the Everything MCP Server.* `; @@ -178,7 +231,7 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => { description: "Simulates a deep research operation that gathers, analyzes, and synthesizes information. " + "Demonstrates MCP task-based operations with progress through multiple stages. " + - "If 'ambiguous' is true and client supports elicitation, pauses for clarification (input_required status).", + "If 'ambiguous' is true and client supports elicitation, sends an elicitation request for clarification.", inputSchema: SimulateResearchQuerySchema, execution: { taskSupport: "required" }, }, @@ -200,20 +253,23 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => { topic: validatedArgs.topic, ambiguous: validatedArgs.ambiguous && clientSupportsElicitation, currentStage: 0, - waitingForClarification: false, completed: false, }; researchStates.set(task.taskId, state); // Start background research (don't await - runs asynchronously) - runResearchProcess(task.taskId, validatedArgs, extra.taskStore).catch( - (error) => { - console.error(`Research task ${task.taskId} failed:`, error); - extra.taskStore - .updateTaskStatus(task.taskId, "failed", String(error)) - .catch(console.error); - } - ); + // Pass sendRequest so elicitation can be sent directly from the background process + runResearchProcess( + task.taskId, + validatedArgs, + extra.taskStore, + extra.sendRequest + ).catch((error) => { + console.error(`Research task ${task.taskId} failed:`, error); + extra.taskStore + .updateTaskStatus(task.taskId, "failed", String(error)) + .catch(console.error); + }); return { task }; }, @@ -228,77 +284,11 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => { }, /** - * Returns the task result, or handles input_required via elicitation side-channel. + * Returns the task result. + * Elicitation is now handled directly in the background process. */ getTaskResult: async (args, extra): Promise => { - const task = await extra.taskStore.getTask(extra.taskId); - const state = researchStates.get(extra.taskId); - - // Handle input_required - use tasks/result as side-channel for elicitation - if (task?.status === "input_required" && state?.waitingForClarification) { - // Send elicitation request through the side-channel - const elicitationResult = await extra.sendRequest( - { - method: "elicitation/create", - params: { - message: `The research query "${state.topic}" could have multiple interpretations. Please clarify what you're looking for:`, - requestedSchema: { - type: "object", - properties: { - interpretation: { - type: "string", - title: "Clarification", - description: "Which interpretation of the topic do you mean?", - oneOf: getInterpretationsForTopic(state.topic), - }, - }, - required: ["interpretation"], - }, - }, - }, - ElicitResultSchema, - { timeout: 5 * 60 * 1000 /* 5 minutes */ } - ); - - // Process elicitation response - if ( - elicitationResult.action === "accept" && - elicitationResult.content - ) { - state.clarification = - (elicitationResult.content as { interpretation?: string }) - .interpretation || "User accepted without selection"; - } else if (elicitationResult.action === "decline") { - state.clarification = "User declined - using default interpretation"; - } else { - state.clarification = "User cancelled - using default interpretation"; - } - - state.waitingForClarification = false; - - // Resume background processing from current stage - runResearchProcess(extra.taskId, { - topic: state.topic, - ambiguous: false, // Don't ask again - }, extra.taskStore).catch((error) => { - console.error(`Research task ${extra.taskId} failed:`, error); - extra.taskStore - .updateTaskStatus(extra.taskId, "failed", String(error)) - .catch(console.error); - }); - - // Return indication that work is resuming (client should poll again) - return { - content: [ - { - type: "text", - text: `Resuming research with clarification: "${state.clarification}"`, - }, - ], - }; - } - - // Normal case: return the stored result + // Return the stored result const result = await extra.taskStore.getTaskResult(extra.taskId); // Clean up state