Skip to content

Commit c53a0f3

Browse files
olaservoclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 6556e33 commit c53a0f3

1 file changed

Lines changed: 80 additions & 90 deletions

File tree

src/everything/tools/simulate-research-query.ts

Lines changed: 80 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
GetTaskResult,
66
Task,
77
ElicitResultSchema,
8+
ServerRequest,
89
} from "@modelcontextprotocol/sdk/types.js";
910
import { CreateTaskResult } from "@modelcontextprotocol/sdk/experimental";
11+
import type { AnySchema, SchemaOutput } from "@modelcontextprotocol/sdk/server/zod-compat.js";
1012

1113
// Tool input schema
1214
const SimulateResearchQuerySchema = z.object({
@@ -36,7 +38,6 @@ interface ResearchState {
3638
ambiguous: boolean;
3739
currentStage: number;
3840
clarification?: string;
39-
waitingForClarification: boolean;
4041
completed: boolean;
4142
result?: CallToolResult;
4243
}
@@ -47,6 +48,7 @@ const researchStates = new Map<string, ResearchState>();
4748
/**
4849
* Runs the background research process.
4950
* Updates task status as it progresses through stages.
51+
* If clarification is needed, sends elicitation request directly.
5052
*/
5153
async function runResearchProcess(
5254
taskId: string,
@@ -62,7 +64,12 @@ async function runResearchProcess(
6264
status: "completed" | "failed",
6365
result: CallToolResult
6466
) => Promise<void>;
65-
}
67+
},
68+
sendRequest: <U extends AnySchema>(
69+
request: ServerRequest,
70+
resultSchema: U,
71+
options?: { timeout?: number }
72+
) => Promise<SchemaOutput<U>>
6673
): Promise<void> {
6774
const state = researchStates.get(taskId);
6875
if (!state) return;
@@ -79,14 +86,59 @@ async function runResearchProcess(
7986

8087
// At synthesis stage (index 2), check if clarification is needed
8188
if (i === 2 && state.ambiguous && !state.clarification) {
82-
state.waitingForClarification = true;
89+
// Update status to show we're requesting input
8390
await taskStore.updateTaskStatus(
8491
taskId,
8592
"input_required",
86-
`Found multiple interpretations for "${state.topic}". Please clarify your intent.`
93+
`Found multiple interpretations for "${state.topic}". Requesting clarification...`
94+
);
95+
96+
// Send elicitation directly and await response
97+
const elicitationResult = await sendRequest(
98+
{
99+
method: "elicitation/create",
100+
params: {
101+
message: `The research query "${state.topic}" could have multiple interpretations. Please clarify what you're looking for:`,
102+
requestedSchema: {
103+
type: "object",
104+
properties: {
105+
interpretation: {
106+
type: "string",
107+
title: "Clarification",
108+
description: "Which interpretation of the topic do you mean?",
109+
oneOf: getInterpretationsForTopic(state.topic),
110+
},
111+
},
112+
required: ["interpretation"],
113+
},
114+
},
115+
},
116+
ElicitResultSchema,
117+
{ timeout: 5 * 60 * 1000 /* 5 minutes */ }
118+
);
119+
120+
// Process elicitation response
121+
if (
122+
elicitationResult.action === "accept" &&
123+
elicitationResult.content
124+
) {
125+
state.clarification =
126+
(elicitationResult.content as { interpretation?: string })
127+
.interpretation || "User accepted without selection";
128+
} else if (elicitationResult.action === "decline") {
129+
state.clarification = "User declined - using default interpretation";
130+
} else {
131+
state.clarification = "User cancelled - using default interpretation";
132+
}
133+
134+
// Resume with working status
135+
await taskStore.updateTaskStatus(
136+
taskId,
137+
"working",
138+
`Received clarification: "${state.clarification}". Continuing...`
87139
);
88-
// Wait for clarification - the getTaskResult handler will resume this
89-
return;
140+
141+
// Continue processing (no return - just keep going through the loop)
90142
}
91143

92144
// Simulate work for this stage
@@ -131,17 +183,18 @@ This tool demonstrates MCP's task-based execution pattern for long-running opera
131183
3. Status progressed: \`working\` → ${state.clarification ? `\`input_required\` → \`working\` → ` : ""}\`completed\`
132184
4. Client calls \`tasks/result\` → Server returns this final result
133185
134-
${state.clarification ? `**input_required Flow:**
135-
When the query was ambiguous, the task paused with \`input_required\` status.
136-
The client called \`tasks/result\` prematurely, which triggered an elicitation
137-
request via the side-channel. After receiving clarification ("${state.clarification}"),
138-
the task resumed processing.
186+
${state.clarification ? `**Elicitation Flow:**
187+
When the query was ambiguous, the server sent an \`elicitation/create\` request
188+
directly to the client. The task status changed to \`input_required\` while
189+
awaiting user input. After receiving clarification ("${state.clarification}"),
190+
the task resumed processing and completed.
139191
` : ""}
140192
**Key Concepts:**
141193
- Tasks enable "call now, fetch later" patterns
142194
- \`statusMessage\` provides human-readable progress updates
143195
- Tasks have TTL (time-to-live) for automatic cleanup
144196
- \`pollInterval\` suggests how often to check status
197+
- Elicitation requests can be sent directly during task execution
145198
146199
*This is a simulated research report from the Everything MCP Server.*
147200
`;
@@ -178,7 +231,7 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => {
178231
description:
179232
"Simulates a deep research operation that gathers, analyzes, and synthesizes information. " +
180233
"Demonstrates MCP task-based operations with progress through multiple stages. " +
181-
"If 'ambiguous' is true and client supports elicitation, pauses for clarification (input_required status).",
234+
"If 'ambiguous' is true and client supports elicitation, sends an elicitation request for clarification.",
182235
inputSchema: SimulateResearchQuerySchema,
183236
execution: { taskSupport: "required" },
184237
},
@@ -200,20 +253,23 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => {
200253
topic: validatedArgs.topic,
201254
ambiguous: validatedArgs.ambiguous && clientSupportsElicitation,
202255
currentStage: 0,
203-
waitingForClarification: false,
204256
completed: false,
205257
};
206258
researchStates.set(task.taskId, state);
207259

