-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Expand file tree
/
Copy pathsimulate-research-query.ts
More file actions
339 lines (298 loc) · 11.2 KB
/
simulate-research-query.ts
File metadata and controls
339 lines (298 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
CallToolResult,
GetTaskResult,
Task,
ElicitResult,
ElicitResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { CreateTaskResult } from "@modelcontextprotocol/sdk/experimental/tasks";
// Tool input schema
const SimulateResearchQuerySchema = z.object({
topic: z.string().describe("The research topic to investigate"),
ambiguous: z
.boolean()
.default(false)
.describe(
"Simulate an ambiguous query that requires clarification (triggers input_required status)"
),
});
// Research stages
const STAGES = [
"Gathering sources",
"Analyzing content",
"Synthesizing findings",
"Generating report",
];
// Duration per stage in milliseconds
const STAGE_DURATION = 1000;
// Internal state for tracking research tasks
interface ResearchState {
topic: string;
ambiguous: boolean;
currentStage: number;
clarification?: string;
completed: boolean;
result?: CallToolResult;
}
// Map to store research state per task
const researchStates = new Map<string, ResearchState>();
/**
* Runs the background research process.
* Updates task status as it progresses through stages.
* 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,
args: z.infer<typeof SimulateResearchQuerySchema>,
taskStore: {
updateTaskStatus: (
taskId: string,
status: Task["status"],
message?: string
) => Promise<void>;
storeTaskResult: (
taskId: string,
status: "completed" | "failed",
result: CallToolResult
) => Promise<void>;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendRequest: any
): Promise<void> {
const state = researchStates.get(taskId);
if (!state) return;
// Process each stage
for (let i = state.currentStage; i < STAGES.length; i++) {
state.currentStage = i;
// Check if task was cancelled externally
if (state.completed) return;
// Update status message for current stage
await taskStore.updateTaskStatus(taskId, "working", `${STAGES[i]}...`);
// At synthesis stage (index 2), check if clarification is needed
if (i === 2 && state.ambiguous && !state.clarification) {
// Update status to show we're requesting input (spec SHOULD)
await taskStore.updateTaskStatus(
taskId,
"input_required",
`Found multiple interpretations for "${state.topic}". Requesting clarification...`
);
try {
// relatedTask queues elicitation via task message queue → delivered through tasks/result on all transports
const elicitResult: ElicitResult = 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,
{ relatedTask: { taskId } }
);
// Process elicitation response
if (elicitResult.action === "accept" && elicitResult.content) {
state.clarification =
(elicitResult.content as { interpretation?: string })
.interpretation || "User accepted without selection";
} else if (elicitResult.action === "decline") {
state.clarification = "User declined - using default interpretation";
} else {
state.clarification = "User cancelled - using default interpretation";
}
} catch (error) {
// Elicitation failed - use default interpretation and continue
console.warn(
`Elicitation failed for task ${taskId}:`,
error instanceof Error ? error.message : String(error)
);
state.clarification = "technical (default - elicitation unavailable)";
}
// Resume with working status (spec SHOULD)
await taskStore.updateTaskStatus(
taskId,
"working",
`Continuing with interpretation: "${state.clarification}"...`
);
// Continue processing (no return - just keep going through the loop)
}
// Simulate work for this stage
await new Promise((resolve) => setTimeout(resolve, STAGE_DURATION));
}
// All stages complete - generate result
state.completed = true;
const result = generateResearchReport(state);
state.result = result;
await taskStore.storeTaskResult(taskId, "completed", result);
}
/**
* Generates the final research report with educational content about tasks.
*/
function generateResearchReport(state: ResearchState): CallToolResult {
const topic = state.clarification
? `${state.topic} (${state.clarification})`
: state.topic;
const report = `# Research Report: ${topic}
## Research Parameters
- **Topic**: ${state.topic}
${state.clarification ? `- **Clarification**: ${state.clarification}` : ""}
## Synthesis
This research query was processed through ${STAGES.length} stages:
${STAGES.map((s, i) => `- Stage ${i + 1}: ${s} ✓`).join("\n")}
---
## About This Demo (SEP-1686: Tasks)
This tool demonstrates MCP's task-based execution pattern for long-running operations:
**Task Lifecycle Demonstrated:**
1. \`tools/call\` with \`task\` parameter → Server returns \`CreateTaskResult\` (not the final result)
2. Client polls \`tasks/get\` → Server returns current status and \`statusMessage\`
3. Status progressed: \`working\` → ${
state.clarification ? `\`input_required\` → \`working\` → ` : ""
}\`completed\`
4. Client calls \`tasks/result\` → Server returns this final result
${
state.clarification
? `**Elicitation Flow:**
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")
? `**Note:** Elicitation failed and a default interpretation was used.`
: `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 use \`relatedTask\` to queue via tasks/result (works on all transports)
*This is a simulated research report from the Everything MCP Server.*
`;
return {
content: [
{
type: "text",
text: report,
},
],
};
}
/**
* Registers the 'simulate-research-query' tool as a task-based tool.
*
* This tool demonstrates the MCP Tasks feature (SEP-1686) with a real-world scenario:
* a research tool that gathers and synthesizes information from multiple sources.
* If the query is ambiguous, it pauses to ask for clarification before completing.
*
* @param {McpServer} server - The McpServer instance where the tool will be registered.
*/
export const registerSimulateResearchQueryTool = (server: McpServer) => {
// Check if client supports elicitation (needed for input_required flow)
const clientCapabilities = server.server.getClientCapabilities() || {};
const clientSupportsElicitation: boolean =
clientCapabilities.elicitation !== undefined;
server.experimental.tasks.registerToolTask(
"simulate-research-query",
{
title: "Simulate Research Query",
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, sends an elicitation request for clarification.",
inputSchema: SimulateResearchQuerySchema,
execution: { taskSupport: "required" },
},
{
/**
* Creates a new research task and starts background processing.
*/
createTask: async (args, extra): Promise<CreateTaskResult> => {
const validatedArgs = SimulateResearchQuerySchema.parse(args);
// Create the task in the store
const task = await extra.taskStore.createTask({
ttl: 300000, // 5 minutes
pollInterval: 1000,
});
// Initialize research state
const state: ResearchState = {
topic: validatedArgs.topic,
ambiguous: validatedArgs.ambiguous && clientSupportsElicitation,
currentStage: 0,
completed: false,
};
researchStates.set(task.taskId, state);
// Start background research (don't await - runs asynchronously)
// Pass sendRequest for elicitation (queued via task message queue, works on all transports)
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 };
},
/**
* Returns the current status of the research task.
*/
getTask: async (args, extra): Promise<GetTaskResult> => {
return await extra.taskStore.getTask(extra.taskId);
},
/**
* Returns the task result.
* Elicitation is now handled directly in the background process.
*/
getTaskResult: async (args, extra): Promise<CallToolResult> => {
// Return the stored result
const result = await extra.taskStore.getTaskResult(extra.taskId);
// Clean up state
researchStates.delete(extra.taskId);
return result as CallToolResult;
},
}
);
};
/**
* Returns contextual interpretation options based on the topic.
*/
function getInterpretationsForTopic(
topic: string
): Array<{ const: string; title: string }> {
const lowerTopic = topic.toLowerCase();
// Example: contextual interpretations for "python"
if (lowerTopic.includes("python")) {
return [
{ const: "programming", title: "Python programming language" },
{ const: "snake", title: "Python snake species" },
{ const: "comedy", title: "Monty Python comedy group" },
];
}
// Default generic interpretations
return [
{ const: "technical", title: "Technical/scientific perspective" },
{ const: "historical", title: "Historical perspective" },
{ const: "current", title: "Current events/news perspective" },
];
}