@modelcontextprotocol/server 2.0.0-alpha.3 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/{ajvProvider-Birb50r-.mjs → ajvProvider-BQMcjynJ.mjs} +952 -154
- package/dist/ajvProvider-BQMcjynJ.mjs.map +1 -0
- package/dist/{ajvProvider-DZ_siXcF.d.mts → ajvProvider-Dzgk80kq.d.mts} +58 -11
- package/dist/ajvProvider-Dzgk80kq.d.mts.map +1 -0
- package/dist/{cfWorkerProvider-BrJKpSFH.mjs → cfWorkerProvider-BDC2rVl3.mjs} +21 -5
- package/dist/cfWorkerProvider-BDC2rVl3.mjs.map +1 -0
- package/dist/{cfWorkerProvider-DUhk5Ewx.d.mts → cfWorkerProvider-DmvjVsvQ.d.mts} +13 -6
- package/dist/cfWorkerProvider-DmvjVsvQ.d.mts.map +1 -0
- package/dist/{transport-DMKhEchd.d.mts → createMcpHandler-Du3hjXvf.d.mts} +5283 -1559
- package/dist/createMcpHandler-Du3hjXvf.d.mts.map +1 -0
- package/dist/index.d.mts +167 -2015
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1238 -1281
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-JttQJlI9.mjs +9998 -0
- package/dist/mcp-JttQJlI9.mjs.map +1 -0
- package/dist/shimsNode.d.mts +1 -1
- package/dist/shimsNode.mjs +1 -1
- package/dist/shimsWorkerd.d.mts +1 -1
- package/dist/shimsWorkerd.mjs +1 -1
- package/dist/stdio.d.mts +61 -3
- package/dist/stdio.d.mts.map +1 -1
- package/dist/stdio.mjs +457 -2
- package/dist/stdio.mjs.map +1 -1
- package/dist/types-DBYdVs-n.d.mts +1099 -0
- package/dist/types-DBYdVs-n.d.mts.map +1 -0
- package/dist/validators/ajv.d.mts +1 -1
- package/dist/validators/ajv.mjs +1 -1
- package/dist/validators/cfWorker.d.mts +1 -1
- package/dist/validators/cfWorker.mjs +1 -1
- package/package.json +3 -6
- package/dist/ajvProvider-Birb50r-.mjs.map +0 -1
- package/dist/ajvProvider-DZ_siXcF.d.mts.map +0 -1
- package/dist/cfWorkerProvider-BrJKpSFH.mjs.map +0 -1
- package/dist/cfWorkerProvider-DUhk5Ewx.d.mts.map +0 -1
- package/dist/src-Pa1iAvsj.mjs +0 -3386
- package/dist/src-Pa1iAvsj.mjs.map +0 -1
- package/dist/transport-DMKhEchd.d.mts.map +0 -1
- package/dist/types-R2RTIcjk.d.mts +0 -66
- package/dist/types-R2RTIcjk.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,1358 +1,588 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as classifyInboundRequest, At as SdkErrorCode, B as isInitializedNotification, C as isSpecType, Ct as TRACEPARENT_META_KEY, D as inputResponse, Dt as checkResourceAllowed, E as inputRequired, Et as requiredClientCapabilitiesForRequest, F as validateMcpParamHeaders, Ft as isCompletable, G as isJSONRPCResponse, H as isJSONRPCErrorResponse, I as assertCompleteRequestPrompt, J as parseJSONRPCMessage, K as isJSONRPCResultResponse, L as assertCompleteRequestResourceTemplate, M as modernOnlyStrictRejection, Mt as OAuthError, N as validateStandardRequestHeaders, Nt as OAuthErrorCode, O as LADDER_ERROR_HTTP_STATUS, Ot as resourceUrlFromServerUrl, P as scanXMcpHeaderDeclarations, Pt as completable, Q as requestMetaOf, R as isCallToolResult, S as setNegotiatedProtocolVersion, St as SUPPORTED_PROTOCOL_VERSIONS, T as acceptedContent, Tt as missingClientCapabilities, U as isJSONRPCNotification, V as isInputRequiredResult, W as isJSONRPCRequest, Y as JSONRPCMessageSchema, _ as STDIO_DEFAULT_MAX_BUFFER_SIZE, _t as METHOD_NOT_FOUND, a as seedClientIdentityFromEnvelope, at as ProtocolErrorCode, b as getDisplayName, bt as RELATED_TASK_META_KEY, ct as CLIENT_CAPABILITIES_META_KEY, d as createServerNotifier, dt as INTERNAL_ERROR, et as MissingRequiredClientCapabilityError, f as fromJsonSchema$1, ft as INVALID_PARAMS, g as ReadBuffer, gt as LOG_LEVEL_META_KEY, h as createFetchWithInit, ht as LATEST_PROTOCOL_VERSION, i as installModernOnlyHandlers, it as UrlElicitationRequiredError, j as httpStatusForErrorCode, jt as SdkHttpError, kt as SdkError, l as createListenRouter, lt as CLIENT_INFO_META_KEY, m as UriTemplate, mt as JSONRPC_VERSION, n as ResourceTemplate, nt as ResourceNotFoundError, o as DEFAULT_LISTEN_KEEPALIVE_MS, ot as SUPPORTED_MODERN_PROTOCOL_VERSIONS, p as InMemoryTransport, pt as INVALID_REQUEST, q as isTaskAugmentedRequestParams, r as Server, rt as UnsupportedProtocolVersionError, s as DEFAULT_MAX_SUBSCRIPTIONS, st as BAGGAGE_META_KEY, t as McpServer, tt as ProtocolError, u as InMemoryServerEventBus, ut as DEFAULT_NEGOTIATED_PROTOCOL_VERSION, v as deserializeMessage, vt as PARSE_ERROR, w as specTypeSchemas, wt as TRACESTATE_META_KEY, x as DEFAULT_REQUEST_TIMEOUT_MSEC, xt as SUBSCRIPTION_ID_META_KEY, y as serializeMessage, yt as PROTOCOL_VERSION_META_KEY, z as isInitializeRequest } from "./mcp-JttQJlI9.mjs";
|
|
2
2
|
import { DefaultJsonSchemaValidator } from "@modelcontextprotocol/server/_shims";
|
|
3
3
|
|
|
4
|
-
//#region src/server/
|
|
5
|
-
const COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
|
|
4
|
+
//#region src/server/perRequestTransport.ts
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* ```ts source="./completable.examples.ts#completable_basicUsage"
|
|
11
|
-
* server.registerPrompt(
|
|
12
|
-
* 'review-code',
|
|
13
|
-
* {
|
|
14
|
-
* title: 'Code Review',
|
|
15
|
-
* argsSchema: z.object({
|
|
16
|
-
* language: completable(z.string().describe('Programming language'), value =>
|
|
17
|
-
* ['typescript', 'javascript', 'python', 'rust', 'go'].filter(lang => lang.startsWith(value))
|
|
18
|
-
* )
|
|
19
|
-
* })
|
|
20
|
-
* },
|
|
21
|
-
* ({ language }) => ({
|
|
22
|
-
* messages: [
|
|
23
|
-
* {
|
|
24
|
-
* role: 'user' as const,
|
|
25
|
-
* content: {
|
|
26
|
-
* type: 'text' as const,
|
|
27
|
-
* text: `Review this ${language} code.`
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
* ]
|
|
31
|
-
* })
|
|
32
|
-
* );
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @see {@linkcode server/mcp.McpServer.registerPrompt | McpServer.registerPrompt} for using completable schemas in prompt argument definitions
|
|
36
|
-
*/
|
|
37
|
-
function completable(schema, complete) {
|
|
38
|
-
Object.defineProperty(schema, COMPLETABLE_SYMBOL, {
|
|
39
|
-
value: { complete },
|
|
40
|
-
enumerable: false,
|
|
41
|
-
writable: false,
|
|
42
|
-
configurable: false
|
|
43
|
-
});
|
|
44
|
-
return schema;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Checks if a schema is completable (has completion metadata).
|
|
48
|
-
*/
|
|
49
|
-
function isCompletable(schema) {
|
|
50
|
-
return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Gets the completer callback from a completable schema, if it exists.
|
|
54
|
-
*/
|
|
55
|
-
function getCompleter(schema) {
|
|
56
|
-
return schema[COMPLETABLE_SYMBOL]?.complete;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/server/server.ts
|
|
61
|
-
/**
|
|
62
|
-
* An MCP server on top of a pluggable transport.
|
|
63
|
-
*
|
|
64
|
-
* This server will automatically respond to the initialization flow as initiated from the client.
|
|
65
|
-
*
|
|
66
|
-
* @deprecated Use {@linkcode server/mcp.McpServer | McpServer} instead for the high-level API. Only use `Server` for advanced use cases.
|
|
6
|
+
* The per-request micro-transport: a real, connected `Transport` whose whole
|
|
7
|
+
* lifetime is one HTTP exchange. See the module documentation for the
|
|
8
|
+
* response shapes it produces.
|
|
67
9
|
*/
|
|
68
|
-
var
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
oninitialized;
|
|
79
|
-
/**
|
|
80
|
-
* Initializes this server with the given name and version information.
|
|
81
|
-
*/
|
|
82
|
-
constructor(_serverInfo, options) {
|
|
83
|
-
super(options);
|
|
84
|
-
this._serverInfo = _serverInfo;
|
|
85
|
-
this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
|
|
86
|
-
this._instructions = options?.instructions;
|
|
87
|
-
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
|
|
88
|
-
this.setRequestHandler("initialize", (request) => this._oninitialize(request));
|
|
89
|
-
this.setNotificationHandler("notifications/initialized", () => this.oninitialized?.());
|
|
90
|
-
if (this._capabilities.logging) this._registerLoggingHandler();
|
|
91
|
-
}
|
|
10
|
+
var PerRequestHTTPServerTransport = class {
|
|
11
|
+
onclose;
|
|
12
|
+
onerror;
|
|
13
|
+
onmessage;
|
|
14
|
+
_classification;
|
|
15
|
+
_responseMode;
|
|
16
|
+
_started = false;
|
|
17
|
+
_used = false;
|
|
18
|
+
_closed = false;
|
|
19
|
+
_terminalDelivered = false;
|
|
92
20
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
21
|
+
* `true` only while the inbound message is being delivered synchronously
|
|
22
|
+
* to the connected protocol layer. The pre-handler gates (the era
|
|
23
|
+
* registry gate, the edge→instance handoff check, the missing-handler
|
|
24
|
+
* rejection) answer inside this window; request handlers always run
|
|
25
|
+
* after it (the protocol layer defers them to a microtask). An error
|
|
26
|
+
* sent inside the window is therefore ladder-originated, and an error
|
|
27
|
+
* sent after it is handler-produced.
|
|
98
28
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
29
|
+
_dispatchWindowOpen = false;
|
|
30
|
+
_requestId;
|
|
31
|
+
_deferredResponse;
|
|
32
|
+
_sse;
|
|
33
|
+
_abortCleanup;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this._classification = options.classification;
|
|
36
|
+
this._responseMode = options.responseMode ?? "auto";
|
|
107
37
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
...ctx,
|
|
112
|
-
mcpReq: {
|
|
113
|
-
...ctx.mcpReq,
|
|
114
|
-
log: (level, data, logger) => this.sendLoggingMessage({
|
|
115
|
-
level,
|
|
116
|
-
data,
|
|
117
|
-
logger
|
|
118
|
-
}),
|
|
119
|
-
elicitInput: (params, options) => this.elicitInput(params, options),
|
|
120
|
-
requestSampling: (params, options) => this.createMessage(params, options)
|
|
121
|
-
},
|
|
122
|
-
http: hasHttpInfo ? {
|
|
123
|
-
...ctx.http,
|
|
124
|
-
req: transportInfo?.request,
|
|
125
|
-
closeSSE: transportInfo?.closeSSEStream,
|
|
126
|
-
closeStandaloneSSE: transportInfo?.closeStandaloneSSEStream
|
|
127
|
-
} : void 0
|
|
128
|
-
};
|
|
38
|
+
async start() {
|
|
39
|
+
if (this._started) throw new Error("PerRequestHTTPServerTransport is already started");
|
|
40
|
+
this._started = true;
|
|
129
41
|
}
|
|
130
|
-
_loggingLevels = /* @__PURE__ */ new Map();
|
|
131
|
-
LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index]));
|
|
132
|
-
isMessageIgnored = (level, sessionId) => {
|
|
133
|
-
const currentLevel = this._loggingLevels.get(sessionId);
|
|
134
|
-
return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel) : false;
|
|
135
|
-
};
|
|
136
42
|
/**
|
|
137
|
-
*
|
|
43
|
+
* Serves the single exchange: delivers the classified message to the
|
|
44
|
+
* connected server instance and resolves with the HTTP response.
|
|
138
45
|
*
|
|
139
|
-
*
|
|
46
|
+
* Throws when called a second time (the transport is strictly
|
|
47
|
+
* single-use), or before a server has been connected to the transport.
|
|
48
|
+
* The returned promise rejects with a connection-closed error when the
|
|
49
|
+
* transport is closed before a response was produced (for example because
|
|
50
|
+
* the client disconnected).
|
|
140
51
|
*/
|
|
141
|
-
|
|
142
|
-
if (this.
|
|
143
|
-
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*/
|
|
151
|
-
_wrapHandler(method, handler) {
|
|
152
|
-
if (method !== "tools/call") return handler;
|
|
153
|
-
return async (request, ctx) => {
|
|
154
|
-
const validatedRequest = parseSchema(CallToolRequestSchema, request);
|
|
155
|
-
if (!validatedRequest.success) {
|
|
156
|
-
const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
|
157
|
-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
|
|
158
|
-
}
|
|
159
|
-
const validationResult = parseSchema(CallToolResultSchema, await handler(request, ctx));
|
|
160
|
-
if (!validationResult.success) {
|
|
161
|
-
const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
|
162
|
-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
|
|
163
|
-
}
|
|
164
|
-
return validationResult.data;
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
assertCapabilityForMethod(method) {
|
|
168
|
-
switch (method) {
|
|
169
|
-
case "sampling/createMessage":
|
|
170
|
-
if (!this._clientCapabilities?.sampling) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support sampling (required for ${method})`);
|
|
171
|
-
break;
|
|
172
|
-
case "elicitation/create":
|
|
173
|
-
if (!this._clientCapabilities?.elicitation) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support elicitation (required for ${method})`);
|
|
174
|
-
break;
|
|
175
|
-
case "roots/list":
|
|
176
|
-
if (!this._clientCapabilities?.roots) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support listing roots (required for ${method})`);
|
|
177
|
-
break;
|
|
178
|
-
case "ping": break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
assertNotificationCapability(method) {
|
|
182
|
-
switch (method) {
|
|
183
|
-
case "notifications/message":
|
|
184
|
-
if (!this._capabilities.logging) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
|
|
185
|
-
break;
|
|
186
|
-
case "notifications/resources/updated":
|
|
187
|
-
case "notifications/resources/list_changed":
|
|
188
|
-
if (!this._capabilities.resources) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support notifying about resources (required for ${method})`);
|
|
189
|
-
break;
|
|
190
|
-
case "notifications/tools/list_changed":
|
|
191
|
-
if (!this._capabilities.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support notifying of tool list changes (required for ${method})`);
|
|
192
|
-
break;
|
|
193
|
-
case "notifications/prompts/list_changed":
|
|
194
|
-
if (!this._capabilities.prompts) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support notifying of prompt list changes (required for ${method})`);
|
|
195
|
-
break;
|
|
196
|
-
case "notifications/elicitation/complete":
|
|
197
|
-
if (!this._clientCapabilities?.elicitation?.url) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support URL elicitation (required for ${method})`);
|
|
198
|
-
break;
|
|
199
|
-
case "notifications/cancelled": break;
|
|
200
|
-
case "notifications/progress": break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
assertRequestHandlerCapability(method) {
|
|
204
|
-
switch (method) {
|
|
205
|
-
case "completion/complete":
|
|
206
|
-
if (!this._capabilities.completions) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support completions (required for ${method})`);
|
|
207
|
-
break;
|
|
208
|
-
case "logging/setLevel":
|
|
209
|
-
if (!this._capabilities.logging) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
|
|
210
|
-
break;
|
|
211
|
-
case "prompts/get":
|
|
212
|
-
case "prompts/list":
|
|
213
|
-
if (!this._capabilities.prompts) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support prompts (required for ${method})`);
|
|
214
|
-
break;
|
|
215
|
-
case "resources/list":
|
|
216
|
-
case "resources/templates/list":
|
|
217
|
-
case "resources/read":
|
|
218
|
-
if (!this._capabilities.resources) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support resources (required for ${method})`);
|
|
219
|
-
break;
|
|
220
|
-
case "tools/call":
|
|
221
|
-
case "tools/list":
|
|
222
|
-
if (!this._capabilities.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support tools (required for ${method})`);
|
|
223
|
-
break;
|
|
224
|
-
case "ping":
|
|
225
|
-
case "initialize": break;
|
|
52
|
+
async handleMessage(message, extra) {
|
|
53
|
+
if (this._used) throw new Error("PerRequestHTTPServerTransport serves exactly one exchange; construct a new transport per request");
|
|
54
|
+
if (!this._started || this.onmessage === void 0) throw new Error("PerRequestHTTPServerTransport is not connected: connect a server to this transport before handling a message");
|
|
55
|
+
if (this._closed) throw new Error("PerRequestHTTPServerTransport is closed");
|
|
56
|
+
this._used = true;
|
|
57
|
+
const signal = extra?.request?.signal;
|
|
58
|
+
if (signal?.aborted) {
|
|
59
|
+
await this.close();
|
|
60
|
+
throw new SdkError(SdkErrorCode.ConnectionClosed, "The request was aborted before it could be handled");
|
|
226
61
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this._clientVersion = request.params.clientInfo;
|
|
232
|
-
const protocolVersion = this._supportedProtocolVersions.includes(requestedVersion) ? requestedVersion : this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION;
|
|
233
|
-
this._negotiatedProtocolVersion = protocolVersion;
|
|
234
|
-
this.transport?.setProtocolVersion?.(protocolVersion);
|
|
235
|
-
return {
|
|
236
|
-
protocolVersion,
|
|
237
|
-
capabilities: this.getCapabilities(),
|
|
238
|
-
serverInfo: this._serverInfo,
|
|
239
|
-
...this._instructions && { instructions: this._instructions }
|
|
62
|
+
const messageExtra = {
|
|
63
|
+
classification: this._classification,
|
|
64
|
+
...extra?.request !== void 0 && { request: extra.request },
|
|
65
|
+
...extra?.authInfo !== void 0 && { authInfo: extra.authInfo }
|
|
240
66
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
getNegotiatedProtocolVersion() {
|
|
260
|
-
return this._negotiatedProtocolVersion;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Returns the current server capabilities.
|
|
264
|
-
*/
|
|
265
|
-
getCapabilities() {
|
|
266
|
-
return this._capabilities;
|
|
267
|
-
}
|
|
268
|
-
async ping() {
|
|
269
|
-
return this._requestWithSchema({ method: "ping" }, EmptyResultSchema);
|
|
270
|
-
}
|
|
271
|
-
async createMessage(params, options) {
|
|
272
|
-
if ((params.tools || params.toolChoice) && !this._clientCapabilities?.sampling?.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support sampling tools capability.");
|
|
273
|
-
if (params.messages.length > 0) {
|
|
274
|
-
const lastMessage = params.messages.at(-1);
|
|
275
|
-
const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
|
|
276
|
-
const hasToolResults = lastContent.some((c) => c.type === "tool_result");
|
|
277
|
-
const previousMessage = params.messages.length > 1 ? params.messages.at(-2) : void 0;
|
|
278
|
-
const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
|
|
279
|
-
const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
|
|
280
|
-
if (hasToolResults) {
|
|
281
|
-
if (lastContent.some((c) => c.type !== "tool_result")) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "The last message must contain only tool_result content if any is present");
|
|
282
|
-
if (!hasPreviousToolUse) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "tool_result blocks are not matching any tool_use from the previous message");
|
|
67
|
+
if (isJSONRPCRequest(message)) {
|
|
68
|
+
this._requestId = message.id;
|
|
69
|
+
let resolve;
|
|
70
|
+
let reject;
|
|
71
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
72
|
+
resolve = promiseResolve;
|
|
73
|
+
reject = promiseReject;
|
|
74
|
+
});
|
|
75
|
+
this._deferredResponse = {
|
|
76
|
+
promise,
|
|
77
|
+
resolve,
|
|
78
|
+
reject,
|
|
79
|
+
settled: false
|
|
80
|
+
};
|
|
81
|
+
if (signal !== void 0) {
|
|
82
|
+
const onAbort = () => void this.close();
|
|
83
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
84
|
+
this._abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
283
85
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
86
|
+
this._dispatchWindowOpen = true;
|
|
87
|
+
try {
|
|
88
|
+
this.onmessage(message, messageExtra);
|
|
89
|
+
} finally {
|
|
90
|
+
this._dispatchWindowOpen = false;
|
|
288
91
|
}
|
|
92
|
+
if (this._responseMode === "sse" && !this._closed && !this._deferredResponse.settled) this.upgradeToSse();
|
|
93
|
+
return promise;
|
|
289
94
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
params
|
|
293
|
-
}, CreateMessageResultWithToolsSchema, options);
|
|
294
|
-
return this._requestWithSchema({
|
|
295
|
-
method: "sampling/createMessage",
|
|
296
|
-
params
|
|
297
|
-
}, CreateMessageResultSchema, options);
|
|
95
|
+
this.onmessage(message, messageExtra);
|
|
96
|
+
return new Response(null, { status: 202 });
|
|
298
97
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
98
|
+
async send(message, options) {
|
|
99
|
+
if (this._closed) return;
|
|
100
|
+
const isResponse = isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message);
|
|
101
|
+
const relatedId = isResponse ? message.id : options?.relatedRequestId;
|
|
102
|
+
if (this._requestId === void 0 || relatedId === void 0 || relatedId !== this._requestId) {
|
|
103
|
+
if (isResponse) this.onerror?.(/* @__PURE__ */ new Error(`Received a response for an unknown request id: ${String(message.id)}`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (isResponse) {
|
|
107
|
+
if (this._terminalDelivered) return;
|
|
108
|
+
this._terminalDelivered = true;
|
|
109
|
+
const ladderStatus = this._dispatchWindowOpen && isJSONRPCErrorResponse(message) ? LADDER_ERROR_HTTP_STATUS[message.error.code] : void 0;
|
|
110
|
+
if (ladderStatus !== void 0 && this._sse === void 0) {
|
|
111
|
+
this.settleResponse(Response.json(message, {
|
|
112
|
+
status: ladderStatus,
|
|
113
|
+
headers: { "Content-Type": "application/json" }
|
|
114
|
+
}));
|
|
115
|
+
queueMicrotask(() => void this.close());
|
|
116
|
+
return;
|
|
315
117
|
}
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
};
|
|
322
|
-
const result = await this._requestWithSchema({
|
|
323
|
-
method: "elicitation/create",
|
|
324
|
-
params: formParams
|
|
325
|
-
}, ElicitResultSchema, options);
|
|
326
|
-
if (result.action === "accept" && result.content && formParams.requestedSchema) try {
|
|
327
|
-
const validationResult = this._jsonSchemaValidator.getValidator(formParams.requestedSchema)(result.content);
|
|
328
|
-
if (!validationResult.valid) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Elicitation response content does not match requested schema: ${validationResult.errorMessage}`);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
if (error instanceof ProtocolError) throw error;
|
|
331
|
-
throw new ProtocolError(ProtocolErrorCode.InternalError, `Error validating elicitation response: ${error instanceof Error ? error.message : String(error)}`);
|
|
332
|
-
}
|
|
333
|
-
return result;
|
|
118
|
+
if (this._sse !== void 0 || this._responseMode === "sse") {
|
|
119
|
+
if (this._sse === void 0) this.upgradeToSse();
|
|
120
|
+
this.writeMessageFrame(message);
|
|
121
|
+
this.finalizeStream();
|
|
122
|
+
return;
|
|
334
123
|
}
|
|
124
|
+
this.settleResponse(Response.json(message, {
|
|
125
|
+
status: 200,
|
|
126
|
+
headers: { "Content-Type": "application/json" }
|
|
127
|
+
}));
|
|
128
|
+
queueMicrotask(() => void this.close());
|
|
129
|
+
return;
|
|
335
130
|
}
|
|
131
|
+
if (this._responseMode === "json") return;
|
|
132
|
+
if (this._sse === void 0) this.upgradeToSse();
|
|
133
|
+
this.writeMessageFrame(message);
|
|
336
134
|
}
|
|
337
135
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* @param elicitationId The ID of the elicitation to mark as complete.
|
|
342
|
-
* @param options Optional notification options. Useful when the completion notification should be related to a prior request.
|
|
343
|
-
* @returns A function that emits the completion notification when awaited.
|
|
136
|
+
* Writes an SSE comment frame (a keep-alive heartbeat). Dropped when the
|
|
137
|
+
* exchange is not currently streaming.
|
|
344
138
|
*/
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
params: { elicitationId }
|
|
350
|
-
}, options);
|
|
139
|
+
writeCommentFrame(comment) {
|
|
140
|
+
if (this._closed || this._sse === void 0 || this._sse.closed) return;
|
|
141
|
+
const frame = comment.split("\n").map((line) => `: ${line}`).join("\n");
|
|
142
|
+
this.writeFrame(`${frame}\n\n`);
|
|
351
143
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
144
|
+
async close() {
|
|
145
|
+
if (this._closed) return;
|
|
146
|
+
this._closed = true;
|
|
147
|
+
this._abortCleanup?.();
|
|
148
|
+
this._abortCleanup = void 0;
|
|
149
|
+
if (this._sse !== void 0 && !this._sse.closed) {
|
|
150
|
+
this._sse.closed = true;
|
|
151
|
+
try {
|
|
152
|
+
this._sse.controller.close();
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
if (this._deferredResponse !== void 0 && !this._deferredResponse.settled) {
|
|
156
|
+
this._deferredResponse.settled = true;
|
|
157
|
+
this._deferredResponse.reject(new SdkError(SdkErrorCode.ConnectionClosed, "Connection closed before a response was produced"));
|
|
158
|
+
}
|
|
159
|
+
this.onclose?.();
|
|
364
160
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
* @param params
|
|
370
|
-
* @param sessionId Optional for stateless transports and backward compatibility.
|
|
371
|
-
*
|
|
372
|
-
* @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).
|
|
373
|
-
* Remains functional during the deprecation window (at least twelve months).
|
|
374
|
-
* Migrate to stderr logging (STDIO servers) or OpenTelemetry.
|
|
375
|
-
*/
|
|
376
|
-
async sendLoggingMessage(params, sessionId) {
|
|
377
|
-
if (this._capabilities.logging && !this.isMessageIgnored(params.level, sessionId)) return this.notification({
|
|
378
|
-
method: "notifications/message",
|
|
379
|
-
params
|
|
380
|
-
});
|
|
161
|
+
settleResponse(response) {
|
|
162
|
+
if (this._deferredResponse === void 0 || this._deferredResponse.settled) return;
|
|
163
|
+
this._deferredResponse.settled = true;
|
|
164
|
+
this._deferredResponse.resolve(response);
|
|
381
165
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
166
|
+
upgradeToSse() {
|
|
167
|
+
let controller;
|
|
168
|
+
const readable = new ReadableStream({
|
|
169
|
+
start: (streamController) => {
|
|
170
|
+
controller = streamController;
|
|
171
|
+
},
|
|
172
|
+
cancel: () => {
|
|
173
|
+
this.close();
|
|
174
|
+
}
|
|
386
175
|
});
|
|
176
|
+
this._sse = {
|
|
177
|
+
controller,
|
|
178
|
+
encoder: new TextEncoder(),
|
|
179
|
+
closed: false
|
|
180
|
+
};
|
|
181
|
+
this.settleResponse(new Response(readable, {
|
|
182
|
+
status: 200,
|
|
183
|
+
headers: {
|
|
184
|
+
"Content-Type": "text/event-stream",
|
|
185
|
+
"Cache-Control": "no-cache",
|
|
186
|
+
Connection: "keep-alive",
|
|
187
|
+
"X-Accel-Buffering": "no"
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
387
190
|
}
|
|
388
|
-
|
|
389
|
-
|
|
191
|
+
finalizeStream() {
|
|
192
|
+
if (this._sse !== void 0 && !this._sse.closed) {
|
|
193
|
+
this._sse.closed = true;
|
|
194
|
+
try {
|
|
195
|
+
this._sse.controller.close();
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
queueMicrotask(() => void this.close());
|
|
390
199
|
}
|
|
391
|
-
|
|
392
|
-
|
|
200
|
+
writeMessageFrame(message) {
|
|
201
|
+
this.writeFrame(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
|
393
202
|
}
|
|
394
|
-
|
|
395
|
-
|
|
203
|
+
writeFrame(frame) {
|
|
204
|
+
if (this._sse === void 0 || this._sse.closed) return;
|
|
205
|
+
try {
|
|
206
|
+
this._sse.controller.enqueue(this._sse.encoder.encode(frame));
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.onerror?.(/* @__PURE__ */ new Error(`Failed to write to the response stream: ${error}`));
|
|
209
|
+
}
|
|
396
210
|
}
|
|
397
211
|
};
|
|
398
212
|
|
|
399
213
|
//#endregion
|
|
400
|
-
//#region src/server/
|
|
214
|
+
//#region src/server/invoke.ts
|
|
401
215
|
/**
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
* {@linkcode Server} instance available via the {@linkcode McpServer.server | server} property.
|
|
216
|
+
* Serves one classified inbound message on the given server instance and
|
|
217
|
+
* returns the HTTP response for the exchange.
|
|
405
218
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
219
|
+
* The instance is connected to a fresh single-exchange transport, the message
|
|
220
|
+
* is injected through the normal transport message path, and whatever the
|
|
221
|
+
* dispatch layer produces (the handler result, a protocol-level rejection, or
|
|
222
|
+
* streamed related messages followed by the result) is captured as the
|
|
223
|
+
* returned `Response`. For request exchanges, teardown rides the transport's
|
|
224
|
+
* close chain once the terminal response has been delivered; notification
|
|
225
|
+
* exchanges resolve with the 202 response immediately and do NOT run the
|
|
226
|
+
* close chain — the transport stays connected until the caller closes it or
|
|
227
|
+
* drops the per-request instance, which is the caller's choice either way.
|
|
228
|
+
*/
|
|
229
|
+
async function invoke(server, message, ctx) {
|
|
230
|
+
const transport = new PerRequestHTTPServerTransport({
|
|
231
|
+
classification: ctx.classification,
|
|
232
|
+
...ctx.responseMode !== void 0 && { responseMode: ctx.responseMode }
|
|
233
|
+
});
|
|
234
|
+
await server.connect(transport);
|
|
235
|
+
return transport.handleMessage(message, {
|
|
236
|
+
...ctx.request !== void 0 && { request: ctx.request },
|
|
237
|
+
...ctx.authInfo !== void 0 && { authInfo: ctx.authInfo }
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/server/streamableHttp.ts
|
|
243
|
+
/**
|
|
244
|
+
* Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
|
|
245
|
+
* using Web Standard APIs (`Request`, `Response`, `ReadableStream`).
|
|
246
|
+
*
|
|
247
|
+
* This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
|
|
248
|
+
*
|
|
249
|
+
* In stateful mode:
|
|
250
|
+
* - Session ID is generated and included in response headers
|
|
251
|
+
* - Session ID is always included in initialization responses
|
|
252
|
+
* - Requests with invalid session IDs are rejected with `404 Not Found`
|
|
253
|
+
* - Non-initialization requests without a session ID are rejected with `400 Bad Request`
|
|
254
|
+
* - State is maintained in-memory (connections, message history)
|
|
255
|
+
*
|
|
256
|
+
* In stateless mode:
|
|
257
|
+
* - No Session ID is included in any responses
|
|
258
|
+
* - No session validation is performed
|
|
259
|
+
*
|
|
260
|
+
* @example Stateful setup
|
|
261
|
+
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateful"
|
|
262
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
263
|
+
*
|
|
264
|
+
* const transport = new WebStandardStreamableHTTPServerTransport({
|
|
265
|
+
* sessionIdGenerator: () => crypto.randomUUID()
|
|
266
|
+
* });
|
|
267
|
+
*
|
|
268
|
+
* await server.connect(transport);
|
|
269
|
+
* ```
|
|
270
|
+
*
|
|
271
|
+
* @example Stateless setup
|
|
272
|
+
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateless"
|
|
273
|
+
* const transport = new WebStandardStreamableHTTPServerTransport({
|
|
274
|
+
* sessionIdGenerator: undefined
|
|
275
|
+
* });
|
|
276
|
+
* ```
|
|
277
|
+
*
|
|
278
|
+
* @example Hono.js
|
|
279
|
+
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_hono"
|
|
280
|
+
* app.all('/mcp', async c => {
|
|
281
|
+
* return transport.handleRequest(c.req.raw);
|
|
411
282
|
* });
|
|
412
283
|
* ```
|
|
284
|
+
*
|
|
285
|
+
* @example Cloudflare Workers
|
|
286
|
+
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_workers"
|
|
287
|
+
* const worker = {
|
|
288
|
+
* async fetch(request: Request): Promise<Response> {
|
|
289
|
+
* return transport.handleRequest(request);
|
|
290
|
+
* }
|
|
291
|
+
* };
|
|
292
|
+
* ```
|
|
413
293
|
*/
|
|
414
|
-
var
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
294
|
+
var WebStandardStreamableHTTPServerTransport = class {
|
|
295
|
+
sessionIdGenerator;
|
|
296
|
+
_started = false;
|
|
297
|
+
_closed = false;
|
|
298
|
+
_streamMapping = /* @__PURE__ */ new Map();
|
|
299
|
+
_requestToStreamMapping = /* @__PURE__ */ new Map();
|
|
300
|
+
_requestResponseMap = /* @__PURE__ */ new Map();
|
|
301
|
+
_initialized = false;
|
|
302
|
+
_enableJsonResponse = false;
|
|
303
|
+
_standaloneSseStreamId = "_GET_stream";
|
|
304
|
+
_eventStore;
|
|
305
|
+
_onsessioninitialized;
|
|
306
|
+
_onsessionclosed;
|
|
307
|
+
_allowedHosts;
|
|
308
|
+
_allowedOrigins;
|
|
309
|
+
_enableDnsRebindingProtection;
|
|
310
|
+
_retryInterval;
|
|
311
|
+
_supportedProtocolVersions;
|
|
312
|
+
sessionId;
|
|
313
|
+
onclose;
|
|
314
|
+
onerror;
|
|
315
|
+
onmessage;
|
|
316
|
+
constructor(options = {}) {
|
|
317
|
+
this.sessionIdGenerator = options.sessionIdGenerator;
|
|
318
|
+
this._enableJsonResponse = options.enableJsonResponse ?? false;
|
|
319
|
+
this._eventStore = options.eventStore;
|
|
320
|
+
this._onsessioninitialized = options.onsessioninitialized;
|
|
321
|
+
this._onsessionclosed = options.onsessionclosed;
|
|
322
|
+
this._allowedHosts = options.allowedHosts;
|
|
323
|
+
this._allowedOrigins = options.allowedOrigins;
|
|
324
|
+
this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
|
|
325
|
+
this._retryInterval = options.retryInterval;
|
|
326
|
+
this._supportedProtocolVersions = options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
|
|
428
327
|
}
|
|
429
328
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* The `server` object assumes ownership of the {@linkcode Transport}, replacing any callbacks that have already been set, and expects that it is the only user of the {@linkcode Transport} instance going forward.
|
|
433
|
-
*
|
|
434
|
-
* @example
|
|
435
|
-
* ```ts source="./mcp.examples.ts#McpServer_connect_stdio"
|
|
436
|
-
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
437
|
-
* const transport = new StdioServerTransport();
|
|
438
|
-
* await server.connect(transport);
|
|
439
|
-
* ```
|
|
329
|
+
* Starts the transport. This is required by the {@linkcode Transport} interface but is a no-op
|
|
330
|
+
* for the Streamable HTTP transport as connections are managed per-request.
|
|
440
331
|
*/
|
|
441
|
-
async
|
|
442
|
-
|
|
332
|
+
async start() {
|
|
333
|
+
if (this._started) throw new Error("Transport already started");
|
|
334
|
+
this._started = true;
|
|
443
335
|
}
|
|
444
336
|
/**
|
|
445
|
-
*
|
|
337
|
+
* Sets the supported protocol versions for header validation.
|
|
338
|
+
* Called by the server during {@linkcode server/server.Server.connect | connect()} to pass its supported versions.
|
|
446
339
|
*/
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
_toolHandlersInitialized = false;
|
|
451
|
-
setToolRequestHandlers() {
|
|
452
|
-
if (this._toolHandlersInitialized) return;
|
|
453
|
-
this.server.assertCanSetRequestHandler("tools/list");
|
|
454
|
-
this.server.assertCanSetRequestHandler("tools/call");
|
|
455
|
-
this.server.registerCapabilities({ tools: { listChanged: this.server.getCapabilities().tools?.listChanged ?? true } });
|
|
456
|
-
this.server.setRequestHandler("tools/list", () => ({ tools: Object.entries(this._registeredTools).filter(([, tool]) => tool.enabled).map(([name, tool]) => {
|
|
457
|
-
const toolDefinition = {
|
|
458
|
-
name,
|
|
459
|
-
title: tool.title,
|
|
460
|
-
description: tool.description,
|
|
461
|
-
inputSchema: tool.inputSchema ? standardSchemaToJsonSchema(tool.inputSchema, "input") : EMPTY_OBJECT_JSON_SCHEMA,
|
|
462
|
-
annotations: tool.annotations,
|
|
463
|
-
icons: tool.icons,
|
|
464
|
-
execution: tool.execution,
|
|
465
|
-
_meta: tool._meta
|
|
466
|
-
};
|
|
467
|
-
if (tool.outputSchema) toolDefinition.outputSchema = standardSchemaToJsonSchema(tool.outputSchema, "output");
|
|
468
|
-
return toolDefinition;
|
|
469
|
-
}) }));
|
|
470
|
-
this.server.setRequestHandler("tools/call", async (request, ctx) => {
|
|
471
|
-
const tool = this._registeredTools[request.params.name];
|
|
472
|
-
if (!tool) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
|
473
|
-
if (!tool.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
|
|
474
|
-
try {
|
|
475
|
-
const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
|
|
476
|
-
const result = await this.executeToolHandler(tool, args, ctx);
|
|
477
|
-
await this.validateToolOutput(tool, result, request.params.name);
|
|
478
|
-
return result;
|
|
479
|
-
} catch (error) {
|
|
480
|
-
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) throw error;
|
|
481
|
-
return this.createToolError(error instanceof Error ? error.message : String(error));
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
this._toolHandlersInitialized = true;
|
|
340
|
+
setSupportedProtocolVersions(versions) {
|
|
341
|
+
this._supportedProtocolVersions = versions;
|
|
485
342
|
}
|
|
486
343
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* @param errorMessage - The error message.
|
|
490
|
-
* @returns The tool error result.
|
|
344
|
+
* Helper to create a JSON error response
|
|
491
345
|
*/
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
text: errorMessage
|
|
497
|
-
}],
|
|
498
|
-
isError: true
|
|
346
|
+
createJsonErrorResponse(status, code, message, options) {
|
|
347
|
+
const error = {
|
|
348
|
+
code,
|
|
349
|
+
message
|
|
499
350
|
};
|
|
351
|
+
if (options?.data !== void 0) error.data = options.data;
|
|
352
|
+
return Response.json({
|
|
353
|
+
jsonrpc: "2.0",
|
|
354
|
+
error,
|
|
355
|
+
id: null
|
|
356
|
+
}, {
|
|
357
|
+
status,
|
|
358
|
+
headers: {
|
|
359
|
+
"Content-Type": "application/json",
|
|
360
|
+
...options?.headers
|
|
361
|
+
}
|
|
362
|
+
});
|
|
500
363
|
}
|
|
501
364
|
/**
|
|
502
|
-
* Validates
|
|
365
|
+
* Validates request headers for DNS rebinding protection.
|
|
366
|
+
* @returns Error response if validation fails, `undefined` if validation passes.
|
|
503
367
|
*/
|
|
504
|
-
|
|
505
|
-
if (!
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
368
|
+
validateRequestHeaders(req) {
|
|
369
|
+
if (!this._enableDnsRebindingProtection) return;
|
|
370
|
+
if (this._allowedHosts && this._allowedHosts.length > 0) {
|
|
371
|
+
const hostHeader = req.headers.get("host");
|
|
372
|
+
if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
|
|
373
|
+
const error = `Invalid Host header: ${hostHeader}`;
|
|
374
|
+
this.onerror?.(new Error(error));
|
|
375
|
+
return this.createJsonErrorResponse(403, -32e3, error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (this._allowedOrigins && this._allowedOrigins.length > 0) {
|
|
379
|
+
const originHeader = req.headers.get("origin");
|
|
380
|
+
if (originHeader && !this._allowedOrigins.includes(originHeader)) {
|
|
381
|
+
const error = `Invalid Origin header: ${originHeader}`;
|
|
382
|
+
this.onerror?.(new Error(error));
|
|
383
|
+
return this.createJsonErrorResponse(403, -32e3, error);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
509
386
|
}
|
|
510
387
|
/**
|
|
511
|
-
*
|
|
388
|
+
* Handles an incoming HTTP request, whether `GET`, `POST`, or `DELETE`
|
|
389
|
+
* Returns a `Response` object (Web Standard)
|
|
512
390
|
*/
|
|
513
|
-
async
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
391
|
+
async handleRequest(req, options) {
|
|
392
|
+
const validationError = this.validateRequestHeaders(req);
|
|
393
|
+
if (validationError) return validationError;
|
|
394
|
+
switch (req.method) {
|
|
395
|
+
case "POST": return this.handlePostRequest(req, options);
|
|
396
|
+
case "GET": return this.handleGetRequest(req);
|
|
397
|
+
case "DELETE": return this.handleDeleteRequest(req);
|
|
398
|
+
default: return this.handleUnsupportedRequest();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
520
401
|
/**
|
|
521
|
-
*
|
|
402
|
+
* Returns true if the client's protocol version supports empty SSE data in
|
|
403
|
+
* priming events (the fix shipped with protocol version `2025-11-25`).
|
|
404
|
+
*
|
|
405
|
+
* The version is checked for membership in this transport instance's
|
|
406
|
+
* supported protocol versions rather than with an open-ended
|
|
407
|
+
* `>= '2025-11-25'` comparison: the value may come from an `initialize`
|
|
408
|
+
* request body, which (unlike the `MCP-Protocol-Version` header) is not
|
|
409
|
+
* validated against `supportedProtocolVersions` before reaching this
|
|
410
|
+
* check. An unknown future version string must not silently enable
|
|
411
|
+
* behavior reserved for versions this transport actually supports.
|
|
522
412
|
*/
|
|
523
|
-
|
|
524
|
-
return
|
|
525
|
-
}
|
|
526
|
-
_completionHandlerInitialized = false;
|
|
527
|
-
setCompletionRequestHandler() {
|
|
528
|
-
if (this._completionHandlerInitialized) return;
|
|
529
|
-
this.server.assertCanSetRequestHandler("completion/complete");
|
|
530
|
-
this.server.registerCapabilities({ completions: {} });
|
|
531
|
-
this.server.setRequestHandler("completion/complete", async (request) => {
|
|
532
|
-
switch (request.params.ref.type) {
|
|
533
|
-
case "ref/prompt":
|
|
534
|
-
assertCompleteRequestPrompt(request);
|
|
535
|
-
return this.handlePromptCompletion(request, request.params.ref);
|
|
536
|
-
case "ref/resource":
|
|
537
|
-
assertCompleteRequestResourceTemplate(request);
|
|
538
|
-
return this.handleResourceCompletion(request, request.params.ref);
|
|
539
|
-
default: throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
this._completionHandlerInitialized = true;
|
|
413
|
+
supportsEmptySSEData(protocolVersion) {
|
|
414
|
+
return this._supportedProtocolVersions.includes(protocolVersion) && protocolVersion >= "2025-11-25";
|
|
543
415
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (!
|
|
553
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Writes a priming event to establish resumption capability.
|
|
418
|
+
* Only sends if `eventStore` is configured (opt-in for resumability) and
|
|
419
|
+
* the client's protocol version supports empty SSE data (a supported
|
|
420
|
+
* version that is >= `2025-11-25`).
|
|
421
|
+
*/
|
|
422
|
+
async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
|
|
423
|
+
if (!this._eventStore) return;
|
|
424
|
+
if (!this.supportsEmptySSEData(protocolVersion)) return;
|
|
425
|
+
const primingEventId = await this._eventStore.storeEvent(streamId, {});
|
|
426
|
+
let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
|
|
427
|
+
if (this._retryInterval !== void 0) primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
|
|
428
|
+
controller.enqueue(encoder.encode(primingEvent));
|
|
554
429
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Handles `GET` requests for SSE stream
|
|
432
|
+
*/
|
|
433
|
+
async handleGetRequest(req) {
|
|
434
|
+
if (!req.headers.get("accept")?.includes("text/event-stream")) {
|
|
435
|
+
this.onerror?.(/* @__PURE__ */ new Error("Not Acceptable: Client must accept text/event-stream"));
|
|
436
|
+
return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept text/event-stream");
|
|
560
437
|
}
|
|
561
|
-
const
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
this.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
const result = await template.resourceTemplate.listCallback(ctx);
|
|
582
|
-
for (const resource of result.resources) templateResources.push({
|
|
583
|
-
...template.metadata,
|
|
584
|
-
...resource
|
|
585
|
-
});
|
|
438
|
+
const sessionError = this.validateSession(req);
|
|
439
|
+
if (sessionError) return sessionError;
|
|
440
|
+
const protocolError = this.validateProtocolVersion(req);
|
|
441
|
+
if (protocolError) return protocolError;
|
|
442
|
+
if (this._eventStore) {
|
|
443
|
+
const lastEventId = req.headers.get("last-event-id");
|
|
444
|
+
if (lastEventId) return this.replayEvents(lastEventId);
|
|
445
|
+
}
|
|
446
|
+
if (this._streamMapping.get(this._standaloneSseStreamId) !== void 0) {
|
|
447
|
+
this.onerror?.(/* @__PURE__ */ new Error("Conflict: Only one SSE stream is allowed per session"));
|
|
448
|
+
return this.createJsonErrorResponse(409, -32e3, "Conflict: Only one SSE stream is allowed per session");
|
|
449
|
+
}
|
|
450
|
+
const encoder = new TextEncoder();
|
|
451
|
+
let streamController;
|
|
452
|
+
const readable = new ReadableStream({
|
|
453
|
+
start: (controller) => {
|
|
454
|
+
streamController = controller;
|
|
455
|
+
},
|
|
456
|
+
cancel: () => {
|
|
457
|
+
if (this._streamMapping.get(this._standaloneSseStreamId)?.controller === streamController) this._streamMapping.delete(this._standaloneSseStreamId);
|
|
586
458
|
}
|
|
587
|
-
return { resources: [...resources, ...templateResources] };
|
|
588
|
-
});
|
|
589
|
-
this.server.setRequestHandler("resources/templates/list", async () => {
|
|
590
|
-
return { resourceTemplates: Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({
|
|
591
|
-
name,
|
|
592
|
-
uriTemplate: template.resourceTemplate.uriTemplate.toString(),
|
|
593
|
-
...template.metadata
|
|
594
|
-
})) };
|
|
595
459
|
});
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
460
|
+
const headers = {
|
|
461
|
+
"Content-Type": "text/event-stream",
|
|
462
|
+
"Cache-Control": "no-cache, no-transform",
|
|
463
|
+
Connection: "keep-alive"
|
|
464
|
+
};
|
|
465
|
+
if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
|
|
466
|
+
this._streamMapping.set(this._standaloneSseStreamId, {
|
|
467
|
+
controller: streamController,
|
|
468
|
+
encoder,
|
|
469
|
+
cleanup: () => {
|
|
470
|
+
this._streamMapping.delete(this._standaloneSseStreamId);
|
|
471
|
+
try {
|
|
472
|
+
streamController.close();
|
|
473
|
+
} catch {}
|
|
606
474
|
}
|
|
607
|
-
throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, `Resource ${uri} not found`);
|
|
608
|
-
});
|
|
609
|
-
this._resourceHandlersInitialized = true;
|
|
610
|
-
}
|
|
611
|
-
_promptHandlersInitialized = false;
|
|
612
|
-
setPromptRequestHandlers() {
|
|
613
|
-
if (this._promptHandlersInitialized) return;
|
|
614
|
-
this.server.assertCanSetRequestHandler("prompts/list");
|
|
615
|
-
this.server.assertCanSetRequestHandler("prompts/get");
|
|
616
|
-
this.server.registerCapabilities({ prompts: { listChanged: this.server.getCapabilities().prompts?.listChanged ?? true } });
|
|
617
|
-
this.server.setRequestHandler("prompts/list", () => ({ prompts: Object.entries(this._registeredPrompts).filter(([, prompt]) => prompt.enabled).map(([name, prompt]) => {
|
|
618
|
-
return {
|
|
619
|
-
name,
|
|
620
|
-
title: prompt.title,
|
|
621
|
-
description: prompt.description,
|
|
622
|
-
arguments: prompt.argsSchema ? promptArgumentsFromStandardSchema(prompt.argsSchema) : void 0,
|
|
623
|
-
icons: prompt.icons,
|
|
624
|
-
_meta: prompt._meta
|
|
625
|
-
};
|
|
626
|
-
}) }));
|
|
627
|
-
this.server.setRequestHandler("prompts/get", async (request, ctx) => {
|
|
628
|
-
const prompt = this._registeredPrompts[request.params.name];
|
|
629
|
-
if (!prompt) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
|
|
630
|
-
if (!prompt.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`);
|
|
631
|
-
return prompt.handler(request.params.arguments, ctx);
|
|
632
475
|
});
|
|
633
|
-
|
|
476
|
+
return new Response(readable, { headers });
|
|
634
477
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (this._registeredResourceTemplates[name]) throw new Error(`Resource template ${name} is already registered`);
|
|
644
|
-
const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config.title, uriOrTemplate, config, readCallback);
|
|
645
|
-
this.setResourceRequestHandlers();
|
|
646
|
-
this.sendResourceListChanged();
|
|
647
|
-
return registeredResourceTemplate;
|
|
478
|
+
/**
|
|
479
|
+
* Replays events that would have been sent after the specified event ID
|
|
480
|
+
* Only used when resumability is enabled
|
|
481
|
+
*/
|
|
482
|
+
async replayEvents(lastEventId) {
|
|
483
|
+
if (!this._eventStore) {
|
|
484
|
+
this.onerror?.(/* @__PURE__ */ new Error("Event store not configured"));
|
|
485
|
+
return this.createJsonErrorResponse(400, -32e3, "Event store not configured");
|
|
648
486
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
enabled: true,
|
|
657
|
-
disable: () => registeredResource.update({ enabled: false }),
|
|
658
|
-
enable: () => registeredResource.update({ enabled: true }),
|
|
659
|
-
remove: () => registeredResource.update({ uri: null }),
|
|
660
|
-
update: (updates) => {
|
|
661
|
-
if (updates.uri !== void 0 && updates.uri !== uri) {
|
|
662
|
-
delete this._registeredResources[uri];
|
|
663
|
-
if (updates.uri) this._registeredResources[updates.uri] = registeredResource;
|
|
487
|
+
try {
|
|
488
|
+
let streamId;
|
|
489
|
+
if (this._eventStore.getStreamIdForEventId) {
|
|
490
|
+
streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
|
|
491
|
+
if (!streamId) {
|
|
492
|
+
this.onerror?.(/* @__PURE__ */ new Error("Invalid event ID format"));
|
|
493
|
+
return this.createJsonErrorResponse(400, -32e3, "Invalid event ID format");
|
|
664
494
|
}
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (updates.callback !== void 0) registeredResource.readCallback = updates.callback;
|
|
669
|
-
if (updates.enabled !== void 0) registeredResource.enabled = updates.enabled;
|
|
670
|
-
this.sendResourceListChanged();
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
this._registeredResources[uri] = registeredResource;
|
|
674
|
-
return registeredResource;
|
|
675
|
-
}
|
|
676
|
-
_createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
|
|
677
|
-
const registeredResourceTemplate = {
|
|
678
|
-
resourceTemplate: template,
|
|
679
|
-
title,
|
|
680
|
-
metadata,
|
|
681
|
-
readCallback,
|
|
682
|
-
enabled: true,
|
|
683
|
-
disable: () => registeredResourceTemplate.update({ enabled: false }),
|
|
684
|
-
enable: () => registeredResourceTemplate.update({ enabled: true }),
|
|
685
|
-
remove: () => registeredResourceTemplate.update({ name: null }),
|
|
686
|
-
update: (updates) => {
|
|
687
|
-
if (updates.name !== void 0 && updates.name !== name) {
|
|
688
|
-
delete this._registeredResourceTemplates[name];
|
|
689
|
-
if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate;
|
|
495
|
+
if (this._streamMapping.get(streamId) !== void 0) {
|
|
496
|
+
this.onerror?.(/* @__PURE__ */ new Error("Conflict: Stream already has an active connection"));
|
|
497
|
+
return this.createJsonErrorResponse(409, -32e3, "Conflict: Stream already has an active connection");
|
|
690
498
|
}
|
|
691
|
-
if (updates.title !== void 0) registeredResourceTemplate.title = updates.title;
|
|
692
|
-
if (updates.template !== void 0) registeredResourceTemplate.resourceTemplate = updates.template;
|
|
693
|
-
if (updates.metadata !== void 0) registeredResourceTemplate.metadata = updates.metadata;
|
|
694
|
-
if (updates.callback !== void 0) registeredResourceTemplate.readCallback = updates.callback;
|
|
695
|
-
if (updates.enabled !== void 0) registeredResourceTemplate.enabled = updates.enabled;
|
|
696
|
-
this.sendResourceListChanged();
|
|
697
499
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
handler: createPromptHandler(name, argsSchema, callback),
|
|
714
|
-
enabled: true,
|
|
715
|
-
disable: () => registeredPrompt.update({ enabled: false }),
|
|
716
|
-
enable: () => registeredPrompt.update({ enabled: true }),
|
|
717
|
-
remove: () => registeredPrompt.update({ name: null }),
|
|
718
|
-
update: (updates) => {
|
|
719
|
-
if (updates.name !== void 0 && updates.name !== name) {
|
|
720
|
-
delete this._registeredPrompts[name];
|
|
721
|
-
if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt;
|
|
500
|
+
const headers = {
|
|
501
|
+
"Content-Type": "text/event-stream",
|
|
502
|
+
"Cache-Control": "no-cache, no-transform",
|
|
503
|
+
Connection: "keep-alive"
|
|
504
|
+
};
|
|
505
|
+
if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
|
|
506
|
+
const encoder = new TextEncoder();
|
|
507
|
+
let streamController;
|
|
508
|
+
let replayedStreamId;
|
|
509
|
+
const readable = new ReadableStream({
|
|
510
|
+
start: (controller) => {
|
|
511
|
+
streamController = controller;
|
|
512
|
+
},
|
|
513
|
+
cancel: () => {
|
|
514
|
+
if (replayedStreamId !== void 0 && this._streamMapping.get(replayedStreamId)?.controller === streamController) this._streamMapping.delete(replayedStreamId);
|
|
722
515
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
516
|
+
});
|
|
517
|
+
const replayedEventIds = /* @__PURE__ */ new Set();
|
|
518
|
+
replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
|
|
519
|
+
replayedEventIds.add(eventId);
|
|
520
|
+
if (!this.writeSSEEvent(streamController, encoder, message, eventId)) try {
|
|
521
|
+
streamController.close();
|
|
522
|
+
} catch {}
|
|
523
|
+
} });
|
|
524
|
+
this._streamMapping.set(replayedStreamId, {
|
|
525
|
+
controller: streamController,
|
|
526
|
+
encoder,
|
|
527
|
+
replayedEventIds,
|
|
528
|
+
cleanup: () => {
|
|
529
|
+
this._streamMapping.delete(replayedStreamId);
|
|
530
|
+
try {
|
|
531
|
+
streamController.close();
|
|
532
|
+
} catch {}
|
|
732
533
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
534
|
+
});
|
|
535
|
+
if (replayedStreamId !== this._standaloneSseStreamId) {
|
|
536
|
+
if (![...this._requestToStreamMapping.values()].includes(replayedStreamId)) {
|
|
537
|
+
this._streamMapping.delete(replayedStreamId);
|
|
538
|
+
try {
|
|
539
|
+
streamController.close();
|
|
540
|
+
} catch {}
|
|
736
541
|
}
|
|
737
|
-
if (needsHandlerRegen) registeredPrompt.handler = createPromptHandler(name, currentArgsSchema, currentCallback);
|
|
738
|
-
if (updates.enabled !== void 0) registeredPrompt.enabled = updates.enabled;
|
|
739
|
-
this.sendPromptListChanged();
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
this._registeredPrompts[name] = registeredPrompt;
|
|
743
|
-
if (argsSchema) {
|
|
744
|
-
const shape = getSchemaShape(argsSchema);
|
|
745
|
-
if (shape) {
|
|
746
|
-
if (Object.values(shape).some((field) => {
|
|
747
|
-
return isCompletable(unwrapOptionalSchema(field));
|
|
748
|
-
})) this.setCompletionRequestHandler();
|
|
749
542
|
}
|
|
543
|
+
return new Response(readable, { headers });
|
|
544
|
+
} catch (error) {
|
|
545
|
+
this.onerror?.(error);
|
|
546
|
+
return this.createJsonErrorResponse(500, -32e3, "Error replaying events");
|
|
750
547
|
}
|
|
751
|
-
return registeredPrompt;
|
|
752
|
-
}
|
|
753
|
-
_createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, _meta, handler) {
|
|
754
|
-
validateAndWarnToolName(name);
|
|
755
|
-
let currentHandler = handler;
|
|
756
|
-
const registeredTool = {
|
|
757
|
-
title,
|
|
758
|
-
description,
|
|
759
|
-
inputSchema,
|
|
760
|
-
outputSchema,
|
|
761
|
-
annotations,
|
|
762
|
-
icons,
|
|
763
|
-
execution,
|
|
764
|
-
_meta,
|
|
765
|
-
handler,
|
|
766
|
-
executor: createToolExecutor(inputSchema, handler),
|
|
767
|
-
enabled: true,
|
|
768
|
-
disable: () => registeredTool.update({ enabled: false }),
|
|
769
|
-
enable: () => registeredTool.update({ enabled: true }),
|
|
770
|
-
remove: () => registeredTool.update({ name: null }),
|
|
771
|
-
update: (updates) => {
|
|
772
|
-
if (updates.name !== void 0 && updates.name !== name) {
|
|
773
|
-
if (typeof updates.name === "string") validateAndWarnToolName(updates.name);
|
|
774
|
-
delete this._registeredTools[name];
|
|
775
|
-
if (updates.name) this._registeredTools[updates.name] = registeredTool;
|
|
776
|
-
}
|
|
777
|
-
if (updates.title !== void 0) registeredTool.title = updates.title;
|
|
778
|
-
if (updates.description !== void 0) registeredTool.description = updates.description;
|
|
779
|
-
let needsExecutorRegen = false;
|
|
780
|
-
if (updates.paramsSchema !== void 0) {
|
|
781
|
-
registeredTool.inputSchema = updates.paramsSchema;
|
|
782
|
-
needsExecutorRegen = true;
|
|
783
|
-
}
|
|
784
|
-
if (updates.callback !== void 0) {
|
|
785
|
-
registeredTool.handler = updates.callback;
|
|
786
|
-
currentHandler = updates.callback;
|
|
787
|
-
needsExecutorRegen = true;
|
|
788
|
-
}
|
|
789
|
-
if (needsExecutorRegen) registeredTool.executor = createToolExecutor(registeredTool.inputSchema, currentHandler);
|
|
790
|
-
if (updates.outputSchema !== void 0) registeredTool.outputSchema = updates.outputSchema;
|
|
791
|
-
if (updates.annotations !== void 0) registeredTool.annotations = updates.annotations;
|
|
792
|
-
if (updates.icons !== void 0) registeredTool.icons = updates.icons;
|
|
793
|
-
if (updates._meta !== void 0) registeredTool._meta = updates._meta;
|
|
794
|
-
if (updates.enabled !== void 0) registeredTool.enabled = updates.enabled;
|
|
795
|
-
this.sendToolListChanged();
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
this._registeredTools[name] = registeredTool;
|
|
799
|
-
this.setToolRequestHandlers();
|
|
800
|
-
this.sendToolListChanged();
|
|
801
|
-
return registeredTool;
|
|
802
|
-
}
|
|
803
|
-
registerTool(name, config, cb) {
|
|
804
|
-
if (this._registeredTools[name]) throw new Error(`Tool ${name} is already registered`);
|
|
805
|
-
const { title, description, inputSchema, outputSchema, annotations, icons, _meta } = config;
|
|
806
|
-
return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, _meta, cb);
|
|
807
|
-
}
|
|
808
|
-
registerPrompt(name, config, cb) {
|
|
809
|
-
if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
|
|
810
|
-
const { title, description, argsSchema, icons, _meta } = config;
|
|
811
|
-
const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, _meta);
|
|
812
|
-
this.setPromptRequestHandlers();
|
|
813
|
-
this.sendPromptListChanged();
|
|
814
|
-
return registeredPrompt;
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Checks if the server is connected to a transport.
|
|
818
|
-
* @returns `true` if the server is connected
|
|
819
|
-
*/
|
|
820
|
-
isConnected() {
|
|
821
|
-
return this.server.transport !== void 0;
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Sends a logging message to the client, if connected.
|
|
825
|
-
* Note: You only need to send the parameters object, not the entire JSON-RPC message.
|
|
826
|
-
* @see {@linkcode LoggingMessageNotification}
|
|
827
|
-
* @param params
|
|
828
|
-
* @param sessionId Optional for stateless transports and backward compatibility.
|
|
829
|
-
*
|
|
830
|
-
* @example
|
|
831
|
-
* ```ts source="./mcp.examples.ts#McpServer_sendLoggingMessage_basic"
|
|
832
|
-
* await server.sendLoggingMessage({
|
|
833
|
-
* level: 'info',
|
|
834
|
-
* data: 'Processing complete'
|
|
835
|
-
* });
|
|
836
|
-
* ```
|
|
837
|
-
*
|
|
838
|
-
* @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577).
|
|
839
|
-
* Remains functional during the deprecation window (at least twelve months).
|
|
840
|
-
* Migrate to stderr logging (STDIO servers) or OpenTelemetry.
|
|
841
|
-
*/
|
|
842
|
-
async sendLoggingMessage(params, sessionId) {
|
|
843
|
-
return this.server.sendLoggingMessage(params, sessionId);
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Sends a resource list changed event to the client, if connected.
|
|
847
|
-
*/
|
|
848
|
-
sendResourceListChanged() {
|
|
849
|
-
if (this.isConnected()) this.server.sendResourceListChanged();
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Sends a tool list changed event to the client, if connected.
|
|
853
|
-
*/
|
|
854
|
-
sendToolListChanged() {
|
|
855
|
-
if (this.isConnected()) this.server.sendToolListChanged();
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Sends a prompt list changed event to the client, if connected.
|
|
859
|
-
*/
|
|
860
|
-
sendPromptListChanged() {
|
|
861
|
-
if (this.isConnected()) this.server.sendPromptListChanged();
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
/**
|
|
865
|
-
* A resource template combines a URI pattern with optional functionality to enumerate
|
|
866
|
-
* all resources matching that pattern.
|
|
867
|
-
*/
|
|
868
|
-
var ResourceTemplate = class {
|
|
869
|
-
_uriTemplate;
|
|
870
|
-
constructor(uriTemplate, _callbacks) {
|
|
871
|
-
this._callbacks = _callbacks;
|
|
872
|
-
this._uriTemplate = typeof uriTemplate === "string" ? new UriTemplate(uriTemplate) : uriTemplate;
|
|
873
548
|
}
|
|
874
549
|
/**
|
|
875
|
-
*
|
|
550
|
+
* Writes an event to an SSE stream via controller with proper formatting
|
|
876
551
|
*/
|
|
877
|
-
|
|
878
|
-
|
|
552
|
+
writeSSEEvent(controller, encoder, message, eventId) {
|
|
553
|
+
try {
|
|
554
|
+
let eventData = `event: message\n`;
|
|
555
|
+
if (eventId) eventData += `id: ${eventId}\n`;
|
|
556
|
+
eventData += `data: ${JSON.stringify(message)}\n\n`;
|
|
557
|
+
controller.enqueue(encoder.encode(eventData));
|
|
558
|
+
return true;
|
|
559
|
+
} catch (error) {
|
|
560
|
+
this.onerror?.(error);
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
879
563
|
}
|
|
880
564
|
/**
|
|
881
|
-
*
|
|
565
|
+
* Handles unsupported requests (`PUT`, `PATCH`, etc.)
|
|
882
566
|
*/
|
|
883
|
-
|
|
884
|
-
|
|
567
|
+
handleUnsupportedRequest() {
|
|
568
|
+
this.onerror?.(/* @__PURE__ */ new Error("Method not allowed."));
|
|
569
|
+
return Response.json({
|
|
570
|
+
jsonrpc: "2.0",
|
|
571
|
+
error: {
|
|
572
|
+
code: -32e3,
|
|
573
|
+
message: "Method not allowed."
|
|
574
|
+
},
|
|
575
|
+
id: null
|
|
576
|
+
}, {
|
|
577
|
+
status: 405,
|
|
578
|
+
headers: {
|
|
579
|
+
Allow: "GET, POST, DELETE",
|
|
580
|
+
"Content-Type": "application/json"
|
|
581
|
+
}
|
|
582
|
+
});
|
|
885
583
|
}
|
|
886
584
|
/**
|
|
887
|
-
*
|
|
888
|
-
*/
|
|
889
|
-
completeCallback(variable) {
|
|
890
|
-
return this._callbacks.complete?.[variable];
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
/**
|
|
894
|
-
* Creates an executor that invokes the handler with the appropriate arguments.
|
|
895
|
-
* When `inputSchema` is defined, the handler is called with `(args, ctx)`.
|
|
896
|
-
* When `inputSchema` is undefined, the handler is called with just `(ctx)`.
|
|
897
|
-
*/
|
|
898
|
-
function createToolExecutor(inputSchema, handler) {
|
|
899
|
-
if (inputSchema) {
|
|
900
|
-
const callback$1 = handler;
|
|
901
|
-
return async (args, ctx) => callback$1(args, ctx);
|
|
902
|
-
}
|
|
903
|
-
const callback = handler;
|
|
904
|
-
return async (_args, ctx) => callback(ctx);
|
|
905
|
-
}
|
|
906
|
-
const EMPTY_OBJECT_JSON_SCHEMA = {
|
|
907
|
-
type: "object",
|
|
908
|
-
properties: {}
|
|
909
|
-
};
|
|
910
|
-
/**
|
|
911
|
-
* Creates a type-safe prompt handler that captures the schema and callback in a closure.
|
|
912
|
-
* This eliminates the need for type assertions at the call site.
|
|
913
|
-
*/
|
|
914
|
-
function createPromptHandler(name, argsSchema, callback) {
|
|
915
|
-
if (argsSchema) {
|
|
916
|
-
const typedCallback = callback;
|
|
917
|
-
return async (args, ctx) => {
|
|
918
|
-
const parseResult = await validateStandardSchema(argsSchema, args);
|
|
919
|
-
if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${parseResult.error}`);
|
|
920
|
-
return typedCallback(parseResult.data, ctx);
|
|
921
|
-
};
|
|
922
|
-
} else {
|
|
923
|
-
const typedCallback = callback;
|
|
924
|
-
return async (_args, ctx) => {
|
|
925
|
-
return typedCallback(ctx);
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
function createCompletionResult(suggestions) {
|
|
930
|
-
return { completion: {
|
|
931
|
-
values: suggestions.map(String).slice(0, 100),
|
|
932
|
-
total: suggestions.length,
|
|
933
|
-
hasMore: suggestions.length > 100
|
|
934
|
-
} };
|
|
935
|
-
}
|
|
936
|
-
const EMPTY_COMPLETION_RESULT = { completion: {
|
|
937
|
-
values: [],
|
|
938
|
-
hasMore: false
|
|
939
|
-
} };
|
|
940
|
-
/** @internal Gets the shape of a Zod object schema */
|
|
941
|
-
function getSchemaShape(schema) {
|
|
942
|
-
const candidate = schema;
|
|
943
|
-
if (candidate.shape && typeof candidate.shape === "object") return candidate.shape;
|
|
944
|
-
}
|
|
945
|
-
/** @internal Checks if a Zod schema is optional */
|
|
946
|
-
function isOptionalSchema(schema) {
|
|
947
|
-
return schema?.type === "optional";
|
|
948
|
-
}
|
|
949
|
-
/** @internal Unwraps an optional Zod schema */
|
|
950
|
-
function unwrapOptionalSchema(schema) {
|
|
951
|
-
if (!isOptionalSchema(schema)) return schema;
|
|
952
|
-
return schema.def?.innerType ?? schema;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
//#endregion
|
|
956
|
-
//#region src/server/middleware/hostHeaderValidation.ts
|
|
957
|
-
/**
|
|
958
|
-
* Parse and validate a `Host` header against an allowlist of hostnames (port-agnostic).
|
|
959
|
-
*
|
|
960
|
-
* - Input host header may include a port (e.g. `localhost:3000`) or IPv6 brackets (e.g. `[::1]:3000`).
|
|
961
|
-
* - Allowlist items should be hostnames only (no ports). For IPv6, include brackets (e.g. `[::1]`).
|
|
962
|
-
*/
|
|
963
|
-
function validateHostHeader(hostHeader, allowedHostnames) {
|
|
964
|
-
if (!hostHeader) return {
|
|
965
|
-
ok: false,
|
|
966
|
-
errorCode: "missing_host",
|
|
967
|
-
message: "Missing Host header"
|
|
968
|
-
};
|
|
969
|
-
let hostname;
|
|
970
|
-
try {
|
|
971
|
-
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
972
|
-
} catch {
|
|
973
|
-
return {
|
|
974
|
-
ok: false,
|
|
975
|
-
errorCode: "invalid_host_header",
|
|
976
|
-
message: `Invalid Host header: ${hostHeader}`,
|
|
977
|
-
hostHeader
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
if (!allowedHostnames.includes(hostname)) return {
|
|
981
|
-
ok: false,
|
|
982
|
-
errorCode: "invalid_host",
|
|
983
|
-
message: `Invalid Host: ${hostname}`,
|
|
984
|
-
hostHeader,
|
|
985
|
-
hostname
|
|
986
|
-
};
|
|
987
|
-
return {
|
|
988
|
-
ok: true,
|
|
989
|
-
hostname
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Convenience allowlist for `localhost` DNS rebinding protection.
|
|
994
|
-
*/
|
|
995
|
-
function localhostAllowedHostnames() {
|
|
996
|
-
return [
|
|
997
|
-
"localhost",
|
|
998
|
-
"127.0.0.1",
|
|
999
|
-
"[::1]"
|
|
1000
|
-
];
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Web-standard `Request` helper for DNS rebinding protection.
|
|
1004
|
-
* @example
|
|
1005
|
-
* ```ts source="./hostHeaderValidation.examples.ts#hostHeaderValidationResponse_basicUsage"
|
|
1006
|
-
* const result = validateHostHeader(req.headers.get('host'), ['localhost']);
|
|
1007
|
-
* ```
|
|
1008
|
-
*/
|
|
1009
|
-
function hostHeaderValidationResponse(req, allowedHostnames) {
|
|
1010
|
-
const result = validateHostHeader(req.headers.get("host"), allowedHostnames);
|
|
1011
|
-
if (result.ok) return void 0;
|
|
1012
|
-
return Response.json({
|
|
1013
|
-
jsonrpc: "2.0",
|
|
1014
|
-
error: {
|
|
1015
|
-
code: -32e3,
|
|
1016
|
-
message: result.message
|
|
1017
|
-
},
|
|
1018
|
-
id: null
|
|
1019
|
-
}, {
|
|
1020
|
-
status: 403,
|
|
1021
|
-
headers: { "Content-Type": "application/json" }
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
//#endregion
|
|
1026
|
-
//#region src/server/streamableHttp.ts
|
|
1027
|
-
/**
|
|
1028
|
-
* Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
|
|
1029
|
-
* using Web Standard APIs (`Request`, `Response`, `ReadableStream`).
|
|
1030
|
-
*
|
|
1031
|
-
* This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
|
|
1032
|
-
*
|
|
1033
|
-
* In stateful mode:
|
|
1034
|
-
* - Session ID is generated and included in response headers
|
|
1035
|
-
* - Session ID is always included in initialization responses
|
|
1036
|
-
* - Requests with invalid session IDs are rejected with `404 Not Found`
|
|
1037
|
-
* - Non-initialization requests without a session ID are rejected with `400 Bad Request`
|
|
1038
|
-
* - State is maintained in-memory (connections, message history)
|
|
1039
|
-
*
|
|
1040
|
-
* In stateless mode:
|
|
1041
|
-
* - No Session ID is included in any responses
|
|
1042
|
-
* - No session validation is performed
|
|
1043
|
-
*
|
|
1044
|
-
* @example Stateful setup
|
|
1045
|
-
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateful"
|
|
1046
|
-
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
1047
|
-
*
|
|
1048
|
-
* const transport = new WebStandardStreamableHTTPServerTransport({
|
|
1049
|
-
* sessionIdGenerator: () => crypto.randomUUID()
|
|
1050
|
-
* });
|
|
1051
|
-
*
|
|
1052
|
-
* await server.connect(transport);
|
|
1053
|
-
* ```
|
|
1054
|
-
*
|
|
1055
|
-
* @example Stateless setup
|
|
1056
|
-
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateless"
|
|
1057
|
-
* const transport = new WebStandardStreamableHTTPServerTransport({
|
|
1058
|
-
* sessionIdGenerator: undefined
|
|
1059
|
-
* });
|
|
1060
|
-
* ```
|
|
1061
|
-
*
|
|
1062
|
-
* @example Hono.js
|
|
1063
|
-
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_hono"
|
|
1064
|
-
* app.all('/mcp', async c => {
|
|
1065
|
-
* return transport.handleRequest(c.req.raw);
|
|
1066
|
-
* });
|
|
1067
|
-
* ```
|
|
1068
|
-
*
|
|
1069
|
-
* @example Cloudflare Workers
|
|
1070
|
-
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_workers"
|
|
1071
|
-
* const worker = {
|
|
1072
|
-
* async fetch(request: Request): Promise<Response> {
|
|
1073
|
-
* return transport.handleRequest(request);
|
|
1074
|
-
* }
|
|
1075
|
-
* };
|
|
1076
|
-
* ```
|
|
1077
|
-
*/
|
|
1078
|
-
var WebStandardStreamableHTTPServerTransport = class {
|
|
1079
|
-
sessionIdGenerator;
|
|
1080
|
-
_started = false;
|
|
1081
|
-
_closed = false;
|
|
1082
|
-
_streamMapping = /* @__PURE__ */ new Map();
|
|
1083
|
-
_requestToStreamMapping = /* @__PURE__ */ new Map();
|
|
1084
|
-
_requestResponseMap = /* @__PURE__ */ new Map();
|
|
1085
|
-
_initialized = false;
|
|
1086
|
-
_enableJsonResponse = false;
|
|
1087
|
-
_standaloneSseStreamId = "_GET_stream";
|
|
1088
|
-
_eventStore;
|
|
1089
|
-
_onsessioninitialized;
|
|
1090
|
-
_onsessionclosed;
|
|
1091
|
-
_allowedHosts;
|
|
1092
|
-
_allowedOrigins;
|
|
1093
|
-
_enableDnsRebindingProtection;
|
|
1094
|
-
_retryInterval;
|
|
1095
|
-
_supportedProtocolVersions;
|
|
1096
|
-
sessionId;
|
|
1097
|
-
onclose;
|
|
1098
|
-
onerror;
|
|
1099
|
-
onmessage;
|
|
1100
|
-
constructor(options = {}) {
|
|
1101
|
-
this.sessionIdGenerator = options.sessionIdGenerator;
|
|
1102
|
-
this._enableJsonResponse = options.enableJsonResponse ?? false;
|
|
1103
|
-
this._eventStore = options.eventStore;
|
|
1104
|
-
this._onsessioninitialized = options.onsessioninitialized;
|
|
1105
|
-
this._onsessionclosed = options.onsessionclosed;
|
|
1106
|
-
this._allowedHosts = options.allowedHosts;
|
|
1107
|
-
this._allowedOrigins = options.allowedOrigins;
|
|
1108
|
-
this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
|
|
1109
|
-
this._retryInterval = options.retryInterval;
|
|
1110
|
-
this._supportedProtocolVersions = options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Starts the transport. This is required by the {@linkcode Transport} interface but is a no-op
|
|
1114
|
-
* for the Streamable HTTP transport as connections are managed per-request.
|
|
1115
|
-
*/
|
|
1116
|
-
async start() {
|
|
1117
|
-
if (this._started) throw new Error("Transport already started");
|
|
1118
|
-
this._started = true;
|
|
1119
|
-
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Sets the supported protocol versions for header validation.
|
|
1122
|
-
* Called by the server during {@linkcode server/server.Server.connect | connect()} to pass its supported versions.
|
|
1123
|
-
*/
|
|
1124
|
-
setSupportedProtocolVersions(versions) {
|
|
1125
|
-
this._supportedProtocolVersions = versions;
|
|
1126
|
-
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Helper to create a JSON error response
|
|
1129
|
-
*/
|
|
1130
|
-
createJsonErrorResponse(status, code, message, options) {
|
|
1131
|
-
const error = {
|
|
1132
|
-
code,
|
|
1133
|
-
message
|
|
1134
|
-
};
|
|
1135
|
-
if (options?.data !== void 0) error.data = options.data;
|
|
1136
|
-
return Response.json({
|
|
1137
|
-
jsonrpc: "2.0",
|
|
1138
|
-
error,
|
|
1139
|
-
id: null
|
|
1140
|
-
}, {
|
|
1141
|
-
status,
|
|
1142
|
-
headers: {
|
|
1143
|
-
"Content-Type": "application/json",
|
|
1144
|
-
...options?.headers
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Validates request headers for DNS rebinding protection.
|
|
1150
|
-
* @returns Error response if validation fails, `undefined` if validation passes.
|
|
1151
|
-
*/
|
|
1152
|
-
validateRequestHeaders(req) {
|
|
1153
|
-
if (!this._enableDnsRebindingProtection) return;
|
|
1154
|
-
if (this._allowedHosts && this._allowedHosts.length > 0) {
|
|
1155
|
-
const hostHeader = req.headers.get("host");
|
|
1156
|
-
if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
|
|
1157
|
-
const error = `Invalid Host header: ${hostHeader}`;
|
|
1158
|
-
this.onerror?.(new Error(error));
|
|
1159
|
-
return this.createJsonErrorResponse(403, -32e3, error);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
if (this._allowedOrigins && this._allowedOrigins.length > 0) {
|
|
1163
|
-
const originHeader = req.headers.get("origin");
|
|
1164
|
-
if (originHeader && !this._allowedOrigins.includes(originHeader)) {
|
|
1165
|
-
const error = `Invalid Origin header: ${originHeader}`;
|
|
1166
|
-
this.onerror?.(new Error(error));
|
|
1167
|
-
return this.createJsonErrorResponse(403, -32e3, error);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* Handles an incoming HTTP request, whether `GET`, `POST`, or `DELETE`
|
|
1173
|
-
* Returns a `Response` object (Web Standard)
|
|
1174
|
-
*/
|
|
1175
|
-
async handleRequest(req, options) {
|
|
1176
|
-
const validationError = this.validateRequestHeaders(req);
|
|
1177
|
-
if (validationError) return validationError;
|
|
1178
|
-
switch (req.method) {
|
|
1179
|
-
case "POST": return this.handlePostRequest(req, options);
|
|
1180
|
-
case "GET": return this.handleGetRequest(req);
|
|
1181
|
-
case "DELETE": return this.handleDeleteRequest(req);
|
|
1182
|
-
default: return this.handleUnsupportedRequest();
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Returns true if the client's protocol version supports empty SSE data in
|
|
1187
|
-
* priming events (the fix shipped with protocol version `2025-11-25`).
|
|
1188
|
-
*
|
|
1189
|
-
* The version is checked for membership in this transport instance's
|
|
1190
|
-
* supported protocol versions rather than with an open-ended
|
|
1191
|
-
* `>= '2025-11-25'` comparison: the value may come from an `initialize`
|
|
1192
|
-
* request body, which (unlike the `MCP-Protocol-Version` header) is not
|
|
1193
|
-
* validated against `supportedProtocolVersions` before reaching this
|
|
1194
|
-
* check. An unknown future version string must not silently enable
|
|
1195
|
-
* behavior reserved for versions this transport actually supports.
|
|
1196
|
-
*/
|
|
1197
|
-
supportsEmptySSEData(protocolVersion) {
|
|
1198
|
-
return this._supportedProtocolVersions.includes(protocolVersion) && protocolVersion >= "2025-11-25";
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Writes a priming event to establish resumption capability.
|
|
1202
|
-
* Only sends if `eventStore` is configured (opt-in for resumability) and
|
|
1203
|
-
* the client's protocol version supports empty SSE data (a supported
|
|
1204
|
-
* version that is >= `2025-11-25`).
|
|
1205
|
-
*/
|
|
1206
|
-
async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
|
|
1207
|
-
if (!this._eventStore) return;
|
|
1208
|
-
if (!this.supportsEmptySSEData(protocolVersion)) return;
|
|
1209
|
-
const primingEventId = await this._eventStore.storeEvent(streamId, {});
|
|
1210
|
-
let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
|
|
1211
|
-
if (this._retryInterval !== void 0) primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
|
|
1212
|
-
controller.enqueue(encoder.encode(primingEvent));
|
|
1213
|
-
}
|
|
1214
|
-
/**
|
|
1215
|
-
* Handles `GET` requests for SSE stream
|
|
1216
|
-
*/
|
|
1217
|
-
async handleGetRequest(req) {
|
|
1218
|
-
if (!req.headers.get("accept")?.includes("text/event-stream")) {
|
|
1219
|
-
this.onerror?.(/* @__PURE__ */ new Error("Not Acceptable: Client must accept text/event-stream"));
|
|
1220
|
-
return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept text/event-stream");
|
|
1221
|
-
}
|
|
1222
|
-
const sessionError = this.validateSession(req);
|
|
1223
|
-
if (sessionError) return sessionError;
|
|
1224
|
-
const protocolError = this.validateProtocolVersion(req);
|
|
1225
|
-
if (protocolError) return protocolError;
|
|
1226
|
-
if (this._eventStore) {
|
|
1227
|
-
const lastEventId = req.headers.get("last-event-id");
|
|
1228
|
-
if (lastEventId) return this.replayEvents(lastEventId);
|
|
1229
|
-
}
|
|
1230
|
-
if (this._streamMapping.get(this._standaloneSseStreamId) !== void 0) {
|
|
1231
|
-
this.onerror?.(/* @__PURE__ */ new Error("Conflict: Only one SSE stream is allowed per session"));
|
|
1232
|
-
return this.createJsonErrorResponse(409, -32e3, "Conflict: Only one SSE stream is allowed per session");
|
|
1233
|
-
}
|
|
1234
|
-
const encoder = new TextEncoder();
|
|
1235
|
-
let streamController;
|
|
1236
|
-
const readable = new ReadableStream({
|
|
1237
|
-
start: (controller) => {
|
|
1238
|
-
streamController = controller;
|
|
1239
|
-
},
|
|
1240
|
-
cancel: () => {
|
|
1241
|
-
this._streamMapping.delete(this._standaloneSseStreamId);
|
|
1242
|
-
}
|
|
1243
|
-
});
|
|
1244
|
-
const headers = {
|
|
1245
|
-
"Content-Type": "text/event-stream",
|
|
1246
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1247
|
-
Connection: "keep-alive"
|
|
1248
|
-
};
|
|
1249
|
-
if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
|
|
1250
|
-
this._streamMapping.set(this._standaloneSseStreamId, {
|
|
1251
|
-
controller: streamController,
|
|
1252
|
-
encoder,
|
|
1253
|
-
cleanup: () => {
|
|
1254
|
-
this._streamMapping.delete(this._standaloneSseStreamId);
|
|
1255
|
-
try {
|
|
1256
|
-
streamController.close();
|
|
1257
|
-
} catch {}
|
|
1258
|
-
}
|
|
1259
|
-
});
|
|
1260
|
-
return new Response(readable, { headers });
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* Replays events that would have been sent after the specified event ID
|
|
1264
|
-
* Only used when resumability is enabled
|
|
1265
|
-
*/
|
|
1266
|
-
async replayEvents(lastEventId) {
|
|
1267
|
-
if (!this._eventStore) {
|
|
1268
|
-
this.onerror?.(/* @__PURE__ */ new Error("Event store not configured"));
|
|
1269
|
-
return this.createJsonErrorResponse(400, -32e3, "Event store not configured");
|
|
1270
|
-
}
|
|
1271
|
-
try {
|
|
1272
|
-
let streamId;
|
|
1273
|
-
if (this._eventStore.getStreamIdForEventId) {
|
|
1274
|
-
streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
|
|
1275
|
-
if (!streamId) {
|
|
1276
|
-
this.onerror?.(/* @__PURE__ */ new Error("Invalid event ID format"));
|
|
1277
|
-
return this.createJsonErrorResponse(400, -32e3, "Invalid event ID format");
|
|
1278
|
-
}
|
|
1279
|
-
if (this._streamMapping.get(streamId) !== void 0) {
|
|
1280
|
-
this.onerror?.(/* @__PURE__ */ new Error("Conflict: Stream already has an active connection"));
|
|
1281
|
-
return this.createJsonErrorResponse(409, -32e3, "Conflict: Stream already has an active connection");
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
const headers = {
|
|
1285
|
-
"Content-Type": "text/event-stream",
|
|
1286
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1287
|
-
Connection: "keep-alive"
|
|
1288
|
-
};
|
|
1289
|
-
if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
|
|
1290
|
-
const encoder = new TextEncoder();
|
|
1291
|
-
let streamController;
|
|
1292
|
-
const readable = new ReadableStream({
|
|
1293
|
-
start: (controller) => {
|
|
1294
|
-
streamController = controller;
|
|
1295
|
-
},
|
|
1296
|
-
cancel: () => {}
|
|
1297
|
-
});
|
|
1298
|
-
const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
|
|
1299
|
-
if (!this.writeSSEEvent(streamController, encoder, message, eventId)) try {
|
|
1300
|
-
streamController.close();
|
|
1301
|
-
} catch {}
|
|
1302
|
-
} });
|
|
1303
|
-
this._streamMapping.set(replayedStreamId, {
|
|
1304
|
-
controller: streamController,
|
|
1305
|
-
encoder,
|
|
1306
|
-
cleanup: () => {
|
|
1307
|
-
this._streamMapping.delete(replayedStreamId);
|
|
1308
|
-
try {
|
|
1309
|
-
streamController.close();
|
|
1310
|
-
} catch {}
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
return new Response(readable, { headers });
|
|
1314
|
-
} catch (error) {
|
|
1315
|
-
this.onerror?.(error);
|
|
1316
|
-
return this.createJsonErrorResponse(500, -32e3, "Error replaying events");
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Writes an event to an SSE stream via controller with proper formatting
|
|
1321
|
-
*/
|
|
1322
|
-
writeSSEEvent(controller, encoder, message, eventId) {
|
|
1323
|
-
try {
|
|
1324
|
-
let eventData = `event: message\n`;
|
|
1325
|
-
if (eventId) eventData += `id: ${eventId}\n`;
|
|
1326
|
-
eventData += `data: ${JSON.stringify(message)}\n\n`;
|
|
1327
|
-
controller.enqueue(encoder.encode(eventData));
|
|
1328
|
-
return true;
|
|
1329
|
-
} catch (error) {
|
|
1330
|
-
this.onerror?.(error);
|
|
1331
|
-
return false;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Handles unsupported requests (`PUT`, `PATCH`, etc.)
|
|
1336
|
-
*/
|
|
1337
|
-
handleUnsupportedRequest() {
|
|
1338
|
-
this.onerror?.(/* @__PURE__ */ new Error("Method not allowed."));
|
|
1339
|
-
return Response.json({
|
|
1340
|
-
jsonrpc: "2.0",
|
|
1341
|
-
error: {
|
|
1342
|
-
code: -32e3,
|
|
1343
|
-
message: "Method not allowed."
|
|
1344
|
-
},
|
|
1345
|
-
id: null
|
|
1346
|
-
}, {
|
|
1347
|
-
status: 405,
|
|
1348
|
-
headers: {
|
|
1349
|
-
Allow: "GET, POST, DELETE",
|
|
1350
|
-
"Content-Type": "application/json"
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
/**
|
|
1355
|
-
* Handles `POST` requests containing JSON-RPC messages
|
|
585
|
+
* Handles `POST` requests containing JSON-RPC messages
|
|
1356
586
|
*/
|
|
1357
587
|
async handlePostRequest(req, options) {
|
|
1358
588
|
try {
|
|
@@ -1432,7 +662,7 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
1432
662
|
streamController = controller;
|
|
1433
663
|
},
|
|
1434
664
|
cancel: () => {
|
|
1435
|
-
this._streamMapping.delete(streamId);
|
|
665
|
+
if (this._streamMapping.get(streamId)?.controller === streamController) this._streamMapping.delete(streamId);
|
|
1436
666
|
}
|
|
1437
667
|
});
|
|
1438
668
|
const headers = {
|
|
@@ -1571,22 +801,40 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
1571
801
|
if (this._eventStore) eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
|
|
1572
802
|
const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
|
|
1573
803
|
if (standaloneSse === void 0) return;
|
|
1574
|
-
if (standaloneSse.controller && standaloneSse.encoder) this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
|
|
804
|
+
if (standaloneSse.controller && standaloneSse.encoder && (eventId === void 0 || !standaloneSse.replayedEventIds?.has(eventId))) this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
|
|
1575
805
|
return;
|
|
1576
806
|
}
|
|
1577
807
|
const streamId = this._requestToStreamMapping.get(requestId);
|
|
1578
808
|
if (!streamId) throw new Error(`No connection established for request ID: ${String(requestId)}`);
|
|
1579
|
-
|
|
1580
|
-
if (!this._enableJsonResponse
|
|
809
|
+
let stream = this._streamMapping.get(streamId);
|
|
810
|
+
if (!this._enableJsonResponse) {
|
|
1581
811
|
let eventId;
|
|
1582
|
-
if (this._eventStore)
|
|
1583
|
-
|
|
812
|
+
if (this._eventStore) {
|
|
813
|
+
eventId = await this._eventStore.storeEvent(streamId, message);
|
|
814
|
+
stream = this._streamMapping.get(streamId);
|
|
815
|
+
}
|
|
816
|
+
if (stream?.controller && stream?.encoder && (eventId === void 0 || !stream.replayedEventIds?.has(eventId))) this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
|
|
1584
817
|
}
|
|
1585
818
|
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
|
1586
819
|
this._requestResponseMap.set(requestId, message);
|
|
1587
820
|
const relatedIds = [...this._requestToStreamMapping.entries()].filter(([_, sid]) => sid === streamId).map(([id]) => id);
|
|
1588
821
|
if (relatedIds.every((id) => this._requestResponseMap.has(id))) {
|
|
1589
|
-
if (!stream)
|
|
822
|
+
if (!stream) {
|
|
823
|
+
if (this._enableJsonResponse) throw new Error(`No connection established for request ID: ${String(requestId)}`);
|
|
824
|
+
if (!this._eventStore) {
|
|
825
|
+
this.onerror?.(/* @__PURE__ */ new Error(`Response for request ID ${String(requestId)} is undeliverable: per-request stream is disconnected and no eventStore is configured`));
|
|
826
|
+
for (const id of relatedIds) {
|
|
827
|
+
this._requestResponseMap.delete(id);
|
|
828
|
+
this._requestToStreamMapping.delete(id);
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
for (const id of relatedIds) {
|
|
833
|
+
this._requestResponseMap.delete(id);
|
|
834
|
+
this._requestToStreamMapping.delete(id);
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
1590
838
|
if (this._enableJsonResponse && stream.resolveJson) {
|
|
1591
839
|
const headers = { "Content-Type": "application/json" };
|
|
1592
840
|
if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
|
|
@@ -1599,6 +847,7 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
1599
847
|
status: 200,
|
|
1600
848
|
headers
|
|
1601
849
|
}));
|
|
850
|
+
stream.cleanup();
|
|
1602
851
|
} else stream.cleanup();
|
|
1603
852
|
for (const id of relatedIds) {
|
|
1604
853
|
this._requestResponseMap.delete(id);
|
|
@@ -1609,6 +858,714 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
1609
858
|
}
|
|
1610
859
|
};
|
|
1611
860
|
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/server/createMcpHandler.ts
|
|
863
|
+
/**
|
|
864
|
+
* The JSON-RPC id to echo on an entry-built error response: the body's `id`
|
|
865
|
+
* when the body is a single JSON-RPC request whose id is a string or number,
|
|
866
|
+
* `null` otherwise. Error responses must carry the id of the request they
|
|
867
|
+
* correspond to whenever it could be read; `null` is reserved for the cases
|
|
868
|
+
* where no single request id is determinable — unparseable bodies, body-less
|
|
869
|
+
* methods, notifications, posted responses and batch arrays.
|
|
870
|
+
*/
|
|
871
|
+
function echoableRequestId(body) {
|
|
872
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
873
|
+
const { method, id } = body;
|
|
874
|
+
if (typeof method !== "string") return null;
|
|
875
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
876
|
+
}
|
|
877
|
+
function jsonRpcErrorResponse(httpStatus, code, message, data, id = null) {
|
|
878
|
+
return Response.json({
|
|
879
|
+
jsonrpc: "2.0",
|
|
880
|
+
error: {
|
|
881
|
+
code,
|
|
882
|
+
message,
|
|
883
|
+
...data !== void 0 && { data }
|
|
884
|
+
},
|
|
885
|
+
id
|
|
886
|
+
}, { status: httpStatus });
|
|
887
|
+
}
|
|
888
|
+
function rejectionResponse(rejection, id = null) {
|
|
889
|
+
return jsonRpcErrorResponse(rejection.httpStatus, rejection.code, rejection.message, rejection.data, id);
|
|
890
|
+
}
|
|
891
|
+
function toError(value) {
|
|
892
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
893
|
+
}
|
|
894
|
+
function internalServerErrorResponse(id = null) {
|
|
895
|
+
return jsonRpcErrorResponse(500, -32603, "Internal server error", void 0, id);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* The entry's default legacy serving (`legacy: 'stateless'`): per-request
|
|
899
|
+
* stateless serving of 2025-era traffic using the same factory as the modern
|
|
900
|
+
* path. Exported as a standalone building block for hand-wired compositions
|
|
901
|
+
* (for example mounting legacy stateless serving on its own route next to a
|
|
902
|
+
* strict modern endpoint).
|
|
903
|
+
*
|
|
904
|
+
* Each POST is served by a fresh instance from the factory connected to a
|
|
905
|
+
* fresh streamable HTTP transport constructed with only
|
|
906
|
+
* `sessionIdGenerator: undefined` — the established stateless idiom, unchanged.
|
|
907
|
+
* Because serving is per-request and stateless, GET and DELETE (2025 session
|
|
908
|
+
* operations) are answered with `405` / `Method not allowed.`, exactly like the
|
|
909
|
+
* canonical stateless example.
|
|
910
|
+
*
|
|
911
|
+
* The optional `onerror` callback receives factory and serving failures on
|
|
912
|
+
* this leg (reporting only — the response stays the 500 internal-error body).
|
|
913
|
+
* The entry passes its own `onerror` here when expanding the default, so
|
|
914
|
+
* legacy-leg failures are never silently swallowed.
|
|
915
|
+
*/
|
|
916
|
+
function legacyStatelessFallback(factory, onerror) {
|
|
917
|
+
return async (request, options) => {
|
|
918
|
+
if (request.method.toUpperCase() !== "POST") return jsonRpcErrorResponse(405, -32e3, "Method not allowed.");
|
|
919
|
+
try {
|
|
920
|
+
const product = await factory({
|
|
921
|
+
era: "legacy",
|
|
922
|
+
...options?.authInfo !== void 0 && { authInfo: options.authInfo },
|
|
923
|
+
requestInfo: request
|
|
924
|
+
});
|
|
925
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
926
|
+
await product.connect(transport);
|
|
927
|
+
const teardown = () => {
|
|
928
|
+
transport.close().catch(() => {});
|
|
929
|
+
product.close().catch(() => {});
|
|
930
|
+
};
|
|
931
|
+
request.signal?.addEventListener("abort", teardown, { once: true });
|
|
932
|
+
const response = await transport.handleRequest(request, {
|
|
933
|
+
...options?.authInfo !== void 0 && { authInfo: options.authInfo },
|
|
934
|
+
...options?.parsedBody !== void 0 && { parsedBody: options.parsedBody }
|
|
935
|
+
});
|
|
936
|
+
if (response.body === null || !(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
937
|
+
teardown();
|
|
938
|
+
return response;
|
|
939
|
+
}
|
|
940
|
+
const reader = response.body.getReader();
|
|
941
|
+
let toreDown = false;
|
|
942
|
+
const completeExchange = () => {
|
|
943
|
+
if (!toreDown) {
|
|
944
|
+
toreDown = true;
|
|
945
|
+
teardown();
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
const monitoredBody = new ReadableStream({
|
|
949
|
+
pull: async (controller) => {
|
|
950
|
+
try {
|
|
951
|
+
const { done, value } = await reader.read();
|
|
952
|
+
if (done) {
|
|
953
|
+
completeExchange();
|
|
954
|
+
controller.close();
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (value !== void 0) controller.enqueue(value);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
completeExchange();
|
|
960
|
+
controller.error(error);
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
cancel: (reason) => {
|
|
964
|
+
completeExchange();
|
|
965
|
+
return reader.cancel(reason).catch(() => {});
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
return new Response(monitoredBody, {
|
|
969
|
+
status: response.status,
|
|
970
|
+
statusText: response.statusText,
|
|
971
|
+
headers: response.headers
|
|
972
|
+
});
|
|
973
|
+
} catch (error) {
|
|
974
|
+
try {
|
|
975
|
+
onerror?.(toError(error));
|
|
976
|
+
} catch {}
|
|
977
|
+
return internalServerErrorResponse(echoableRequestId(options?.parsedBody));
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* The entry's classification step: read the request body exactly once (unless
|
|
983
|
+
* a pre-parsed body is supplied) and classify the request with
|
|
984
|
+
* {@linkcode classifyInboundRequest}. This is the single code path behind both
|
|
985
|
+
* {@linkcode createMcpHandler}'s routing and the exported
|
|
986
|
+
* {@linkcode isLegacyRequest} predicate, so the two can never disagree.
|
|
987
|
+
*
|
|
988
|
+
* Pass `needsForward: false` when the caller never reads `forwardRequest` —
|
|
989
|
+
* the body-preserving clone is then skipped and `forwardRequest` is the
|
|
990
|
+
* (consumed) input request.
|
|
991
|
+
*/
|
|
992
|
+
async function classifyEntryRequest(request, providedParsedBody, needsForward = true) {
|
|
993
|
+
const httpMethod = request.method.toUpperCase();
|
|
994
|
+
let body;
|
|
995
|
+
let parsedBody = providedParsedBody;
|
|
996
|
+
let forwardRequest = request;
|
|
997
|
+
let unparseable = false;
|
|
998
|
+
if (httpMethod === "POST") {
|
|
999
|
+
if (parsedBody === void 0) {
|
|
1000
|
+
if (needsForward) forwardRequest = request.clone();
|
|
1001
|
+
let bodyText;
|
|
1002
|
+
try {
|
|
1003
|
+
bodyText = await request.text();
|
|
1004
|
+
} catch {
|
|
1005
|
+
return { step: "unreadable-body" };
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
body = bodyText.length === 0 ? void 0 : JSON.parse(bodyText);
|
|
1009
|
+
} catch {
|
|
1010
|
+
unparseable = true;
|
|
1011
|
+
}
|
|
1012
|
+
if (!unparseable && body !== void 0) parsedBody = body;
|
|
1013
|
+
} else body = parsedBody;
|
|
1014
|
+
if (unparseable || body === void 0) return {
|
|
1015
|
+
step: "no-json-body",
|
|
1016
|
+
forwardRequest
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
step: "classified",
|
|
1021
|
+
outcome: classifyInboundRequest({
|
|
1022
|
+
httpMethod,
|
|
1023
|
+
protocolVersionHeader: request.headers.get("mcp-protocol-version") ?? void 0,
|
|
1024
|
+
mcpMethodHeader: request.headers.get("mcp-method") ?? void 0,
|
|
1025
|
+
mcpNameHeader: request.headers.get("mcp-name") ?? void 0,
|
|
1026
|
+
...body !== void 0 && { body }
|
|
1027
|
+
}),
|
|
1028
|
+
body,
|
|
1029
|
+
parsedBody,
|
|
1030
|
+
forwardRequest
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Whether {@linkcode createMcpHandler} would route this request to its legacy
|
|
1035
|
+
* (2025-era) serving rather than the modern (2026-07-28) path.
|
|
1036
|
+
*
|
|
1037
|
+
* Call it with just the request: `await isLegacyRequest(request)`. For a
|
|
1038
|
+
* `POST` the body is read from an internal clone, so the request you pass
|
|
1039
|
+
* stays fully readable for whichever handler you route it to — no second
|
|
1040
|
+
* argument is needed. (In a Node `(req, res)` handler, build that `Request`
|
|
1041
|
+
* with `toWebRequest(req)` from `@modelcontextprotocol/node`; behind a body
|
|
1042
|
+
* parser, which has already drained the Node stream, build it as
|
|
1043
|
+
* `toWebRequest(req, req.body)` so the bytes come from the parsed body —
|
|
1044
|
+
* either way the predicate still takes just the request.) The optional
|
|
1045
|
+
* `parsedBody` is a perf escape hatch for a body you already hold parsed:
|
|
1046
|
+
* pass it and the predicate classifies from the value directly, reading and
|
|
1047
|
+
* cloning nothing. It is needed, not just faster, when the request's own
|
|
1048
|
+
* body was already read — the internal clone is then impossible (cloning a
|
|
1049
|
+
* used body throws a `TypeError`), so such a single-argument call rejects
|
|
1050
|
+
* instead of guessing.
|
|
1051
|
+
*
|
|
1052
|
+
* This is the entry's own classification step exported as a predicate — it
|
|
1053
|
+
* runs exactly the code `createMcpHandler` runs to make the routing decision,
|
|
1054
|
+
* not a re-implementation — so a hand-wired composition that branches on it
|
|
1055
|
+
* can never disagree with the entry. Use it to keep an existing legacy
|
|
1056
|
+
* deployment (for example a sessionful streamable HTTP wiring) serving 2025
|
|
1057
|
+
* traffic next to a strict modern endpoint, now that the entry has no
|
|
1058
|
+
* handler-valued `legacy` option:
|
|
1059
|
+
*
|
|
1060
|
+
* ```ts
|
|
1061
|
+
* import { createMcpHandler, isLegacyRequest } from '@modelcontextprotocol/server';
|
|
1062
|
+
*
|
|
1063
|
+
* const modern = createMcpHandler(factory, { legacy: 'reject' });
|
|
1064
|
+
*
|
|
1065
|
+
* export default {
|
|
1066
|
+
* async fetch(request: Request): Promise<Response> {
|
|
1067
|
+
* if (await isLegacyRequest(request)) {
|
|
1068
|
+
* // e.g. an existing sessionful WebStandardStreamableHTTPServerTransport wiring
|
|
1069
|
+
* return myExistingLegacyHandler(request);
|
|
1070
|
+
* }
|
|
1071
|
+
* return modern.fetch(request);
|
|
1072
|
+
* }
|
|
1073
|
+
* };
|
|
1074
|
+
* ```
|
|
1075
|
+
*
|
|
1076
|
+
* Semantics (identical to the entry's routing):
|
|
1077
|
+
*
|
|
1078
|
+
* - Returns `true` only for requests with no per-request `_meta` envelope
|
|
1079
|
+
* claim: claim-less POSTs (including the `initialize` handshake and 2025-era
|
|
1080
|
+
* notification POSTs without a modern protocol-version header), body-less
|
|
1081
|
+
* GET/DELETE session operations, all-legacy JSON-RPC batch arrays, posted
|
|
1082
|
+
* JSON-RPC responses, and POSTs whose body is empty or not valid JSON.
|
|
1083
|
+
* - Returns `false` for everything the modern path answers, including its
|
|
1084
|
+
* validation-ladder rejections: a request carrying the envelope claim (even
|
|
1085
|
+
* one naming a revision the endpoint does not serve — the modern path
|
|
1086
|
+
* answers it with the unsupported-protocol-version error), a malformed
|
|
1087
|
+
* envelope behind a present claim (answered `-32602`), a request whose
|
|
1088
|
+
* `MCP-Protocol-Version` header names a modern revision but that lacks the
|
|
1089
|
+
* envelope (`-32602`), and header/body mismatches (`-32020`). Consumers
|
|
1090
|
+
* routing on the predicate must send `false` traffic to the modern handler,
|
|
1091
|
+
* never to a legacy handler — the modern path owns those error answers.
|
|
1092
|
+
* - `server/discover` probes sent by negotiating clients always carry the
|
|
1093
|
+
* envelope claim, so they are never legacy; a hand-built claim-less POST to
|
|
1094
|
+
* a method named `server/discover` has no claim and classifies legacy,
|
|
1095
|
+
* exactly as the entry itself routes it.
|
|
1096
|
+
*/
|
|
1097
|
+
async function isLegacyRequest(request, parsedBody) {
|
|
1098
|
+
const classified = await classifyEntryRequest(parsedBody === void 0 && request.method.toUpperCase() === "POST" ? request.clone() : request, parsedBody, false);
|
|
1099
|
+
return classified.step === "no-json-body" || classified.step === "classified" && classified.outcome.kind === "legacy";
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Creates an HTTP handler that serves the 2026-07-28 protocol revision from a
|
|
1103
|
+
* per-request server factory and, by default, falls back to old-school
|
|
1104
|
+
* stateless serving for 2025-era traffic. Pass `legacy: 'reject'` for a
|
|
1105
|
+
* modern-only strict endpoint.
|
|
1106
|
+
*
|
|
1107
|
+
* Mounting: `handler.fetch` is the web-standard face (Cloudflare Workers,
|
|
1108
|
+
* Deno, Bun, Hono's `c.req.raw`); for Express/Fastify/plain `node:http`, wrap
|
|
1109
|
+
* the handler once with `toNodeHandler(handler)` from
|
|
1110
|
+
* `@modelcontextprotocol/node`. When mounting bare on a fetch-native runtime,
|
|
1111
|
+
* put Origin/Host validation in front of the handler — the entry itself is
|
|
1112
|
+
* deliberately validation-free:
|
|
1113
|
+
*
|
|
1114
|
+
* ```ts
|
|
1115
|
+
* import { hostHeaderValidationResponse, originValidationResponse, localhostAllowedHostnames, localhostAllowedOrigins } from '@modelcontextprotocol/server';
|
|
1116
|
+
*
|
|
1117
|
+
* export default {
|
|
1118
|
+
* async fetch(request: Request): Promise<Response> {
|
|
1119
|
+
* const rejected =
|
|
1120
|
+
* hostHeaderValidationResponse(request, localhostAllowedHostnames()) ??
|
|
1121
|
+
* originValidationResponse(request, localhostAllowedOrigins());
|
|
1122
|
+
* return rejected ?? handler.fetch(request);
|
|
1123
|
+
* }
|
|
1124
|
+
* };
|
|
1125
|
+
* ```
|
|
1126
|
+
*
|
|
1127
|
+
* Use ONE factory for both legs: the same tools/resources/prompts definition
|
|
1128
|
+
* backs the modern path and the stateless legacy fallback, so the two eras can
|
|
1129
|
+
* never drift apart. To keep an existing legacy deployment (for example a
|
|
1130
|
+
* sessionful streamable HTTP wiring) serving 2025 traffic instead of the
|
|
1131
|
+
* stateless fallback, route in user land with {@linkcode isLegacyRequest} in
|
|
1132
|
+
* front of a strict handler — see that predicate's documentation for the
|
|
1133
|
+
* pattern. Power users composing transport-neutral routing can also use the
|
|
1134
|
+
* exported building blocks directly: {@linkcode classifyInboundRequest} for
|
|
1135
|
+
* the era decision and `PerRequestHTTPServerTransport` for single-exchange
|
|
1136
|
+
* serving.
|
|
1137
|
+
*
|
|
1138
|
+
* The entry performs no token verification: `authInfo` given to `fetch` is
|
|
1139
|
+
* passed through to handlers and the factory as-is and is never derived from
|
|
1140
|
+
* request headers.
|
|
1141
|
+
*/
|
|
1142
|
+
function createMcpHandler(factory, options = {}) {
|
|
1143
|
+
const { legacy, onerror, responseMode } = options;
|
|
1144
|
+
if (typeof legacy === "function") throw new TypeError("The 'legacy' option only accepts 'stateless' or 'reject', not a handler function. To serve 2025-era traffic with your own handler, route in user land with the exported isLegacyRequest(request) predicate in front of a strict (legacy: 'reject') handler.");
|
|
1145
|
+
/** Modern per-request instances with an exchange still in flight (close() tears these down). */
|
|
1146
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
1147
|
+
let closed = false;
|
|
1148
|
+
const reportError = (error) => {
|
|
1149
|
+
try {
|
|
1150
|
+
onerror?.(error);
|
|
1151
|
+
} catch {}
|
|
1152
|
+
};
|
|
1153
|
+
const bus = options.bus ?? new InMemoryServerEventBus(reportError);
|
|
1154
|
+
const notify = createServerNotifier(bus);
|
|
1155
|
+
const listenRouter = createListenRouter({
|
|
1156
|
+
bus,
|
|
1157
|
+
maxSubscriptions: options.maxSubscriptions ?? DEFAULT_MAX_SUBSCRIPTIONS,
|
|
1158
|
+
keepAliveMs: options.keepAliveMs ?? DEFAULT_LISTEN_KEEPALIVE_MS,
|
|
1159
|
+
onerror: reportError
|
|
1160
|
+
});
|
|
1161
|
+
if (responseMode === "json") console.warn("responseMode: 'json' drops mid-call notifications. subscriptions/listen streams are always served over SSE regardless; other notifications emitted before a result are dropped.");
|
|
1162
|
+
const legacyHandler = legacy === "reject" ? void 0 : legacyStatelessFallback(factory, reportError);
|
|
1163
|
+
async function serveModern(route, request, authInfo) {
|
|
1164
|
+
const claimedRevision = route.classification.revision;
|
|
1165
|
+
if (claimedRevision === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedRevision)) {
|
|
1166
|
+
const error = new UnsupportedProtocolVersionError({
|
|
1167
|
+
supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
|
|
1168
|
+
requested: claimedRevision ?? "unknown"
|
|
1169
|
+
});
|
|
1170
|
+
reportError(error);
|
|
1171
|
+
return jsonRpcErrorResponse(400, error.code, error.message, error.data, echoableRequestId(route.message));
|
|
1172
|
+
}
|
|
1173
|
+
const stdHeaderRejection = validateStandardRequestHeaders({
|
|
1174
|
+
httpMethod: request.method,
|
|
1175
|
+
mcpMethodHeader: request.headers.get("mcp-method") ?? void 0,
|
|
1176
|
+
mcpNameHeader: request.headers.get("mcp-name") ?? void 0
|
|
1177
|
+
}, route);
|
|
1178
|
+
if (stdHeaderRejection !== void 0) {
|
|
1179
|
+
reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${stdHeaderRejection.cell}): ${stdHeaderRejection.message}`));
|
|
1180
|
+
return rejectionResponse(stdHeaderRejection, echoableRequestId(route.message));
|
|
1181
|
+
}
|
|
1182
|
+
const meta = route.messageKind === "request" ? requestMetaOf(route.message.params) : void 0;
|
|
1183
|
+
const declaredClientCapabilities = meta?.[CLIENT_CAPABILITIES_META_KEY];
|
|
1184
|
+
if (route.messageKind === "request") {
|
|
1185
|
+
const required = requiredClientCapabilitiesForRequest(route.message.method);
|
|
1186
|
+
if (required !== void 0) {
|
|
1187
|
+
const missing = missingClientCapabilities(required, declaredClientCapabilities);
|
|
1188
|
+
if (missing !== void 0) {
|
|
1189
|
+
const error = new MissingRequiredClientCapabilityError({ requiredCapabilities: missing });
|
|
1190
|
+
reportError(error);
|
|
1191
|
+
return jsonRpcErrorResponse(httpStatusForErrorCode(error.code, "ladder"), error.code, error.message, error.data, route.message.id);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const product = await factory({
|
|
1196
|
+
era: "modern",
|
|
1197
|
+
...authInfo !== void 0 && { authInfo },
|
|
1198
|
+
requestInfo: request
|
|
1199
|
+
});
|
|
1200
|
+
const server = product instanceof McpServer ? product.server : product;
|
|
1201
|
+
if (route.messageKind === "request" && route.message.method === "subscriptions/listen") {
|
|
1202
|
+
const capabilities = server.getCapabilities();
|
|
1203
|
+
product.close().catch(reportError);
|
|
1204
|
+
return listenRouter.serve(route.message, request.signal, capabilities);
|
|
1205
|
+
}
|
|
1206
|
+
if (route.messageKind === "request" && route.message.method === "tools/call" && product instanceof McpServer) {
|
|
1207
|
+
const callParams = route.message.params;
|
|
1208
|
+
const toolName = typeof callParams?.name === "string" ? callParams.name : void 0;
|
|
1209
|
+
const inputSchema = toolName === void 0 ? void 0 : product.toolInputSchemaJson(toolName);
|
|
1210
|
+
if (inputSchema !== void 0) {
|
|
1211
|
+
const scan = scanXMcpHeaderDeclarations(inputSchema);
|
|
1212
|
+
if (scan.valid && scan.declarations.length > 0) {
|
|
1213
|
+
const rejection = validateMcpParamHeaders(scan.declarations, callParams?.arguments, request.headers);
|
|
1214
|
+
if (rejection !== void 0) {
|
|
1215
|
+
product.close().catch(reportError);
|
|
1216
|
+
reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${rejection.cell}): ${rejection.message}`));
|
|
1217
|
+
return rejectionResponse(rejection, route.message.id);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
setNegotiatedProtocolVersion(server, claimedRevision);
|
|
1223
|
+
installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
1224
|
+
if (meta !== void 0) seedClientIdentityFromEnvelope(server, {
|
|
1225
|
+
clientInfo: meta[CLIENT_INFO_META_KEY],
|
|
1226
|
+
clientCapabilities: declaredClientCapabilities
|
|
1227
|
+
});
|
|
1228
|
+
const previousOnClose = server.onclose;
|
|
1229
|
+
inflight.add(server);
|
|
1230
|
+
server.onclose = () => {
|
|
1231
|
+
inflight.delete(server);
|
|
1232
|
+
previousOnClose?.();
|
|
1233
|
+
};
|
|
1234
|
+
try {
|
|
1235
|
+
const response = await invoke(product, route.message, {
|
|
1236
|
+
classification: route.classification,
|
|
1237
|
+
request,
|
|
1238
|
+
...authInfo !== void 0 && { authInfo },
|
|
1239
|
+
...responseMode !== void 0 && { responseMode }
|
|
1240
|
+
});
|
|
1241
|
+
if (route.messageKind === "notification") queueMicrotask(() => void server.close().catch(() => {}));
|
|
1242
|
+
return response;
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
if (error instanceof SdkError && error.code === SdkErrorCode.ConnectionClosed) return new Response(null, { status: 499 });
|
|
1245
|
+
await server.close().catch(() => {});
|
|
1246
|
+
inflight.delete(server);
|
|
1247
|
+
reportError(toError(error));
|
|
1248
|
+
return internalServerErrorResponse(echoableRequestId(route.message));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function serveLegacyRoute(route, forwardRequest, authInfo, parsedBody) {
|
|
1252
|
+
if (legacyHandler !== void 0) return legacyHandler(forwardRequest, {
|
|
1253
|
+
...authInfo !== void 0 && { authInfo },
|
|
1254
|
+
...parsedBody !== void 0 && { parsedBody }
|
|
1255
|
+
});
|
|
1256
|
+
const strict = modernOnlyStrictRejection(route, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
1257
|
+
if (strict === void 0) return new Response(null, { status: 202 });
|
|
1258
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only endpoint (${strict.cell}): ${strict.message}`));
|
|
1259
|
+
return rejectionResponse(strict, echoableRequestId(parsedBody));
|
|
1260
|
+
}
|
|
1261
|
+
async function handle(request, requestOptions) {
|
|
1262
|
+
const authInfo = requestOptions?.authInfo;
|
|
1263
|
+
const classified = await classifyEntryRequest(request, requestOptions?.parsedBody);
|
|
1264
|
+
if (classified.step === "unreadable-body") return jsonRpcErrorResponse(400, -32700, "Parse error: the request body could not be read");
|
|
1265
|
+
if (classified.step === "no-json-body") {
|
|
1266
|
+
if (legacyHandler !== void 0) return legacyHandler(classified.forwardRequest, { ...authInfo !== void 0 && { authInfo } });
|
|
1267
|
+
return jsonRpcErrorResponse(400, -32700, "Parse error: the request body is not valid JSON");
|
|
1268
|
+
}
|
|
1269
|
+
const { outcome, body, parsedBody, forwardRequest } = classified;
|
|
1270
|
+
try {
|
|
1271
|
+
switch (outcome.kind) {
|
|
1272
|
+
case "reject":
|
|
1273
|
+
reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${outcome.cell}): ${outcome.message}`));
|
|
1274
|
+
return rejectionResponse(outcome, echoableRequestId(body));
|
|
1275
|
+
case "modern": return await serveModern(outcome, request, authInfo);
|
|
1276
|
+
case "legacy": return await serveLegacyRoute(outcome, forwardRequest, authInfo, parsedBody);
|
|
1277
|
+
}
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
reportError(toError(error));
|
|
1280
|
+
return internalServerErrorResponse(echoableRequestId(body));
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
const fetchFace = async (request, requestOptions) => {
|
|
1284
|
+
if (closed) throw new Error("This MCP handler has been closed");
|
|
1285
|
+
try {
|
|
1286
|
+
return await handle(request, requestOptions);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
reportError(toError(error));
|
|
1289
|
+
return internalServerErrorResponse(echoableRequestId(requestOptions?.parsedBody));
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
return {
|
|
1293
|
+
fetch: fetchFace,
|
|
1294
|
+
notify,
|
|
1295
|
+
bus,
|
|
1296
|
+
close: async () => {
|
|
1297
|
+
closed = true;
|
|
1298
|
+
listenRouter.closeAll();
|
|
1299
|
+
const closing = [...inflight].map((server) => server.close().catch(() => {}));
|
|
1300
|
+
inflight.clear();
|
|
1301
|
+
await Promise.all(closing);
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/server/middleware/hostHeaderValidation.ts
|
|
1308
|
+
/**
|
|
1309
|
+
* Parse and validate a `Host` header against an allowlist of hostnames (port-agnostic).
|
|
1310
|
+
*
|
|
1311
|
+
* - Input host header may include a port (e.g. `localhost:3000`) or IPv6 brackets (e.g. `[::1]:3000`).
|
|
1312
|
+
* - Allowlist items should be hostnames only (no ports). For IPv6, include brackets (e.g. `[::1]`).
|
|
1313
|
+
*/
|
|
1314
|
+
function validateHostHeader(hostHeader, allowedHostnames) {
|
|
1315
|
+
if (!hostHeader) return {
|
|
1316
|
+
ok: false,
|
|
1317
|
+
errorCode: "missing_host",
|
|
1318
|
+
message: "Missing Host header"
|
|
1319
|
+
};
|
|
1320
|
+
let hostname;
|
|
1321
|
+
try {
|
|
1322
|
+
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
1323
|
+
} catch {
|
|
1324
|
+
return {
|
|
1325
|
+
ok: false,
|
|
1326
|
+
errorCode: "invalid_host_header",
|
|
1327
|
+
message: `Invalid Host header: ${hostHeader}`,
|
|
1328
|
+
hostHeader
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
if (!allowedHostnames.includes(hostname)) return {
|
|
1332
|
+
ok: false,
|
|
1333
|
+
errorCode: "invalid_host",
|
|
1334
|
+
message: `Invalid Host: ${hostname}`,
|
|
1335
|
+
hostHeader,
|
|
1336
|
+
hostname
|
|
1337
|
+
};
|
|
1338
|
+
return {
|
|
1339
|
+
ok: true,
|
|
1340
|
+
hostname
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Convenience allowlist for `localhost` DNS rebinding protection.
|
|
1345
|
+
*/
|
|
1346
|
+
function localhostAllowedHostnames() {
|
|
1347
|
+
return [
|
|
1348
|
+
"localhost",
|
|
1349
|
+
"127.0.0.1",
|
|
1350
|
+
"[::1]"
|
|
1351
|
+
];
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Web-standard `Request` helper for DNS rebinding protection.
|
|
1355
|
+
* @example
|
|
1356
|
+
* ```ts source="./hostHeaderValidation.examples.ts#hostHeaderValidationResponse_basicUsage"
|
|
1357
|
+
* const result = validateHostHeader(req.headers.get('host'), ['localhost']);
|
|
1358
|
+
* ```
|
|
1359
|
+
*/
|
|
1360
|
+
function hostHeaderValidationResponse(req, allowedHostnames) {
|
|
1361
|
+
const result = validateHostHeader(req.headers.get("host"), allowedHostnames);
|
|
1362
|
+
if (result.ok) return void 0;
|
|
1363
|
+
return Response.json({
|
|
1364
|
+
jsonrpc: "2.0",
|
|
1365
|
+
error: {
|
|
1366
|
+
code: -32e3,
|
|
1367
|
+
message: result.message
|
|
1368
|
+
},
|
|
1369
|
+
id: null
|
|
1370
|
+
}, {
|
|
1371
|
+
status: 403,
|
|
1372
|
+
headers: { "Content-Type": "application/json" }
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
//#endregion
|
|
1377
|
+
//#region src/server/middleware/originValidation.ts
|
|
1378
|
+
/**
|
|
1379
|
+
* Validate an `Origin` header against an allowlist of hostnames (port-agnostic).
|
|
1380
|
+
*
|
|
1381
|
+
* - A missing/empty `Origin` header passes: non-browser clients do not send one,
|
|
1382
|
+
* and only browser-originated requests carry the header this check defends against.
|
|
1383
|
+
* - Allowlist items are hostnames only (no scheme, no port), the same convention as
|
|
1384
|
+
* `validateHostHeader`. For IPv6, include brackets (e.g. `[::1]`).
|
|
1385
|
+
* - Any present value that cannot be parsed as an origin URL — including the literal
|
|
1386
|
+
* `null` origin browsers send for opaque contexts — is rejected (deny on failure).
|
|
1387
|
+
*/
|
|
1388
|
+
function validateOriginHeader(originHeader, allowedOriginHostnames) {
|
|
1389
|
+
if (originHeader === null || originHeader === void 0 || originHeader === "") return { ok: true };
|
|
1390
|
+
let hostname;
|
|
1391
|
+
try {
|
|
1392
|
+
hostname = new URL(originHeader).hostname;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return {
|
|
1395
|
+
ok: false,
|
|
1396
|
+
errorCode: "invalid_origin_header",
|
|
1397
|
+
message: `Invalid Origin header: ${originHeader}`,
|
|
1398
|
+
originHeader
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
if (hostname === "") return {
|
|
1402
|
+
ok: false,
|
|
1403
|
+
errorCode: "invalid_origin_header",
|
|
1404
|
+
message: `Invalid Origin header: ${originHeader}`,
|
|
1405
|
+
originHeader
|
|
1406
|
+
};
|
|
1407
|
+
if (!allowedOriginHostnames.includes(hostname)) return {
|
|
1408
|
+
ok: false,
|
|
1409
|
+
errorCode: "invalid_origin",
|
|
1410
|
+
message: `Invalid Origin: ${hostname}`,
|
|
1411
|
+
originHeader,
|
|
1412
|
+
hostname
|
|
1413
|
+
};
|
|
1414
|
+
return {
|
|
1415
|
+
ok: true,
|
|
1416
|
+
origin: originHeader,
|
|
1417
|
+
hostname
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Convenience allowlist of localhost-class origin hostnames, mirroring
|
|
1422
|
+
* `localhostAllowedHostnames`.
|
|
1423
|
+
*/
|
|
1424
|
+
function localhostAllowedOrigins() {
|
|
1425
|
+
return [
|
|
1426
|
+
"localhost",
|
|
1427
|
+
"127.0.0.1",
|
|
1428
|
+
"[::1]"
|
|
1429
|
+
];
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Web-standard `Request` helper for Origin validation: returns a `403` JSON-RPC
|
|
1433
|
+
* error response when the request's `Origin` header is not allowed, and
|
|
1434
|
+
* `undefined` when the request may proceed.
|
|
1435
|
+
*
|
|
1436
|
+
* ```ts
|
|
1437
|
+
* const rejected = originValidationResponse(request, localhostAllowedOrigins());
|
|
1438
|
+
* if (rejected) return rejected;
|
|
1439
|
+
* ```
|
|
1440
|
+
*/
|
|
1441
|
+
function originValidationResponse(req, allowedOriginHostnames) {
|
|
1442
|
+
const result = validateOriginHeader(req.headers.get("origin"), allowedOriginHostnames);
|
|
1443
|
+
if (result.ok) return void 0;
|
|
1444
|
+
return Response.json({
|
|
1445
|
+
jsonrpc: "2.0",
|
|
1446
|
+
error: {
|
|
1447
|
+
code: -32e3,
|
|
1448
|
+
message: result.message
|
|
1449
|
+
},
|
|
1450
|
+
id: null
|
|
1451
|
+
}, {
|
|
1452
|
+
status: 403,
|
|
1453
|
+
headers: { "Content-Type": "application/json" }
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/server/requestStateCodec.ts
|
|
1459
|
+
const PREFIX = "v1.";
|
|
1460
|
+
function bytesToBase64Url(bytes) {
|
|
1461
|
+
let bin = "";
|
|
1462
|
+
for (const b of bytes) bin += String.fromCodePoint(b);
|
|
1463
|
+
return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
1464
|
+
}
|
|
1465
|
+
function constantTimeTagEqual(a, b) {
|
|
1466
|
+
if (a.length !== b.length) return false;
|
|
1467
|
+
let r = 0;
|
|
1468
|
+
for (let i = 0; i < a.length; i++) r |= a.codePointAt(i) ^ b.codePointAt(i);
|
|
1469
|
+
return r === 0;
|
|
1470
|
+
}
|
|
1471
|
+
function base64UrlToBytes(s) {
|
|
1472
|
+
const b64 = s.replaceAll("-", "+").replaceAll("_", "/");
|
|
1473
|
+
const bin = atob(b64);
|
|
1474
|
+
const bytes = new Uint8Array(bin.length);
|
|
1475
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.codePointAt(i);
|
|
1476
|
+
return bytes;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Create an opt-in HMAC-SHA256 codec for the multi-round-trip `requestState`
|
|
1480
|
+
* (protocol revision 2026-07-28).
|
|
1481
|
+
*
|
|
1482
|
+
* `requestState` round-trips through the client and is attacker-controlled
|
|
1483
|
+
* input on re-entry. The SDK applies no protection of its own; this helper is
|
|
1484
|
+
* the convenience implementation of the spec's integrity MUST so authors don't
|
|
1485
|
+
* hand-roll HMAC. Wire shape:
|
|
1486
|
+
*
|
|
1487
|
+
* "v1." b64url({"p":<payload>,"exp":<unixSeconds>,"b":<bindTag>?}) "." b64url(mac)
|
|
1488
|
+
*
|
|
1489
|
+
* where `bindTag` is `b64url(HMAC(key, "mcp.requestState.bind:" + bind(ctx))[:16])`
|
|
1490
|
+
* — the binding value is never embedded raw.
|
|
1491
|
+
*
|
|
1492
|
+
* The codec is **signed, not encrypted**: the body is integrity-protected but
|
|
1493
|
+
* the client can base64url-decode it and read the payload (`p`) in clear. Do
|
|
1494
|
+
* not put secrets in the payload; use an AEAD construction if confidentiality
|
|
1495
|
+
* is required. The handler reads its payload back via the typed
|
|
1496
|
+
* `ctx.mcpReq.requestState<T>()` accessor — the seam has already run `verify`
|
|
1497
|
+
* (integrity proven, payload decoded) by the time the handler is entered.
|
|
1498
|
+
*
|
|
1499
|
+
* Verification is fail-closed and constant-time (WebCrypto `subtle.verify` for
|
|
1500
|
+
* the body MAC; a fixed-length XOR-accumulator compare for the bind tag).
|
|
1501
|
+
* See `examples/mrtr/server.ts` for a worked end-to-end example.
|
|
1502
|
+
*
|
|
1503
|
+
* Design comparison (mcp.d `secureRequestState`, the peer SDK's reference
|
|
1504
|
+
* implementation): mcp.d additionally offers an AES-256-GCM encrypted mode and
|
|
1505
|
+
* derives independent cipher / bind-HMAC sub-keys from the operator secret via
|
|
1506
|
+
* HKDF-SHA256, with an auto-generated per-process ephemeral key when none is
|
|
1507
|
+
* supplied. This codec deliberately ships only the signed mode and a single
|
|
1508
|
+
* keyed HMAC (domain-separated by input prefix) — HKDF sub-key derivation and
|
|
1509
|
+
* an encrypted mode are intentionally out of scope for the initial release.
|
|
1510
|
+
*/
|
|
1511
|
+
function createRequestStateCodec(options) {
|
|
1512
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1513
|
+
if (subtle === void 0) throw new TypeError("createRequestStateCodec requires the Web Crypto API (globalThis.crypto.subtle); see https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting for the Node.js polyfill instructions");
|
|
1514
|
+
const keyBytes = typeof options.key === "string" ? new TextEncoder().encode(options.key) : Uint8Array.from(options.key);
|
|
1515
|
+
if (keyBytes.byteLength < 32) throw new RangeError(`createRequestStateCodec: key must be at least 32 bytes (got ${keyBytes.byteLength})`);
|
|
1516
|
+
const ttlSeconds = options.ttlSeconds ?? 600;
|
|
1517
|
+
if (!Number.isFinite(ttlSeconds)) throw new RangeError("createRequestStateCodec: ttlSeconds must be a finite number");
|
|
1518
|
+
const bind = options.bind;
|
|
1519
|
+
let cryptoKey;
|
|
1520
|
+
const importedKey = () => cryptoKey ??= subtle.importKey("raw", keyBytes, {
|
|
1521
|
+
name: "HMAC",
|
|
1522
|
+
hash: "SHA-256"
|
|
1523
|
+
}, false, ["sign", "verify"]);
|
|
1524
|
+
const utf8 = new TextEncoder();
|
|
1525
|
+
const BIND_LABEL = "mcp.requestState.bind:";
|
|
1526
|
+
const bindTag = async (value) => {
|
|
1527
|
+
return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(BIND_LABEL + value))).slice(0, 16));
|
|
1528
|
+
};
|
|
1529
|
+
return {
|
|
1530
|
+
async mint(payload, ctx) {
|
|
1531
|
+
const envelope = {
|
|
1532
|
+
p: payload,
|
|
1533
|
+
exp: Math.floor(Date.now() / 1e3) + ttlSeconds
|
|
1534
|
+
};
|
|
1535
|
+
if (bind !== void 0) {
|
|
1536
|
+
if (ctx === void 0) throw new TypeError("createRequestStateCodec: mint() requires ctx when a bind callback is configured");
|
|
1537
|
+
envelope.b = await bindTag(bind(ctx));
|
|
1538
|
+
}
|
|
1539
|
+
const body = bytesToBase64Url(utf8.encode(JSON.stringify(envelope)));
|
|
1540
|
+
return `${PREFIX}${body}.${bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(PREFIX + body))))}`;
|
|
1541
|
+
},
|
|
1542
|
+
async verify(state, ctx) {
|
|
1543
|
+
const dot = state.lastIndexOf(".");
|
|
1544
|
+
if (!state.startsWith(PREFIX) || dot <= 3) throw new Error("malformed");
|
|
1545
|
+
const body = state.slice(3, dot);
|
|
1546
|
+
let macBytes;
|
|
1547
|
+
try {
|
|
1548
|
+
macBytes = base64UrlToBytes(state.slice(dot + 1));
|
|
1549
|
+
} catch {
|
|
1550
|
+
throw new Error("malformed");
|
|
1551
|
+
}
|
|
1552
|
+
if (!await subtle.verify("HMAC", await importedKey(), macBytes, utf8.encode(PREFIX + body))) throw new Error("mac");
|
|
1553
|
+
let envelope;
|
|
1554
|
+
try {
|
|
1555
|
+
envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlToBytes(body)));
|
|
1556
|
+
} catch {
|
|
1557
|
+
throw new Error("malformed");
|
|
1558
|
+
}
|
|
1559
|
+
if (typeof envelope.exp !== "number" || envelope.exp < Math.floor(Date.now() / 1e3)) throw new Error("expired");
|
|
1560
|
+
if (bind !== void 0) {
|
|
1561
|
+
const expected = await bindTag(bind(ctx));
|
|
1562
|
+
if (envelope.b === void 0 || !constantTimeTagEqual(envelope.b, expected)) throw new Error("bind");
|
|
1563
|
+
} else if (envelope.b !== void 0) throw new Error("bind");
|
|
1564
|
+
return envelope.p;
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1612
1569
|
//#endregion
|
|
1613
1570
|
//#region src/fromJsonSchema.ts
|
|
1614
1571
|
let _defaultValidator;
|
|
@@ -1617,5 +1574,5 @@ function fromJsonSchema(schema, validator) {
|
|
|
1617
1574
|
}
|
|
1618
1575
|
|
|
1619
1576
|
//#endregion
|
|
1620
|
-
export { BAGGAGE_META_KEY, CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, DEFAULT_NEGOTIATED_PROTOCOL_VERSION, DEFAULT_REQUEST_TIMEOUT_MSEC, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, InMemoryTransport, JSONRPC_VERSION, LATEST_PROTOCOL_VERSION, LOG_LEVEL_META_KEY, METHOD_NOT_FOUND, McpServer, OAuthError, OAuthErrorCode, PARSE_ERROR, PROTOCOL_VERSION_META_KEY, ProtocolError, ProtocolErrorCode, RELATED_TASK_META_KEY, ReadBuffer, ResourceTemplate, STDIO_DEFAULT_MAX_BUFFER_SIZE, SUPPORTED_PROTOCOL_VERSIONS, SdkError, SdkErrorCode, SdkHttpError, Server, TRACEPARENT_META_KEY, TRACESTATE_META_KEY, UnsupportedProtocolVersionError, UriTemplate, UrlElicitationRequiredError, WebStandardStreamableHTTPServerTransport, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, checkResourceAllowed, completable, createFetchWithInit, deserializeMessage, fromJsonSchema, getDisplayName, hostHeaderValidationResponse, isCallToolResult, isCompletable, isInitializeRequest, isInitializedNotification, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResponse, isJSONRPCResultResponse, isSpecType, isTaskAugmentedRequestParams, localhostAllowedHostnames, parseJSONRPCMessage, resourceUrlFromServerUrl, serializeMessage, specTypeSchemas, validateHostHeader };
|
|
1577
|
+
export { BAGGAGE_META_KEY, CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, DEFAULT_NEGOTIATED_PROTOCOL_VERSION, DEFAULT_REQUEST_TIMEOUT_MSEC, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, InMemoryServerEventBus, InMemoryTransport, JSONRPC_VERSION, LATEST_PROTOCOL_VERSION, LOG_LEVEL_META_KEY, METHOD_NOT_FOUND, McpServer, MissingRequiredClientCapabilityError, OAuthError, OAuthErrorCode, PARSE_ERROR, PROTOCOL_VERSION_META_KEY, PerRequestHTTPServerTransport, ProtocolError, ProtocolErrorCode, RELATED_TASK_META_KEY, ReadBuffer, ResourceNotFoundError, ResourceTemplate, STDIO_DEFAULT_MAX_BUFFER_SIZE, SUBSCRIPTION_ID_META_KEY, SUPPORTED_PROTOCOL_VERSIONS, SdkError, SdkErrorCode, SdkHttpError, Server, TRACEPARENT_META_KEY, TRACESTATE_META_KEY, UnsupportedProtocolVersionError, UriTemplate, UrlElicitationRequiredError, WebStandardStreamableHTTPServerTransport, acceptedContent, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, checkResourceAllowed, classifyInboundRequest, completable, createFetchWithInit, createMcpHandler, createRequestStateCodec, deserializeMessage, fromJsonSchema, getDisplayName, hostHeaderValidationResponse, inputRequired, inputResponse, isCallToolResult, isCompletable, isInitializeRequest, isInitializedNotification, isInputRequiredResult, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResponse, isJSONRPCResultResponse, isLegacyRequest, isSpecType, isTaskAugmentedRequestParams, legacyStatelessFallback, localhostAllowedHostnames, localhostAllowedOrigins, originValidationResponse, parseJSONRPCMessage, resourceUrlFromServerUrl, serializeMessage, specTypeSchemas, validateHostHeader, validateOriginHeader };
|
|
1621
1578
|
//# sourceMappingURL=index.mjs.map
|