208260
// Start background research (don't await - runs asynchronously)
209-
runResearchProcess(task.taskId, validatedArgs, extra.taskStore).catch(
210-
(error) => {
211-
console.error(`Research task ${task.taskId} failed:`, error);
212-
extra.taskStore
213-
.updateTaskStatus(task.taskId, "failed", String(error))
214-
.catch(console.error);
215-
}
216-
);
261+
// Pass sendRequest so elicitation can be sent directly from the background process
262+
runResearchProcess(
263+
task.taskId,
264+
validatedArgs,
265+
extra.taskStore,
266+
extra.sendRequest
267+
).catch((error) => {
268+
console.error(`Research task ${task.taskId} failed:`, error);
269+
extra.taskStore
270+
.updateTaskStatus(task.taskId, "failed", String(error))
271+
.catch(console.error);
272+
});
217273

218274
return { task };
219275
},
@@ -228,77 +284,11 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => {
228284
},
229285

230286
/**
231-
* Returns the task result, or handles input_required via elicitation side-channel.
287+
* Returns the task result.
288+
* Elicitation is now handled directly in the background process.
232289
*/
233290
getTaskResult: async (args, extra): Promise<CallToolResult> => {
234-
const task = await extra.taskStore.getTask(extra.taskId);
235-
const state = researchStates.get(extra.taskId);
236-
237-
// Handle input_required - use tasks/result as side-channel for elicitation
238-
if (task?.status === "input_required" && state?.waitingForClarification) {
239-
// Send elicitation request through the side-channel
240-
const elicitationResult = await extra.sendRequest(
241-
{
242-
method: "elicitation/create",
243-
params: {
244-
message: `The research query "${state.topic}" could have multiple interpretations. Please clarify what you're looking for:`,
245-
requestedSchema: {
246-
type: "object",
247-
properties: {
248-
interpretation: {
249-
type: "string",
250-
title: "Clarification",
251-
description: "Which interpretation of the topic do you mean?",
252-
oneOf: getInterpretationsForTopic(state.topic),
253-
},
254-
},
255-
required: ["interpretation"],
256-
},
257-
},
258-
},
259-
ElicitResultSchema,
260-
{ timeout: 5 * 60 * 1000 /* 5 minutes */ }
261-
);
262-
263-
// Process elicitation response
264-
if (
265-
elicitationResult.action === "accept" &&
266-
elicitationResult.content
267-
) {
268-
state.clarification =
269-
(elicitationResult.content as { interpretation?: string })
270-
.interpretation || "User accepted without selection";
271-
} else if (elicitationResult.action === "decline") {
272-
state.clarification = "User declined - using default interpretation";
273-
} else {
274-
state.clarification = "User cancelled - using default interpretation";
275-
}
276-
277-
state.waitingForClarification = false;
278-
279-
// Resume background processing from current stage
280-
runResearchProcess(extra.taskId, {
281-
topic: state.topic,
282-
ambiguous: false, // Don't ask again
283-
}, extra.taskStore).catch((error) => {
284-
console.error(`Research task ${extra.taskId} failed:`, error);
285-
extra.taskStore
286-
.updateTaskStatus(extra.taskId, "failed", String(error))
287-
.catch(console.error);
288-
});
289-
290-
// Return indication that work is resuming (client should poll again)
291-
return {
292-
content: [
293-
{
294-
type: "text",
295-
text: `Resuming research with clarification: "${state.clarification}"`,
296-
},
297-
],
298-
};
299-
}
300-
301-
// Normal case: return the stored result
291+
// Return the stored result
302292
const result = await extra.taskStore.getTaskResult(extra.taskId);
303293

304294
// Clean up state

0 commit comments

Comments
 (0)