Skip to content

Commit 9ade571

Browse files
committed
Fix #3234 -
"Everything Server crashes when multiple clients reconnect" * In index.ts - added a variable to hold the initialize timeout - store the timeout in the oninitialized handler - clear the timeout in the cleanup callback * In roots.ts - In the catch block of syncRoots, log the error to the console via .error rather than attempting to send to the client because the most probable case here is that we don't have a connection. * In simulate-research-query.ts - remove redundant local variable in getTask * Everywhere else, prettier.
1 parent eedb060 commit 9ade571

6 files changed

Lines changed: 112 additions & 50 deletions

File tree

src/everything/docs/features.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,14 @@ Use `trigger-sampling-request-async` or `trigger-elicitation-request-async` to d
8989

9090
MCP Tasks are bidirectional - both server and client can be task executors:
9191

92-
| Direction | Request Type | Task Executor | Demo Tool |
93-
|-----------|--------------|---------------|-----------|
94-
| Client -> Server | `tools/call` | Server | `simulate-research-query` |
95-
| Server -> Client | `sampling/createMessage` | Client | `trigger-sampling-request-async` |
96-
| Server -> Client | `elicitation/create` | Client | `trigger-elicitation-request-async` |
92+
| Direction | Request Type | Task Executor | Demo Tool |
93+
| ---------------- | ------------------------ | ------------- | ----------------------------------- |
94+
| Client -> Server | `tools/call` | Server | `simulate-research-query` |
95+
| Server -> Client | `sampling/createMessage` | Client | `trigger-sampling-request-async` |
96+
| Server -> Client | `elicitation/create` | Client | `trigger-elicitation-request-async` |
9797

