Skip to content

Commit 0e0769e

Browse files
authored
Merge branch 'main' into feat/security-hardening
2 parents 4134708 + 549dd02 commit 0e0769e

File tree

8 files changed

+120
-60
lines changed

8 files changed

+120
-60
lines changed

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/server/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const createServer: () => ServerFactoryResponse = () => {
4040
const taskStore = new InMemoryTaskStore();
4141
const taskMessageQueue = new InMemoryTaskMessageQueue();
4242

43+
let initializeTimeout: NodeJS.Timeout | null = null;
44+
4345
// Create the server
4446
const server = new McpServer(
4547
{
@@ -98,7 +100,7 @@ export const createServer: () => ServerFactoryResponse = () => {
98100
// This is delayed until after the `notifications/initialized` handler finishes,
99101
// otherwise, the request gets lost.
100102
const sessionId = server.server.transport?.sessionId;
101-
setTimeout(() => syncRoots(server, sessionId), 350);
103+
initializeTimeout = setTimeout(() => syncRoots(server, sessionId), 350);
102104
};
103105

104106
// Return the ServerFactoryResponse
@@ -110,6 +112,7 @@ export const createServer: () => ServerFactoryResponse = () => {
110112
stopSimulatedResourceUpdates(sessionId);
111113
// Clean up task store timers
112114
taskStore.cleanup();
115+
if (initializeTimeout) clearTimeout(initializeTimeout);
113116
},
114117
} satisfies ServerFactoryResponse;
115118
};

src/everything/server/roots.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,10 @@ export const syncRoots = async (server: McpServer, sessionId?: string) => {
6363
);
6464
}
6565
} catch (error) {
66-
await server.sendLoggingMessage(
67-
{
68-
level: "error",
69-
logger: "everything-server",
70-
data: `Failed to request roots from client: ${
71-
error instanceof Error ? error.message : String(error)
72-
}`,
73-
},
74-
sessionId
66+
console.error(
67+
`Failed to request roots from client ${sessionId}: ${
68+
error instanceof Error ? error.message : String(error)
69+
}`
7570
);
7671
}
7772
};

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)