tiny-http-mcp-server 0.1.0
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/LICENSE +21 -0
- package/README.md +778 -0
- package/dist/auth.d.ts +69 -0
- package/dist/auth.js +261 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +465 -0
- package/dist/composition.json +25 -0
- package/dist/express-middleware.d.ts +18 -0
- package/dist/express-middleware.js +91 -0
- package/dist/http-server.d.ts +48 -0
- package/dist/http-server.js +263 -0
- package/dist/http-transport.d.ts +164 -0
- package/dist/http-transport.js +897 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +6 -0
- package/dist/load-oauth-verifier.d.ts +6 -0
- package/dist/load-oauth-verifier.js +43 -0
- package/dist/parse-body.d.ts +22 -0
- package/dist/parse-body.js +150 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.js +37 -0
- package/dist/sse.d.ts +11 -0
- package/dist/sse.js +22 -0
- package/dist/test-support.d.ts +10 -0
- package/dist/test-support.js +398 -0
- package/dist/testing.d.ts +59 -0
- package/dist/testing.js +191 -0
- package/node_modules/auth-store/LICENSE +21 -0
- package/node_modules/auth-store/README.md +62 -0
- package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
- package/node_modules/auth-store/dist/create-secret-store.js +44 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +47 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +303 -0
- package/node_modules/auth-store/dist/error-codes.d.ts +1 -0
- package/node_modules/auth-store/dist/error-codes.js +5 -0
- package/node_modules/auth-store/dist/index.d.ts +7 -0
- package/node_modules/auth-store/dist/index.js +4 -0
- package/node_modules/auth-store/dist/keychain-store.d.ts +25 -0
- package/node_modules/auth-store/dist/keychain-store.js +154 -0
- package/node_modules/auth-store/dist/provider-store.d.ts +14 -0
- package/node_modules/auth-store/dist/provider-store.js +78 -0
- package/node_modules/auth-store/dist/types.d.ts +22 -0
- package/node_modules/auth-store/dist/types.js +1 -0
- package/node_modules/auth-store/package.json +27 -0
- package/node_modules/mcp-oauth/LICENSE +21 -0
- package/node_modules/mcp-oauth/README.md +70 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +169 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.js +47 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +627 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +207 -0
- package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +164 -0
- package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
- package/node_modules/mcp-oauth/dist/client/types.js +1 -0
- package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
- package/node_modules/mcp-oauth/dist/index.js +7 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +32 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +388 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
- package/node_modules/mcp-oauth/package.json +33 -0
- package/node_modules/tiny-mcp-client/LICENSE +21 -0
- package/node_modules/tiny-mcp-client/README.md +104 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +660 -0
- package/node_modules/tiny-mcp-client/dist/index.js +3870 -0
- package/node_modules/tiny-mcp-client/package.json +30 -0
- package/package.json +63 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { validateHeaderValue } from "node:http";
|
|
2
|
+
import { JSON_RPC_ERROR_CODES } from "tiny-stdio-mcp-server";
|
|
3
|
+
import { formatErrorResponse, formatSuccessResponse } from "tiny-stdio-mcp-server/jsonrpc";
|
|
4
|
+
import { JsonRpcMessageError, readAndClassifyBody } from "./parse-body.js";
|
|
5
|
+
import { createSessionStore, defaultSessionIdGenerator } from "./session.js";
|
|
6
|
+
import { formatSseEvent, SSE_HEADERS } from "./sse.js";
|
|
7
|
+
const ALLOWED_METHODS = "POST, GET, DELETE, OPTIONS";
|
|
8
|
+
const MCP_SESSION_ID_HEADER = "Mcp-Session-Id";
|
|
9
|
+
const LOCAL_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
10
|
+
const DEFAULT_ALLOWED_HEADERS = "Accept, Authorization, Content-Type, Mcp-Session-Id, MCP-Protocol-Version";
|
|
11
|
+
function validateOptionalIntegerOption(name, value, minimum) {
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
16
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export class StreamableHttpTransport {
|
|
21
|
+
server;
|
|
22
|
+
runWithRequestContext;
|
|
23
|
+
sessionIdGenerator;
|
|
24
|
+
enableJsonResponse;
|
|
25
|
+
allowedOrigins;
|
|
26
|
+
allowedHosts;
|
|
27
|
+
maxRequestBytes;
|
|
28
|
+
maxBatchSize;
|
|
29
|
+
maxSessions;
|
|
30
|
+
sessionTtlMs;
|
|
31
|
+
maxStreamsPerSession;
|
|
32
|
+
maxStreamBufferBytes;
|
|
33
|
+
maxSseEventHistory;
|
|
34
|
+
sseKeepAliveMs;
|
|
35
|
+
maxConcurrentToolCalls;
|
|
36
|
+
sessionStore;
|
|
37
|
+
requestIdGenerator;
|
|
38
|
+
observability;
|
|
39
|
+
trustedProxy;
|
|
40
|
+
sessionMessages = new Map();
|
|
41
|
+
sseStreams = new Map();
|
|
42
|
+
sseExpiryTimers = new Map();
|
|
43
|
+
sseEventHistory = new Map();
|
|
44
|
+
responseRequestIds = new WeakMap();
|
|
45
|
+
responseOrigins = new WeakMap();
|
|
46
|
+
responseRejectionReasons = new WeakMap();
|
|
47
|
+
nextNotificationEventId = 1;
|
|
48
|
+
nextRequestId = 1;
|
|
49
|
+
activeToolCalls = 0;
|
|
50
|
+
closed = false;
|
|
51
|
+
sessionExpiryInterval;
|
|
52
|
+
sseKeepAliveInterval;
|
|
53
|
+
constructor(server, options = {}, runWithRequestContext = async (_req, callback) => callback()) {
|
|
54
|
+
this.server = server;
|
|
55
|
+
this.runWithRequestContext = runWithRequestContext;
|
|
56
|
+
this.sessionIdGenerator = hasOwnProperty(options, "sessionIdGenerator")
|
|
57
|
+
? options.sessionIdGenerator
|
|
58
|
+
: defaultSessionIdGenerator;
|
|
59
|
+
this.enableJsonResponse = options.enableJsonResponse ?? false;
|
|
60
|
+
this.allowedOrigins = new Set(options.allowedOrigins ?? []);
|
|
61
|
+
this.allowedHosts = new Set((options.allowedHosts ?? LOCAL_HOSTS).map((host) => this.normalizeHost(host)));
|
|
62
|
+
this.maxRequestBytes = validateOptionalIntegerOption("maxRequestBytes", options.maxRequestBytes, 1);
|
|
63
|
+
this.maxBatchSize = validateOptionalIntegerOption("maxBatchSize", options.maxBatchSize, 1);
|
|
64
|
+
this.maxSessions = validateOptionalIntegerOption("maxSessions", options.maxSessions, 1);
|
|
65
|
+
this.sessionTtlMs = validateOptionalIntegerOption("sessionTtlMs", options.sessionTtlMs, 1);
|
|
66
|
+
this.maxStreamsPerSession =
|
|
67
|
+
validateOptionalIntegerOption("maxStreamsPerSession", options.maxStreamsPerSession, 1) ?? 1;
|
|
68
|
+
this.maxStreamBufferBytes =
|
|
69
|
+
validateOptionalIntegerOption("maxStreamBufferBytes", options.maxStreamBufferBytes, 0) ??
|
|
70
|
+
1024 * 1024;
|
|
71
|
+
this.maxSseEventHistory =
|
|
72
|
+
validateOptionalIntegerOption("maxSseEventHistory", options.maxSseEventHistory, 0) ?? 100;
|
|
73
|
+
this.sseKeepAliveMs =
|
|
74
|
+
validateOptionalIntegerOption("sseKeepAliveMs", options.sseKeepAliveMs, 0) ?? 30_000;
|
|
75
|
+
this.maxConcurrentToolCalls = validateOptionalIntegerOption("maxConcurrentToolCalls", options.maxConcurrentToolCalls, 1);
|
|
76
|
+
this.sessionStore = options.sessionStore ?? createSessionStore();
|
|
77
|
+
this.requestIdGenerator = options.requestIdGenerator ?? (() => `req-${this.nextRequestId++}`);
|
|
78
|
+
this.observability = options.observability ?? {};
|
|
79
|
+
this.trustedProxy = options.trustedProxy ?? false;
|
|
80
|
+
if (this.sessionTtlMs !== undefined) {
|
|
81
|
+
this.sessionExpiryInterval = setInterval(() => {
|
|
82
|
+
try {
|
|
83
|
+
this.purgeExpiredSessions();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}, Math.min(this.sessionTtlMs, 60_000));
|
|
89
|
+
this.sessionExpiryInterval.unref();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async handleRequest(req, res) {
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
const requestId = this.readRequestId(req) ?? this.requestIdGenerator();
|
|
95
|
+
this.responseRequestIds.set(res, requestId);
|
|
96
|
+
const origin = this.readOrigin(req);
|
|
97
|
+
if (origin !== undefined && this.acceptsOriginValue(req, origin)) {
|
|
98
|
+
this.responseOrigins.set(res, origin);
|
|
99
|
+
}
|
|
100
|
+
this.emit({
|
|
101
|
+
type: "request.start",
|
|
102
|
+
requestId,
|
|
103
|
+
method: req.method ?? "",
|
|
104
|
+
path: req.url ?? "",
|
|
105
|
+
sessionId: this.readSessionId(req)
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
if (this.closed) {
|
|
109
|
+
this.respondWithRejection(res, 503, "transport_closed", "The transport is closed; create or use an active transport.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!this.acceptsHost(req)) {
|
|
113
|
+
this.respondWithRejection(res, 403, "host_not_allowed", `Host ${JSON.stringify(this.readRequestHost(req))} is not allowed; add it to allowedHosts.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!this.acceptsOrigin(req)) {
|
|
117
|
+
this.respondWithRejection(res, 403, "origin_not_allowed", `Origin ${JSON.stringify(this.readOrigin(req) ?? "")} is not allowed; add it to allowedOrigins.`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
switch (req.method) {
|
|
121
|
+
case "POST":
|
|
122
|
+
await this.handlePost(req, res);
|
|
123
|
+
return;
|
|
124
|
+
case "GET":
|
|
125
|
+
await this.handleGet(req, res);
|
|
126
|
+
return;
|
|
127
|
+
case "DELETE":
|
|
128
|
+
this.handleDelete(req, res);
|
|
129
|
+
return;
|
|
130
|
+
case "OPTIONS":
|
|
131
|
+
this.handleOptions(req, res);
|
|
132
|
+
return;
|
|
133
|
+
default:
|
|
134
|
+
this.respondWithStatus(res, 405, undefined, {
|
|
135
|
+
Allow: ALLOWED_METHODS
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.emit({
|
|
141
|
+
type: "request.error",
|
|
142
|
+
requestId,
|
|
143
|
+
method: req.method ?? "",
|
|
144
|
+
durationMs: Date.now() - startedAt,
|
|
145
|
+
error,
|
|
146
|
+
sessionId: this.readSessionId(req)
|
|
147
|
+
});
|
|
148
|
+
if (!res.headersSent) {
|
|
149
|
+
this.respondWithStatus(res, 500);
|
|
150
|
+
}
|
|
151
|
+
else if (!res.writableEnded) {
|
|
152
|
+
res.end();
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
this.emit({
|
|
158
|
+
type: "request.end",
|
|
159
|
+
requestId,
|
|
160
|
+
method: req.method ?? "",
|
|
161
|
+
statusCode: res.statusCode,
|
|
162
|
+
durationMs: Date.now() - startedAt,
|
|
163
|
+
sessionId: this.readSessionId(req),
|
|
164
|
+
...(res.statusCode < 200 || res.statusCode >= 300
|
|
165
|
+
? { reason: this.responseRejectionReasons.get(res) ?? "http_error" }
|
|
166
|
+
: {})
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async close() {
|
|
171
|
+
this.closed = true;
|
|
172
|
+
if (this.sessionExpiryInterval !== undefined) {
|
|
173
|
+
clearInterval(this.sessionExpiryInterval);
|
|
174
|
+
this.sessionExpiryInterval = undefined;
|
|
175
|
+
}
|
|
176
|
+
this.stopSseKeepAlive();
|
|
177
|
+
for (const timer of this.sseExpiryTimers.values()) {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
this.sseExpiryTimers.clear();
|
|
181
|
+
for (const sessionId of [...this.sseStreams.keys()]) {
|
|
182
|
+
this.closeStreamsForSession(sessionId);
|
|
183
|
+
}
|
|
184
|
+
for (const sessionId of [...this.sessionMessages.keys()]) {
|
|
185
|
+
this.deleteSession(sessionId, "closed");
|
|
186
|
+
}
|
|
187
|
+
this.sessionMessages.clear();
|
|
188
|
+
}
|
|
189
|
+
async handlePost(req, res) {
|
|
190
|
+
if (!this.isJsonRequest(req)) {
|
|
191
|
+
this.respondWithJsonRpcError(res, 415, JSON_RPC_ERROR_CODES.INVALID_REQUEST, "Invalid Request");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!this.acceptsConfiguredResponse(req)) {
|
|
195
|
+
const expectedType = this.enableJsonResponse ? "application/json" : "text/event-stream";
|
|
196
|
+
this.respondWithRejection(res, 406, "response_type_not_acceptable", `Accept must allow ${expectedType}.`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
let classified;
|
|
200
|
+
try {
|
|
201
|
+
classified = await readAndClassifyBody(req, undefined, {
|
|
202
|
+
maxBytes: this.maxRequestBytes,
|
|
203
|
+
maxBatchSize: this.maxBatchSize
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const message = error instanceof Error ? error.message : "Invalid Request";
|
|
208
|
+
if (message === "Payload too large") {
|
|
209
|
+
this.respondWithJsonRpcError(res, 413, JSON_RPC_ERROR_CODES.INVALID_REQUEST, message);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const code = error instanceof JsonRpcMessageError
|
|
213
|
+
? error.code
|
|
214
|
+
: message === "Parse error"
|
|
215
|
+
? JSON_RPC_ERROR_CODES.PARSE_ERROR
|
|
216
|
+
: JSON_RPC_ERROR_CODES.INVALID_REQUEST;
|
|
217
|
+
const id = error instanceof JsonRpcMessageError ? error.id : null;
|
|
218
|
+
this.respondWithJsonRpcError(res, 400, code, message, id);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const initMessage = classified.messages.find((message) => this.isRequest(message) && message.method === "initialize");
|
|
222
|
+
let sessionId;
|
|
223
|
+
let activeSession;
|
|
224
|
+
if (this.sessionIdGenerator !== undefined) {
|
|
225
|
+
const headerSessionId = this.readSessionId(req);
|
|
226
|
+
if (headerSessionId === undefined) {
|
|
227
|
+
if (initMessage === undefined) {
|
|
228
|
+
this.respondWithRejection(res, 400, "session_id_required", "Mcp-Session-Id is required; initialize a session first.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (this.maxSessions !== undefined && this.sessionCount() >= this.maxSessions) {
|
|
232
|
+
this.respondWithRejection(res, 503, "session_limit_reached", "The server has reached its session limit; close a session or retry later.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const newSessionId = this.sessionIdGenerator();
|
|
236
|
+
if (!this.isValidNewSessionId(newSessionId)) {
|
|
237
|
+
this.respondWithStatus(res, 500);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
sessionId = newSessionId;
|
|
241
|
+
activeSession = this.sessionStore.create(newSessionId);
|
|
242
|
+
const authSubject = this.readAuthSubject(req);
|
|
243
|
+
if (authSubject !== undefined) {
|
|
244
|
+
activeSession.authSubject = authSubject;
|
|
245
|
+
}
|
|
246
|
+
this.emit({ type: "session.created", sessionId: newSessionId });
|
|
247
|
+
this.createLocalMessageSession(newSessionId);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
activeSession = this.getActiveSession(headerSessionId, req);
|
|
251
|
+
if (activeSession === undefined) {
|
|
252
|
+
this.respondWithRejection(res, 404, "session_not_found", "Session was not found or has expired; reinitialize the session.");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
sessionId = headerSessionId;
|
|
256
|
+
this.touchSession(sessionId);
|
|
257
|
+
if (activeSession.protocolVersion !== undefined &&
|
|
258
|
+
!this.acceptsProtocolVersion(req, activeSession.protocolVersion)) {
|
|
259
|
+
this.respondWithRejection(res, 400, "protocol_version_mismatch", `MCP-Protocol-Version must match the session protocol version ${activeSession.protocolVersion}.`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await this.ensureLocalMessageSession(sessionId, activeSession);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const formattedResponses = await this.runWithRequestContext(req, async () => {
|
|
266
|
+
const responses = [];
|
|
267
|
+
for (const message of classified.entries) {
|
|
268
|
+
if (message === null) {
|
|
269
|
+
responses.push(formatErrorResponse(null, {
|
|
270
|
+
code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
|
|
271
|
+
message: "Invalid Request"
|
|
272
|
+
}));
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!("method" in message)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const session = activeSession;
|
|
279
|
+
if (session !== undefined &&
|
|
280
|
+
message.method !== "initialize" &&
|
|
281
|
+
message.method !== "notifications/initialized" &&
|
|
282
|
+
message.method !== "ping" &&
|
|
283
|
+
!session.initialized) {
|
|
284
|
+
if (this.isRequest(message)) {
|
|
285
|
+
responses.push(formatErrorResponse(message.id, {
|
|
286
|
+
code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
|
|
287
|
+
message: "Session not initialized"
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const requestId = this.responseRequestIds.get(res) ?? "";
|
|
293
|
+
const toolName = this.readToolName(message);
|
|
294
|
+
if (message.method === "tools/call" &&
|
|
295
|
+
this.maxConcurrentToolCalls !== undefined &&
|
|
296
|
+
this.activeToolCalls >= this.maxConcurrentToolCalls) {
|
|
297
|
+
if (this.isRequest(message)) {
|
|
298
|
+
responses.push(formatErrorResponse(message.id, {
|
|
299
|
+
code: -32000,
|
|
300
|
+
message: "Too many concurrent tool calls"
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const messageHandler = sessionId === undefined
|
|
306
|
+
? this.server.handleMessage
|
|
307
|
+
: (this.sessionMessages.get(sessionId)?.handleMessage ?? this.server.handleMessage);
|
|
308
|
+
const isToolCall = message.method === "tools/call";
|
|
309
|
+
const toolStartedAt = Date.now();
|
|
310
|
+
if (isToolCall) {
|
|
311
|
+
this.activeToolCalls += 1;
|
|
312
|
+
this.emit({
|
|
313
|
+
type: "tool.start",
|
|
314
|
+
requestId,
|
|
315
|
+
sessionId,
|
|
316
|
+
toolName
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
let handled;
|
|
320
|
+
try {
|
|
321
|
+
handled = await messageHandler(message.method, message.params);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (isToolCall) {
|
|
325
|
+
this.activeToolCalls -= 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const { error, result } = handled;
|
|
329
|
+
if (isToolCall) {
|
|
330
|
+
this.emit({
|
|
331
|
+
type: "tool.end",
|
|
332
|
+
requestId,
|
|
333
|
+
sessionId,
|
|
334
|
+
toolName,
|
|
335
|
+
ok: this.isToolCallOk(error, result),
|
|
336
|
+
durationMs: Date.now() - toolStartedAt
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (session !== undefined && error === undefined) {
|
|
340
|
+
if (message.method === "initialize" && this.isRequest(message)) {
|
|
341
|
+
const initializeResult = result;
|
|
342
|
+
if (typeof initializeResult?.protocolVersion === "string") {
|
|
343
|
+
session.protocolVersion = initializeResult.protocolVersion;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else if (message.method === "notifications/initialized" &&
|
|
347
|
+
session.protocolVersion !== undefined) {
|
|
348
|
+
session.initialized = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (!this.isRequest(message)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (error !== undefined) {
|
|
355
|
+
responses.push(formatErrorResponse(message.id, error));
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (result !== undefined) {
|
|
359
|
+
responses.push(formatSuccessResponse(message.id, result));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return responses;
|
|
363
|
+
});
|
|
364
|
+
if (formattedResponses.length === 0) {
|
|
365
|
+
this.respondWithStatus(res, 202, sessionId);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (this.enableJsonResponse) {
|
|
369
|
+
const body = formattedResponses.length === 1
|
|
370
|
+
? formattedResponses[0]
|
|
371
|
+
: JSON.stringify(formattedResponses.map((responseText) => JSON.parse(responseText)));
|
|
372
|
+
this.respondWithStatus(res, 200, sessionId, { "Content-Type": "application/json" }, body);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
res.writeHead(200, this.withSessionHeader(SSE_HEADERS, sessionId, res));
|
|
376
|
+
for (const formattedResponse of formattedResponses) {
|
|
377
|
+
res.write(formatSseEvent({ data: formattedResponse }));
|
|
378
|
+
}
|
|
379
|
+
res.end();
|
|
380
|
+
}
|
|
381
|
+
async handleGet(req, res) {
|
|
382
|
+
if (this.sessionIdGenerator === undefined) {
|
|
383
|
+
this.respondWithStatus(res, 405, undefined, {
|
|
384
|
+
Allow: ALLOWED_METHODS
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (!this.acceptsResponseType(req, "text/event-stream")) {
|
|
389
|
+
this.respondWithRejection(res, 406, "response_type_not_acceptable", "Accept must allow text/event-stream.");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const sessionId = this.readSessionId(req);
|
|
393
|
+
if (sessionId === undefined) {
|
|
394
|
+
this.respondWithRejection(res, 400, "session_id_required", "Mcp-Session-Id is required; initialize a session first.");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const session = this.getActiveSession(sessionId, req);
|
|
398
|
+
if (session === undefined) {
|
|
399
|
+
this.respondWithRejection(res, 404, "session_not_found", "Session was not found or has expired; reinitialize the session.");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.touchSession(sessionId);
|
|
403
|
+
if (session.initialized &&
|
|
404
|
+
session.protocolVersion !== undefined &&
|
|
405
|
+
!this.acceptsProtocolVersion(req, session.protocolVersion)) {
|
|
406
|
+
this.respondWithRejection(res, 400, "protocol_version_mismatch", `MCP-Protocol-Version must match the session protocol version ${session.protocolVersion}.`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
await this.ensureLocalMessageSession(sessionId, session);
|
|
410
|
+
const existingStreams = this.sseStreams.get(sessionId);
|
|
411
|
+
if ((existingStreams?.size ?? 0) >= this.maxStreamsPerSession) {
|
|
412
|
+
this.respondWithRejection(res, 409, "stream_limit_reached", "This session already has the maximum number of streams; close a stream and retry.");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
let streams = this.sseStreams.get(sessionId);
|
|
416
|
+
if (streams === undefined) {
|
|
417
|
+
streams = new Set();
|
|
418
|
+
this.sseStreams.set(sessionId, streams);
|
|
419
|
+
}
|
|
420
|
+
streams.add(res);
|
|
421
|
+
this.startSseKeepAlive();
|
|
422
|
+
this.emit({
|
|
423
|
+
type: "stream.opened",
|
|
424
|
+
sessionId,
|
|
425
|
+
streamCount: streams.size
|
|
426
|
+
});
|
|
427
|
+
const cleanup = () => {
|
|
428
|
+
this.clearSseExpiryTimer(res);
|
|
429
|
+
const activeStreams = this.sseStreams.get(sessionId);
|
|
430
|
+
if (activeStreams === undefined) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const deleted = activeStreams.delete(res);
|
|
434
|
+
if (deleted) {
|
|
435
|
+
this.emit({
|
|
436
|
+
type: "stream.closed",
|
|
437
|
+
sessionId,
|
|
438
|
+
streamCount: activeStreams.size
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (activeStreams.size === 0) {
|
|
442
|
+
this.sseStreams.delete(sessionId);
|
|
443
|
+
}
|
|
444
|
+
this.stopSseKeepAliveIfIdle();
|
|
445
|
+
};
|
|
446
|
+
req.on("close", cleanup);
|
|
447
|
+
res.on("close", cleanup);
|
|
448
|
+
res.on("finish", cleanup);
|
|
449
|
+
const expiresAt = req.auth?.expiresAt;
|
|
450
|
+
if (expiresAt !== undefined) {
|
|
451
|
+
this.scheduleSseExpiry(res, expiresAt);
|
|
452
|
+
}
|
|
453
|
+
res.writeHead(200, this.withSessionHeader(SSE_HEADERS, sessionId, res));
|
|
454
|
+
this.replaySseEvents(req, res, sessionId);
|
|
455
|
+
res.flushHeaders();
|
|
456
|
+
}
|
|
457
|
+
handleDelete(req, res) {
|
|
458
|
+
if (this.sessionIdGenerator === undefined) {
|
|
459
|
+
this.respondWithStatus(res, 405, undefined, {
|
|
460
|
+
Allow: ALLOWED_METHODS
|
|
461
|
+
});
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const sessionId = this.readSessionId(req);
|
|
465
|
+
if (sessionId === undefined) {
|
|
466
|
+
this.respondWithRejection(res, 400, "session_id_required", "Mcp-Session-Id is required; initialize a session first.");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const session = this.getActiveSession(sessionId, req);
|
|
470
|
+
if (session === undefined) {
|
|
471
|
+
this.respondWithRejection(res, 404, "session_not_found", "Session was not found or has expired; reinitialize the session.");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (session.protocolVersion !== undefined &&
|
|
475
|
+
!this.acceptsProtocolVersion(req, session.protocolVersion)) {
|
|
476
|
+
this.respondWithRejection(res, 400, "protocol_version_mismatch", `MCP-Protocol-Version must match the session protocol version ${session.protocolVersion}.`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!this.deleteSession(sessionId, "client")) {
|
|
480
|
+
this.respondWithRejection(res, 404, "session_not_found", "Session was not found or has expired; reinitialize the session.");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
this.respondWithStatus(res, 204);
|
|
484
|
+
}
|
|
485
|
+
handleOptions(req, res) {
|
|
486
|
+
const requestedMethod = req.headers["access-control-request-method"];
|
|
487
|
+
const method = Array.isArray(requestedMethod) ? requestedMethod[0] : requestedMethod;
|
|
488
|
+
if (method !== undefined && !ALLOWED_METHODS.split(", ").includes(method)) {
|
|
489
|
+
this.respondWithStatus(res, 405, undefined, {
|
|
490
|
+
Allow: ALLOWED_METHODS
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const requestedHeaders = req.headers["access-control-request-headers"];
|
|
495
|
+
this.respondWithStatus(res, 204, undefined, {
|
|
496
|
+
Allow: ALLOWED_METHODS,
|
|
497
|
+
"Access-Control-Allow-Methods": ALLOWED_METHODS,
|
|
498
|
+
"Access-Control-Allow-Headers": Array.isArray(requestedHeaders)
|
|
499
|
+
? requestedHeaders.join(", ")
|
|
500
|
+
: (requestedHeaders ?? DEFAULT_ALLOWED_HEADERS),
|
|
501
|
+
"Access-Control-Max-Age": "600"
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
readSessionId(req) {
|
|
505
|
+
const value = req.headers["mcp-session-id"];
|
|
506
|
+
const id = Array.isArray(value) ? value[0] : value;
|
|
507
|
+
return id !== undefined && id.length > 0 ? id : undefined;
|
|
508
|
+
}
|
|
509
|
+
acceptsProtocolVersion(req, protocolVersion) {
|
|
510
|
+
const value = req.headers["mcp-protocol-version"];
|
|
511
|
+
const header = Array.isArray(value) ? value[0] : value;
|
|
512
|
+
return header === undefined || header === protocolVersion;
|
|
513
|
+
}
|
|
514
|
+
readRequestId(req) {
|
|
515
|
+
const value = req.headers["x-request-id"];
|
|
516
|
+
const requestId = Array.isArray(value) ? value[0] : value;
|
|
517
|
+
return requestId !== undefined && requestId.length > 0 ? requestId : undefined;
|
|
518
|
+
}
|
|
519
|
+
readOrigin(req) {
|
|
520
|
+
const originHeader = req.headers.origin;
|
|
521
|
+
return Array.isArray(originHeader) ? originHeader[0] : originHeader;
|
|
522
|
+
}
|
|
523
|
+
readToolName(message) {
|
|
524
|
+
if (!("method" in message) || message.method !== "tools/call") {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
const params = "params" in message ? message.params : undefined;
|
|
528
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
const name = params.name;
|
|
532
|
+
return typeof name === "string" ? name : undefined;
|
|
533
|
+
}
|
|
534
|
+
getActiveSession(sessionId, req) {
|
|
535
|
+
const session = this.sessionStore.get(sessionId);
|
|
536
|
+
if (session === undefined) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
if (this.isExpired(session)) {
|
|
540
|
+
this.deleteSession(sessionId, "expired");
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
if (session.authSubject !== undefined && session.authSubject !== this.readAuthSubject(req)) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
return session;
|
|
547
|
+
}
|
|
548
|
+
readAuthSubject(req) {
|
|
549
|
+
const auth = req.auth;
|
|
550
|
+
const authSubject = auth?.subject ?? auth?.clientId;
|
|
551
|
+
return authSubject !== undefined && authSubject.length > 0 ? authSubject : undefined;
|
|
552
|
+
}
|
|
553
|
+
touchSession(sessionId) {
|
|
554
|
+
const session = this.sessionStore.get(sessionId);
|
|
555
|
+
if (session === undefined) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (this.sessionStore.touch !== undefined) {
|
|
559
|
+
this.sessionStore.touch(sessionId);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
session.lastSeenAt = new Date();
|
|
563
|
+
}
|
|
564
|
+
isExpired(session) {
|
|
565
|
+
if (this.sessionTtlMs === undefined) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
return Date.now() - session.lastSeenAt.getTime() > this.sessionTtlMs;
|
|
569
|
+
}
|
|
570
|
+
purgeExpiredSessions() {
|
|
571
|
+
if (this.sessionTtlMs === undefined || this.sessionStore.entries === undefined) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
for (const session of [...this.sessionStore.entries()]) {
|
|
575
|
+
if (this.isExpired(session)) {
|
|
576
|
+
this.deleteSession(session.id, "expired");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
sessionCount() {
|
|
581
|
+
if (this.sessionStore.entries !== undefined) {
|
|
582
|
+
return [...this.sessionStore.entries()].length;
|
|
583
|
+
}
|
|
584
|
+
return this.sessionMessages.size;
|
|
585
|
+
}
|
|
586
|
+
deleteSession(sessionId, reason) {
|
|
587
|
+
const deleted = this.sessionStore.delete(sessionId);
|
|
588
|
+
if (!deleted) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
this.sessionMessages.get(sessionId)?.close();
|
|
592
|
+
this.sessionMessages.delete(sessionId);
|
|
593
|
+
this.closeStreamsForSession(sessionId);
|
|
594
|
+
this.sseEventHistory.delete(sessionId);
|
|
595
|
+
this.emit({ type: "session.deleted", sessionId, reason });
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
createLocalMessageSession(sessionId) {
|
|
599
|
+
const messageSession = this.server.createMessageSession((notification) => {
|
|
600
|
+
this.sendNotificationToSession(sessionId, notification);
|
|
601
|
+
});
|
|
602
|
+
this.sessionMessages.set(sessionId, messageSession);
|
|
603
|
+
return messageSession;
|
|
604
|
+
}
|
|
605
|
+
async ensureLocalMessageSession(sessionId, session) {
|
|
606
|
+
const existing = this.sessionMessages.get(sessionId);
|
|
607
|
+
if (existing !== undefined) {
|
|
608
|
+
return existing;
|
|
609
|
+
}
|
|
610
|
+
const messageSession = this.createLocalMessageSession(sessionId);
|
|
611
|
+
if (session.protocolVersion !== undefined) {
|
|
612
|
+
await messageSession.handleMessage("initialize", {
|
|
613
|
+
protocolVersion: session.protocolVersion
|
|
614
|
+
});
|
|
615
|
+
if (session.initialized) {
|
|
616
|
+
await messageSession.handleMessage("notifications/initialized");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return messageSession;
|
|
620
|
+
}
|
|
621
|
+
closeStreamsForSession(sessionId) {
|
|
622
|
+
const streams = this.sseStreams.get(sessionId);
|
|
623
|
+
if (streams === undefined) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
for (const response of streams) {
|
|
627
|
+
this.clearSseExpiryTimer(response);
|
|
628
|
+
if (!response.writableEnded) {
|
|
629
|
+
response.end();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
this.sseStreams.delete(sessionId);
|
|
633
|
+
this.stopSseKeepAliveIfIdle();
|
|
634
|
+
}
|
|
635
|
+
scheduleSseExpiry(response, expiresAt) {
|
|
636
|
+
const remainingMs = Math.max(0, expiresAt * 1_000 - Date.now());
|
|
637
|
+
const timer = setTimeout(() => {
|
|
638
|
+
this.sseExpiryTimers.delete(response);
|
|
639
|
+
if (!response.writableEnded) {
|
|
640
|
+
response.end();
|
|
641
|
+
}
|
|
642
|
+
}, remainingMs);
|
|
643
|
+
timer.unref();
|
|
644
|
+
this.sseExpiryTimers.set(response, timer);
|
|
645
|
+
}
|
|
646
|
+
clearSseExpiryTimer(response) {
|
|
647
|
+
const timer = this.sseExpiryTimers.get(response);
|
|
648
|
+
if (timer === undefined) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
clearTimeout(timer);
|
|
652
|
+
this.sseExpiryTimers.delete(response);
|
|
653
|
+
}
|
|
654
|
+
startSseKeepAlive() {
|
|
655
|
+
if (this.sseKeepAliveMs === 0 || this.sseKeepAliveInterval !== undefined) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
this.sseKeepAliveInterval = setInterval(() => {
|
|
659
|
+
for (const streams of this.sseStreams.values()) {
|
|
660
|
+
for (const response of streams) {
|
|
661
|
+
if (!response.writableEnded) {
|
|
662
|
+
this.writeToLiveGetStream(response, ": keepalive\n\n");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}, this.sseKeepAliveMs);
|
|
667
|
+
this.sseKeepAliveInterval.unref();
|
|
668
|
+
}
|
|
669
|
+
stopSseKeepAliveIfIdle() {
|
|
670
|
+
if ([...this.sseStreams.values()].some((streams) => streams.size > 0)) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.stopSseKeepAlive();
|
|
674
|
+
}
|
|
675
|
+
stopSseKeepAlive() {
|
|
676
|
+
if (this.sseKeepAliveInterval === undefined) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
clearInterval(this.sseKeepAliveInterval);
|
|
680
|
+
this.sseKeepAliveInterval = undefined;
|
|
681
|
+
}
|
|
682
|
+
sendNotificationToSession(sessionId, notification) {
|
|
683
|
+
if (!this.sessionStore.get(sessionId)?.initialized) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const id = this.nextNotificationEventId++;
|
|
687
|
+
const data = JSON.stringify(notification);
|
|
688
|
+
this.recordSseEvent(sessionId, id, data);
|
|
689
|
+
const streams = this.sseStreams.get(sessionId);
|
|
690
|
+
if (streams === undefined) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const event = formatSseEvent({
|
|
694
|
+
id: String(id),
|
|
695
|
+
data
|
|
696
|
+
});
|
|
697
|
+
let latestResponse;
|
|
698
|
+
for (const response of streams) {
|
|
699
|
+
if (!response.writableEnded) {
|
|
700
|
+
latestResponse = response;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (latestResponse !== undefined) {
|
|
704
|
+
this.writeToLiveGetStream(latestResponse, event);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
writeToLiveGetStream(response, data) {
|
|
708
|
+
if ((response.writableLength ?? 0) > this.maxStreamBufferBytes) {
|
|
709
|
+
response.end();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
response.write(data);
|
|
713
|
+
}
|
|
714
|
+
recordSseEvent(sessionId, id, data) {
|
|
715
|
+
if (this.maxSseEventHistory <= 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const history = this.sseEventHistory.get(sessionId) ?? [];
|
|
719
|
+
history.push({ id, data });
|
|
720
|
+
while (history.length > this.maxSseEventHistory) {
|
|
721
|
+
history.shift();
|
|
722
|
+
}
|
|
723
|
+
this.sseEventHistory.set(sessionId, history);
|
|
724
|
+
}
|
|
725
|
+
replaySseEvents(req, res, sessionId) {
|
|
726
|
+
const value = req.headers["last-event-id"];
|
|
727
|
+
const header = Array.isArray(value) ? value[0] : value;
|
|
728
|
+
if (header === undefined) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const lastEventId = Number(header);
|
|
732
|
+
if (!Number.isSafeInteger(lastEventId)) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const history = this.sseEventHistory.get(sessionId) ?? [];
|
|
736
|
+
for (const event of history) {
|
|
737
|
+
if (event.id > lastEventId && !res.writableEnded) {
|
|
738
|
+
res.write(formatSseEvent({ id: String(event.id), data: event.data }));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
isJsonRequest(req) {
|
|
743
|
+
const contentType = req.headers["content-type"];
|
|
744
|
+
if (contentType === undefined) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
const value = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
748
|
+
const type = value.split(";")[0]?.trim().toLowerCase();
|
|
749
|
+
return type === "application/json";
|
|
750
|
+
}
|
|
751
|
+
acceptsOrigin(req) {
|
|
752
|
+
const origin = this.readOrigin(req);
|
|
753
|
+
if (origin === undefined) {
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
return this.acceptsOriginValue(req, origin);
|
|
757
|
+
}
|
|
758
|
+
acceptsOriginValue(req, origin) {
|
|
759
|
+
try {
|
|
760
|
+
const endpointOrigin = new URL(`${this.readRequestProtocol(req)}://${this.readRequestHost(req)}`).origin;
|
|
761
|
+
return origin === endpointOrigin || this.allowedOrigins.has(origin);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
readRequestProtocol(req) {
|
|
768
|
+
if (this.trustedProxy) {
|
|
769
|
+
const forwardedProto = this.readForwardedHeader(req, "x-forwarded-proto")?.toLowerCase();
|
|
770
|
+
if (forwardedProto === "http" || forwardedProto === "https") {
|
|
771
|
+
return forwardedProto;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
|
|
775
|
+
}
|
|
776
|
+
readRequestHost(req) {
|
|
777
|
+
if (this.trustedProxy) {
|
|
778
|
+
const forwardedHost = this.readForwardedHeader(req, "x-forwarded-host");
|
|
779
|
+
if (forwardedHost !== undefined && forwardedHost.length > 0) {
|
|
780
|
+
return forwardedHost;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const host = req.headers.host;
|
|
784
|
+
return Array.isArray(host) ? (host[0] ?? "127.0.0.1") : (host ?? "127.0.0.1");
|
|
785
|
+
}
|
|
786
|
+
readForwardedHeader(req, headerName) {
|
|
787
|
+
const value = req.headers[headerName];
|
|
788
|
+
const header = Array.isArray(value) ? value[0] : value;
|
|
789
|
+
return header?.split(",")[0]?.trim();
|
|
790
|
+
}
|
|
791
|
+
acceptsHost(req) {
|
|
792
|
+
const hostHeader = req.headers.host;
|
|
793
|
+
const host = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
794
|
+
if (host === undefined || host.length === 0) {
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
return this.allowedHosts.has(this.normalizeHost(this.readRequestHost(req)));
|
|
798
|
+
}
|
|
799
|
+
normalizeHost(host) {
|
|
800
|
+
const normalized = host.trim().toLowerCase();
|
|
801
|
+
if (normalized.startsWith("[")) {
|
|
802
|
+
const closing = normalized.indexOf("]");
|
|
803
|
+
if (closing > 0) {
|
|
804
|
+
return normalized.slice(1, closing);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const colonCount = [...normalized].filter((character) => character === ":").length;
|
|
808
|
+
if (colonCount > 1) {
|
|
809
|
+
return normalized;
|
|
810
|
+
}
|
|
811
|
+
return normalized.includes(":") ? (normalized.split(":")[0] ?? normalized) : normalized;
|
|
812
|
+
}
|
|
813
|
+
acceptsConfiguredResponse(req) {
|
|
814
|
+
const expectedType = this.enableJsonResponse ? "application/json" : "text/event-stream";
|
|
815
|
+
return this.acceptsResponseType(req, expectedType);
|
|
816
|
+
}
|
|
817
|
+
acceptsResponseType(req, expectedType) {
|
|
818
|
+
const accept = req.headers.accept;
|
|
819
|
+
if (accept === undefined) {
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
const value = Array.isArray(accept) ? accept.join(",") : accept;
|
|
823
|
+
return value
|
|
824
|
+
.split(",")
|
|
825
|
+
.map((type) => type.split(";")[0]?.trim().toLowerCase())
|
|
826
|
+
.some((type) => type === "*/*" || type === expectedType);
|
|
827
|
+
}
|
|
828
|
+
isValidNewSessionId(sessionId) {
|
|
829
|
+
if (sessionId.length === 0 || this.sessionStore.has(sessionId)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
validateHeaderValue(MCP_SESSION_ID_HEADER, sessionId);
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
isRequest(message) {
|
|
841
|
+
return "method" in message && "id" in message;
|
|
842
|
+
}
|
|
843
|
+
isToolCallOk(error, result) {
|
|
844
|
+
if (error !== undefined) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return !(hasOwnProperty(result, "isError") && result.isError === true);
|
|
851
|
+
}
|
|
852
|
+
respondWithJsonRpcError(res, statusCode, errorCode, message, id = null) {
|
|
853
|
+
this.responseRejectionReasons.set(res, "json_rpc_error");
|
|
854
|
+
this.respondWithStatus(res, statusCode, undefined, { "Content-Type": "application/json" }, formatErrorResponse(id, { code: errorCode, message }));
|
|
855
|
+
}
|
|
856
|
+
respondWithRejection(res, statusCode, reason, message) {
|
|
857
|
+
this.responseRejectionReasons.set(res, reason);
|
|
858
|
+
this.respondWithStatus(res, statusCode, undefined, { "Content-Type": "application/json" }, JSON.stringify({ error: reason, message }));
|
|
859
|
+
}
|
|
860
|
+
respondWithStatus(res, statusCode, sessionId, headers, body) {
|
|
861
|
+
res.writeHead(statusCode, this.withSessionHeader(headers, sessionId, res));
|
|
862
|
+
res.end(body);
|
|
863
|
+
}
|
|
864
|
+
withSessionHeader(headers, sessionId, res) {
|
|
865
|
+
const requestId = res === undefined ? undefined : this.responseRequestIds.get(res);
|
|
866
|
+
const origin = res === undefined ? undefined : this.responseOrigins.get(res);
|
|
867
|
+
const baseHeaders = {
|
|
868
|
+
"X-Content-Type-Options": "nosniff",
|
|
869
|
+
"Referrer-Policy": "no-referrer",
|
|
870
|
+
Vary: "Origin",
|
|
871
|
+
...(requestId === undefined ? {} : { "X-Request-Id": requestId }),
|
|
872
|
+
...(origin === undefined
|
|
873
|
+
? {}
|
|
874
|
+
: {
|
|
875
|
+
"Access-Control-Allow-Origin": origin,
|
|
876
|
+
"Access-Control-Expose-Headers": "Mcp-Session-Id, X-Request-Id"
|
|
877
|
+
})
|
|
878
|
+
};
|
|
879
|
+
if (sessionId === undefined) {
|
|
880
|
+
return {
|
|
881
|
+
...baseHeaders,
|
|
882
|
+
...(headers ?? {})
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
...baseHeaders,
|
|
887
|
+
...(headers ?? {}),
|
|
888
|
+
[MCP_SESSION_ID_HEADER]: sessionId
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
emit(event) {
|
|
892
|
+
this.observability.onEvent?.(event);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function hasOwnProperty(value, key) {
|
|
896
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
897
|
+
}
|