9898
For client-side tasks:
99+
99100
1. Server sends request with task metadata (e.g., `params.task.ttl`)
100101
2. Client creates task and returns `CreateTaskResult` with `taskId`
101102
3. Server polls `tasks/get` for status updates

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ async function runResearchProcess(
106106
interpretation: {
107107
type: "string",
108108
title: "Clarification",
109-
description: "Which interpretation of the topic do you mean?",
109+
description:
110+
"Which interpretation of the topic do you mean?",
110111
oneOf: getInterpretationsForTopic(state.topic),
111112
},
112113
},
@@ -187,18 +188,28 @@ This tool demonstrates MCP's task-based execution pattern for long-running opera
187188
**Task Lifecycle Demonstrated:**
188189
1. \`tools/call\` with \`task\` parameter → Server returns \`CreateTaskResult\` (not the final result)
189190
2. Client polls \`tasks/get\` → Server returns current status and \`statusMessage\`
190-
3. Status progressed: \`working\` → ${state.clarification ? `\`input_required\` → \`working\` → ` : ""}\`completed\`
191+
3. Status progressed: \`working\` → ${
192+
state.clarification ? `\`input_required\` → \`working\` → ` : ""
193+
}\`completed\`
191194
4. Client calls \`tasks/result\` → Server returns this final result
192195
193-
${state.clarification ? `**Elicitation Flow:**
196+
${
197+
state.clarification
198+
? `**Elicitation Flow:**
194199
When the query was ambiguous, the server sent an \`elicitation/create\` request
195200
to the client. The task status changed to \`input_required\` while awaiting user input.
196-
${state.clarification.includes("unavailable on HTTP") ? `
201+
${
202+
state.clarification.includes("unavailable on HTTP")
203+
? `
197204
**Note:** Elicitation was skipped because this server is running over HTTP transport.
198205
The current SDK's \`sendRequest\` only works over STDIO. Full HTTP elicitation support
199206
requires SDK PR #1210's streaming \`elicitInputStream\` API.
200-
` : `After receiving clarification ("${state.clarification}"), the task resumed processing and completed.`}
201-
` : ""}
207+
`
208+
: `After receiving clarification ("${state.clarification}"), the task resumed processing and completed.`
209+
}
210+
`
211+
: ""
212+
}
202213
**Key Concepts:**
203214
- Tasks enable "call now, fetch later" patterns
204215
- \`statusMessage\` provides human-readable progress updates
@@ -288,9 +299,7 @@ export const registerSimulateResearchQueryTool = (server: McpServer) => {
288299
* Returns the current status of the research task.
289300
*/
290301
getTask: async (args, extra): Promise<GetTaskResult> => {
291-
const task = await extra.taskStore.getTask(extra.taskId);
292-
// The SDK's RequestTaskStore.getTask throws if not found, so task is always defined
293-
return task;
302+
return await extra.taskStore.getTask(extra.taskId);
294303
},
295304

296305
/**

src/everything/tools/trigger-elicitation-request-async.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,20 @@ const MAX_POLL_ATTEMPTS = 600;
3131
*
3232
* @param {McpServer} server - The McpServer instance where the tool will be registered.
3333
*/
34-
export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) => {
34+
export const registerTriggerElicitationRequestAsyncTool = (
35+
server: McpServer
36+
) => {
3537
// Check client capabilities
3638
const clientCapabilities = server.server.getClientCapabilities() || {};
3739

3840
// Client must support elicitation AND tasks.requests.elicitation
39-
const clientSupportsElicitation = clientCapabilities.elicitation !== undefined;
40-
const clientTasksCapability = clientCapabilities.tasks as {
41-
requests?: { elicitation?: { create?: object } };
42-
} | undefined;
41+
const clientSupportsElicitation =
42+
clientCapabilities.elicitation !== undefined;
43+
const clientTasksCapability = clientCapabilities.tasks as
44+
| {
45+
requests?: { elicitation?: { create?: object } };
46+
}
47+
| undefined;
4348
const clientSupportsAsyncElicitation =
4449
clientTasksCapability?.requests?.elicitation?.create !== undefined;
4550

@@ -56,7 +61,8 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
5661
task: {
5762
ttl: 600000, // 10 minutes (user input may take a while)
5863
},
59-
message: "Please provide inputs for the following fields (async task demo):",
64+
message:
65+
"Please provide inputs for the following fields (async task demo):",
6066
requestedSchema: {
6167
type: "object" as const,
6268
properties: {
@@ -107,14 +113,18 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
107113
);
108114

109115
// Check if client returned CreateTaskResult (has task object)
110-
const isTaskResult = 'task' in elicitResponse && elicitResponse.task;
116+
const isTaskResult = "task" in elicitResponse && elicitResponse.task;
111117
if (!isTaskResult) {
112118
// Client executed synchronously - return the direct response
113119
return {
114120
content: [
115121
{
116122
type: "text",
117-
text: `[SYNC] Client executed synchronously:\n${JSON.stringify(elicitResponse, null, 2)}`,
123+
text: `[SYNC] Client executed synchronously:\n${JSON.stringify(
124+
elicitResponse,
125+
null,
126+
2
127+
)}`,
118128
},
119129
],
120130
};
@@ -145,19 +155,27 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
145155
method: "tasks/get",
146156
params: { taskId },
147157
},
148-
z.object({
149-
status: z.string(),
150-
statusMessage: z.string().optional(),
151-
}).passthrough()
158+
z
159+
.object({
160+
status: z.string(),
161+
statusMessage: z.string().optional(),
162+
})
163+
.passthrough()
152164
);
153165

154166
taskStatus = pollResult.status;
155167
taskStatusMessage = pollResult.statusMessage;
156168

157169
// Only log status changes or every 10 polls to avoid spam
158-
if (attempts === 1 || attempts % 10 === 0 || taskStatus !== "input_required") {
170+
if (
171+
attempts === 1 ||
172+
attempts % 10 === 0 ||
173+
taskStatus !== "input_required"
174+
) {
159175
statusMessages.push(
160-
`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ""}`
176+
`Poll ${attempts}: ${taskStatus}${
177+
taskStatusMessage ? ` - ${taskStatusMessage}` : ""
178+
}`
161179
);
162180
}
163181
}
@@ -168,7 +186,9 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
168186
content: [
169187
{
170188
type: "text",
171-
text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join("\n")}`,
189+
text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join(
190+
"\n"
191+
)}`,
172192
},
173193
],
174194
};
@@ -180,7 +200,9 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
180200
content: [
181201
{
182202
type: "text",
183-
text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || "No message"}\n\nProgress:\n${statusMessages.join("\n")}`,
203+
text: `[${taskStatus.toUpperCase()}] ${
204+
taskStatusMessage || "No message"
205+
}\n\nProgress:\n${statusMessages.join("\n")}`,
184206
},
185207
],
186208
};
@@ -207,8 +229,10 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
207229
const userData = result.content as Record<string, unknown>;
208230
const lines = [];
209231
if (userData.name) lines.push(`- Name: ${userData.name}`);
210-
if (userData.favoriteColor) lines.push(`- Favorite Color: ${userData.favoriteColor}`);
211-
if (userData.agreeToTerms !== undefined) lines.push(`- Agreed to terms: ${userData.agreeToTerms}`);
232+
if (userData.favoriteColor)
233+
lines.push(`- Favorite Color: ${userData.favoriteColor}`);
234+
if (userData.agreeToTerms !== undefined)
235+
lines.push(`- Agreed to terms: ${userData.agreeToTerms}`);
212236

213237
content.push({
214238
type: "text",
@@ -229,7 +253,9 @@ export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) =>
229253
// Include progress and raw result for debugging
230254
content.push({
231255
type: "text",
232-
text: `\nProgress:\n${statusMessages.join("\n")}\n\nRaw result: ${JSON.stringify(result, null, 2)}`,
256+
text: `\nProgress:\n${statusMessages.join(
257+
"\n"
258+
)}\n\nRaw result: ${JSON.stringify(result, null, 2)}`,
233259
});
234260

235261
return { content };

src/everything/tools/trigger-elicitation-request.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { ElicitResultSchema, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import {
3+
ElicitResultSchema,
4+
CallToolResult,
5+
} from "@modelcontextprotocol/sdk/types.js";
36

47
// Tool configuration
58
const name = "trigger-elicitation-request";

src/everything/tools/trigger-sampling-request-async.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
4848

4949
// Client must support sampling AND tasks.requests.sampling
5050
const clientSupportsSampling = clientCapabilities.sampling !== undefined;
51-
const clientTasksCapability = clientCapabilities.tasks as {
52-
requests?: { sampling?: { createMessage?: object } };
53-
} | undefined;
51+
const clientTasksCapability = clientCapabilities.tasks as
52+
| {
53+
requests?: { sampling?: { createMessage?: object } };
54+
}
55+
| undefined;
5456
const clientSupportsAsyncSampling =
5557
clientTasksCapability?.requests?.sampling?.createMessage !== undefined;
5658

@@ -64,7 +66,9 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
6466

6567
// Create the sampling request WITH task metadata
6668
// The params.task field signals to the client that this should be executed as a task
67-
const request: CreateMessageRequest & { params: { task?: { ttl: number } } } = {
69+
const request: CreateMessageRequest & {
70+
params: { task?: { ttl: number } };
71+
} = {
6872
method: "sampling/createMessage",
6973
params: {
7074
task: {
@@ -112,14 +116,19 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
112116
);
113117

114118
// Check if client returned CreateTaskResult (has task object)
115-
const isTaskResult = 'task' in samplingResponse && samplingResponse.task;
119+
const isTaskResult =
120+
"task" in samplingResponse && samplingResponse.task;
116121
if (!isTaskResult) {
117122
// Client executed synchronously - return the direct response
118123
return {
119124
content: [
120125
{
121126
type: "text",
122-
text: `[SYNC] Client executed synchronously:\n${JSON.stringify(samplingResponse, null, 2)}`,
127+
text: `[SYNC] Client executed synchronously:\n${JSON.stringify(
128+
samplingResponse,
129+
null,
130+
2
131+
)}`,
123132
},
124133
],
125134
};
@@ -150,16 +159,20 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
150159
method: "tasks/get",
151160
params: { taskId },
152161
},
153-
z.object({
154-
status: z.string(),
155-
statusMessage: z.string().optional(),
156-
}).passthrough()
162+
z
163+
.object({
164+
status: z.string(),
165+
statusMessage: z.string().optional(),
166+
})
167+
.passthrough()
157168
);
158169

159170
taskStatus = pollResult.status;
160171
taskStatusMessage = pollResult.statusMessage;
161172
statusMessages.push(
162-
`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ""}`
173+
`Poll ${attempts}: ${taskStatus}${
174+
taskStatusMessage ? ` - ${taskStatusMessage}` : ""
175+
}`
163176
);
164177
}
165178

@@ -169,7 +182,9 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
169182
content: [
170183
{
171184
type: "text",
172-
text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join("\n")}`,
185+
text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join(
186+
"\n"
187+
)}`,
173188
},
174189
],
175190
};
@@ -181,7 +196,9 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
181196
content: [
182197
{
183198
type: "text",
184-
text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || "No message"}\n\nProgress:\n${statusMessages.join("\n")}`,
199+
text: `[${taskStatus.toUpperCase()}] ${
200+
taskStatusMessage || "No message"
201+
}\n\nProgress:\n${statusMessages.join("\n")}`,
185202
},
186203
],
187204
};
@@ -201,7 +218,9 @@ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
201218
content: [
202219
{
203220
type: "text",
204-
text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join("\n")}\n\n**Result:**\n${JSON.stringify(result, null, 2)}`,
221+
text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join(
222+
"\n"
223+
)}\n\n**Result:**\n${JSON.stringify(result, null, 2)}`,
205224
},
206225
],
207226
};

src/everything/transports/streamableHttp.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { StreamableHTTPServerTransport, EventStore } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1+
import {
2+
StreamableHTTPServerTransport,
3+
EventStore,
4+
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
25
import express, { Request, Response } from "express";
36
import { createServer } from "../server/index.js";
47
import { randomUUID } from "node:crypto";
58
import cors from "cors";
69

710
// Simple in-memory event store for SSE resumability
811
class InMemoryEventStore implements EventStore {
9-
private events: Map<string, { streamId: string; message: unknown }> = new Map();
12+
private events: Map<string, { streamId: string; message: unknown }> =
13+
new Map();
1014

1115
async storeEvent(streamId: string, message: unknown): Promise<string> {
1216
const eventId = randomUUID();

0 commit comments

Comments
 (0)