@emseepea/server 0.0.1 → 0.0.3
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 +299 -8
- package/dist/http.d.ts +15 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +284 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +286 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1612 -0
- package/dist/index.js.map +1 -0
- package/dist/telemetry.d.ts +3 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +93 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/ui.d.ts +185 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +263 -0
- package/dist/ui.js.map +1 -0
- package/package.json +4 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
import { createMcpFastifyApp } from "@modelcontextprotocol/fastify";
|
|
2
|
+
import { GetPromptResultSchema, ReadResourceResultSchema, } from "@modelcontextprotocol/core";
|
|
3
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
4
|
+
import { McpServer, OAuthError, OAuthErrorCode, ProtocolError, ProtocolErrorCode, ResourceTemplate, bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, checkResourceAllowed, completable, createMcpHandler, getOAuthProtectedResourceMetadataUrl, inputRequired as sdkInputRequired, acceptedContent as sdkAcceptedContent, inputResponse as sdkInputResponse, isInputRequiredResult, oauthMetadataResponse, specTypeSchemas, verifyBearerToken, } from "@modelcontextprotocol/server";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { isIP } from "node:net";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { installRequestTelemetry } from "./telemetry.js";
|
|
10
|
+
export * from "./ui.js";
|
|
11
|
+
export const inputRequired = Object.assign((spec) => sdkInputRequired({ inputRequests: spec.inputRequests }), {
|
|
12
|
+
elicit: sdkInputRequired.elicit,
|
|
13
|
+
elicitUrl: sdkInputRequired.elicitUrl,
|
|
14
|
+
});
|
|
15
|
+
export function acceptedContent(responses, key, schema) {
|
|
16
|
+
return sdkAcceptedContent(checkedInputResponses(responses), key, schema);
|
|
17
|
+
}
|
|
18
|
+
export function inputResponse(responses, key) {
|
|
19
|
+
const response = sdkInputResponse(checkedInputResponses(responses), key);
|
|
20
|
+
return response.kind === "elicit" ? response : { kind: "missing" };
|
|
21
|
+
}
|
|
22
|
+
const PROTOCOL_VERSION = "2026-07-28";
|
|
23
|
+
const REGISTER = Symbol("register");
|
|
24
|
+
const TOOL_NAME = Symbol("toolName");
|
|
25
|
+
const TOOL_ACCESS = Symbol("toolAccess");
|
|
26
|
+
const TOOL_STREAMING = Symbol("toolStreaming");
|
|
27
|
+
const TOOL_LISTING = Symbol("toolListing");
|
|
28
|
+
const RESOURCE_NAME = Symbol("resourceName");
|
|
29
|
+
const RESOURCE_URI = Symbol("resourceUri");
|
|
30
|
+
const RESOURCE_KIND = Symbol("resourceKind");
|
|
31
|
+
const RESOURCE_MATCHES = Symbol("resourceMatches");
|
|
32
|
+
const RESOURCE_ROUTE = Symbol("resourceRoute");
|
|
33
|
+
const RESOURCE_LISTING = Symbol("resourceListing");
|
|
34
|
+
const PROMPT_NAME = Symbol("promptName");
|
|
35
|
+
const HAS_COMPLETION = Symbol("hasCompletion");
|
|
36
|
+
const PROMPT_LISTING = Symbol("promptListing");
|
|
37
|
+
const CACHEABLE_METHODS = [
|
|
38
|
+
"tools/list",
|
|
39
|
+
"prompts/list",
|
|
40
|
+
"resources/list",
|
|
41
|
+
"resources/templates/list",
|
|
42
|
+
"resources/read",
|
|
43
|
+
"server/discover",
|
|
44
|
+
];
|
|
45
|
+
const runtimes = new WeakMap();
|
|
46
|
+
const requestOperations = new AsyncLocalStorage();
|
|
47
|
+
export function defineTool(definition) {
|
|
48
|
+
const handler = definition.handler;
|
|
49
|
+
return createCheckedTool(definition, handler, false, true);
|
|
50
|
+
}
|
|
51
|
+
export function defineStreamingTool(definition) {
|
|
52
|
+
const handler = definition.handler;
|
|
53
|
+
return createCheckedTool(definition, handler, true, false);
|
|
54
|
+
}
|
|
55
|
+
export function defineMappedTool(definition) {
|
|
56
|
+
const { backendInputSchema, backendOutputSchema, isAvailable, adapter } = definition;
|
|
57
|
+
const mapInput = definition.mapInput;
|
|
58
|
+
const mapOutput = definition.mapOutput;
|
|
59
|
+
const execute = async (input, context) => {
|
|
60
|
+
const adapterContext = Object.freeze({
|
|
61
|
+
signal: context.signal,
|
|
62
|
+
deadlineMs: context.deadlineMs,
|
|
63
|
+
});
|
|
64
|
+
context.signal.throwIfAborted();
|
|
65
|
+
if (isAvailable && await isAvailable(adapterContext) !== true) {
|
|
66
|
+
throw new Error("Mapped tool provider is unavailable");
|
|
67
|
+
}
|
|
68
|
+
context.signal.throwIfAborted();
|
|
69
|
+
const command = await backendInputSchema.safeParseAsync(mapInput(input));
|
|
70
|
+
if (!command.success)
|
|
71
|
+
throw new Error("Mapped backend command does not match its schema");
|
|
72
|
+
context.signal.throwIfAborted();
|
|
73
|
+
const backendResult = await adapter(command.data, adapterContext);
|
|
74
|
+
context.signal.throwIfAborted();
|
|
75
|
+
const parsedBackendResult = await backendOutputSchema.safeParseAsync(backendResult);
|
|
76
|
+
if (!parsedBackendResult.success)
|
|
77
|
+
throw new Error("Backend result does not match its schema");
|
|
78
|
+
context.signal.throwIfAborted();
|
|
79
|
+
return mapOutput(parsedBackendResult.data);
|
|
80
|
+
};
|
|
81
|
+
return createCheckedTool(definition, execute, false, false);
|
|
82
|
+
}
|
|
83
|
+
export function defineResource(definition) {
|
|
84
|
+
const { name, handler } = definition;
|
|
85
|
+
assertRegistrationName("Resource", name);
|
|
86
|
+
const uri = canonicalResourceUri(definition.uri);
|
|
87
|
+
const listing = checkedProtocolValue("Resource", {
|
|
88
|
+
name,
|
|
89
|
+
uri,
|
|
90
|
+
title: definition.title,
|
|
91
|
+
description: definition.description,
|
|
92
|
+
mimeType: definition.mimeType,
|
|
93
|
+
icons: definition.icons,
|
|
94
|
+
annotations: definition.annotations,
|
|
95
|
+
size: definition.size,
|
|
96
|
+
_meta: definition._meta,
|
|
97
|
+
});
|
|
98
|
+
const metadata = Object.freeze({
|
|
99
|
+
title: listing.title,
|
|
100
|
+
description: listing.description,
|
|
101
|
+
mimeType: listing.mimeType,
|
|
102
|
+
icons: listing.icons,
|
|
103
|
+
annotations: listing.annotations,
|
|
104
|
+
size: listing.size,
|
|
105
|
+
_meta: listing._meta,
|
|
106
|
+
});
|
|
107
|
+
const cacheHint = definition.cacheHint === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: normalizeCacheHint(definition.cacheHint, `resource ${name}`);
|
|
110
|
+
const registration = {
|
|
111
|
+
[RESOURCE_NAME]: name,
|
|
112
|
+
[RESOURCE_URI]: uri,
|
|
113
|
+
[RESOURCE_KIND]: "static",
|
|
114
|
+
[RESOURCE_LISTING]: Object.freeze({
|
|
115
|
+
method: "resources/list",
|
|
116
|
+
value: listing,
|
|
117
|
+
}),
|
|
118
|
+
[HAS_COMPLETION]: false,
|
|
119
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
120
|
+
server.registerResource(name, uri, cacheHint ? { ...metadata, cacheHint } : metadata, async (_requestedUri, context) => {
|
|
121
|
+
try {
|
|
122
|
+
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
123
|
+
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
124
|
+
signal.throwIfAborted();
|
|
125
|
+
const result = await handler({
|
|
126
|
+
signal,
|
|
127
|
+
deadlineMs,
|
|
128
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
129
|
+
});
|
|
130
|
+
signal.throwIfAborted();
|
|
131
|
+
if (isInputRequiredResult(result)) {
|
|
132
|
+
assertStatelessInputRequired(result);
|
|
133
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
const parsed = await ReadResourceResultSchema.safeParseAsync(result);
|
|
137
|
+
if (!parsed.success || parsed.data.contents.some((content) => content.uri !== uri)) {
|
|
138
|
+
throw new Error("Resource returned an invalid result");
|
|
139
|
+
}
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
assertResultSize(parsed.data, maxApplicationResultBytes, deadlineMs, signal);
|
|
142
|
+
return parsed.data;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new Error("Resource read failed");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return Object.freeze(registration);
|
|
152
|
+
}
|
|
153
|
+
export function defineResourceTemplate(definition) {
|
|
154
|
+
const { name, handler } = definition;
|
|
155
|
+
assertRegistrationName("Resource template", name);
|
|
156
|
+
const { template, route } = checkedResourceTemplate(definition.uriTemplate);
|
|
157
|
+
const uriTemplate = template.uriTemplate.toString();
|
|
158
|
+
const listing = checkedProtocolValue("ResourceTemplate", {
|
|
159
|
+
name,
|
|
160
|
+
uriTemplate,
|
|
161
|
+
title: definition.title,
|
|
162
|
+
description: definition.description,
|
|
163
|
+
mimeType: definition.mimeType,
|
|
164
|
+
icons: definition.icons,
|
|
165
|
+
annotations: definition.annotations,
|
|
166
|
+
_meta: definition._meta,
|
|
167
|
+
});
|
|
168
|
+
const metadata = Object.freeze({
|
|
169
|
+
title: listing.title,
|
|
170
|
+
description: listing.description,
|
|
171
|
+
mimeType: listing.mimeType,
|
|
172
|
+
icons: listing.icons,
|
|
173
|
+
annotations: listing.annotations,
|
|
174
|
+
_meta: listing._meta,
|
|
175
|
+
});
|
|
176
|
+
const cacheHint = definition.cacheHint === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: normalizeCacheHint(definition.cacheHint, `resource template ${name}`);
|
|
179
|
+
const variableNames = [...template.uriTemplate.variableNames];
|
|
180
|
+
const completions = checkedCompletionHandlers("Resource template", definition.complete, variableNames);
|
|
181
|
+
const registration = {
|
|
182
|
+
[RESOURCE_NAME]: name,
|
|
183
|
+
[RESOURCE_URI]: uriTemplate,
|
|
184
|
+
[RESOURCE_KIND]: "template",
|
|
185
|
+
[RESOURCE_MATCHES]: (uri) => template.uriTemplate.match(uri) !== null,
|
|
186
|
+
[RESOURCE_ROUTE]: route,
|
|
187
|
+
[RESOURCE_LISTING]: Object.freeze({
|
|
188
|
+
method: "resources/templates/list",
|
|
189
|
+
value: listing,
|
|
190
|
+
}),
|
|
191
|
+
[HAS_COMPLETION]: completions.size > 0,
|
|
192
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
193
|
+
const registeredTemplate = completions.size === 0
|
|
194
|
+
? template
|
|
195
|
+
: new ResourceTemplate(uriTemplate, {
|
|
196
|
+
list: undefined,
|
|
197
|
+
complete: Object.fromEntries([...completions].map(([variable, complete]) => [
|
|
198
|
+
variable,
|
|
199
|
+
completionCallback(complete, variable, variableNames, timeoutMs, maxApplicationResultBytes),
|
|
200
|
+
])),
|
|
201
|
+
});
|
|
202
|
+
server.registerResource(name, registeredTemplate, cacheHint ? { ...metadata, cacheHint } : metadata, async (requestedUri, variables, context) => {
|
|
203
|
+
try {
|
|
204
|
+
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
205
|
+
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
206
|
+
signal.throwIfAborted();
|
|
207
|
+
const result = await handler({ uri: requestedUri.href, variables }, {
|
|
208
|
+
signal,
|
|
209
|
+
deadlineMs,
|
|
210
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
211
|
+
});
|
|
212
|
+
signal.throwIfAborted();
|
|
213
|
+
if (isInputRequiredResult(result)) {
|
|
214
|
+
assertStatelessInputRequired(result);
|
|
215
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
const parsed = await ReadResourceResultSchema.safeParseAsync(result);
|
|
219
|
+
if (!parsed.success ||
|
|
220
|
+
parsed.data.contents.some((content) => content.uri !== requestedUri.href)) {
|
|
221
|
+
throw new Error("Resource template returned an invalid result");
|
|
222
|
+
}
|
|
223
|
+
signal.throwIfAborted();
|
|
224
|
+
assertResultSize(parsed.data, maxApplicationResultBytes, deadlineMs, signal);
|
|
225
|
+
return parsed.data;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw new Error("Resource read failed");
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
return Object.freeze(registration);
|
|
235
|
+
}
|
|
236
|
+
export function definePrompt(definition) {
|
|
237
|
+
const { name, argsSchema, handler } = definition;
|
|
238
|
+
assertRegistrationName("Prompt", name);
|
|
239
|
+
const argumentNames = Object.keys(argsSchema.shape);
|
|
240
|
+
const completions = checkedCompletionHandlers("Prompt", definition.complete, argumentNames);
|
|
241
|
+
const listing = checkedProtocolValue("Prompt", {
|
|
242
|
+
name,
|
|
243
|
+
title: definition.title,
|
|
244
|
+
description: definition.description,
|
|
245
|
+
icons: definition.icons,
|
|
246
|
+
_meta: definition._meta,
|
|
247
|
+
arguments: promptArguments(argsSchema),
|
|
248
|
+
});
|
|
249
|
+
const metadata = Object.freeze({
|
|
250
|
+
title: listing.title,
|
|
251
|
+
description: listing.description,
|
|
252
|
+
icons: listing.icons,
|
|
253
|
+
_meta: listing._meta,
|
|
254
|
+
});
|
|
255
|
+
const registration = {
|
|
256
|
+
[PROMPT_NAME]: name,
|
|
257
|
+
[HAS_COMPLETION]: completions.size > 0,
|
|
258
|
+
[PROMPT_LISTING]: listing,
|
|
259
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
260
|
+
server.registerPrompt(name, {
|
|
261
|
+
...metadata,
|
|
262
|
+
argsSchema: sdkPromptMetadataSchema(argsSchema, completions, argumentNames, timeoutMs, maxApplicationResultBytes),
|
|
263
|
+
}, async (args, context) => {
|
|
264
|
+
try {
|
|
265
|
+
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
266
|
+
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
267
|
+
const parsedArgs = await argsSchema.safeParseAsync(args);
|
|
268
|
+
if (!parsedArgs.success)
|
|
269
|
+
throw new Error("Prompt received invalid arguments");
|
|
270
|
+
signal.throwIfAborted();
|
|
271
|
+
const result = await handler(parsedArgs.data, {
|
|
272
|
+
signal,
|
|
273
|
+
deadlineMs,
|
|
274
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
275
|
+
});
|
|
276
|
+
signal.throwIfAborted();
|
|
277
|
+
if (isInputRequiredResult(result)) {
|
|
278
|
+
assertStatelessInputRequired(result);
|
|
279
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
const parsedResult = await GetPromptResultSchema.safeParseAsync(result);
|
|
283
|
+
if (!parsedResult.success)
|
|
284
|
+
throw new Error("Prompt returned an invalid result");
|
|
285
|
+
signal.throwIfAborted();
|
|
286
|
+
assertResultSize(parsedResult.data, maxApplicationResultBytes, deadlineMs, signal);
|
|
287
|
+
return parsedResult.data;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
throw new Error("Prompt rendering failed");
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
return Object.freeze(registration);
|
|
297
|
+
}
|
|
298
|
+
const progressUpdateSchema = z.strictObject({
|
|
299
|
+
progress: z.number().nonnegative(),
|
|
300
|
+
total: z.number().positive().optional(),
|
|
301
|
+
message: z.string().optional(),
|
|
302
|
+
});
|
|
303
|
+
function createCheckedTool(definition, execute, streaming, allowsInputRequired) {
|
|
304
|
+
const { name, inputSchema, outputSchema } = definition;
|
|
305
|
+
assertRegistrationName("Tool", name);
|
|
306
|
+
assertValidMcpHeaderAnnotations(inputSchema);
|
|
307
|
+
const access = normalizeToolAccess(definition.access, definition.requiredScopes);
|
|
308
|
+
const publicAccess = access === "public"
|
|
309
|
+
? Object.freeze({ type: "public" })
|
|
310
|
+
: Object.freeze({ type: "protected", requiredScopes: [...access.requiredScopes] });
|
|
311
|
+
const sdkInputSchema = sdkMetadataSchema(inputSchema);
|
|
312
|
+
const sdkOutputSchema = sdkMetadataSchema(outputSchema);
|
|
313
|
+
const listing = checkedProtocolValue("Tool", {
|
|
314
|
+
name,
|
|
315
|
+
title: definition.title,
|
|
316
|
+
description: definition.description,
|
|
317
|
+
icons: definition.icons,
|
|
318
|
+
annotations: definition.annotations,
|
|
319
|
+
inputSchema: jsonMetadataSchema(inputSchema, "input"),
|
|
320
|
+
outputSchema: jsonMetadataSchema(outputSchema, "output"),
|
|
321
|
+
_meta: {
|
|
322
|
+
...definition._meta,
|
|
323
|
+
"io.emseepea/access": publicAccess,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
const metadata = Object.freeze({
|
|
327
|
+
title: listing.title,
|
|
328
|
+
description: listing.description,
|
|
329
|
+
icons: listing.icons,
|
|
330
|
+
annotations: listing.annotations,
|
|
331
|
+
inputSchema: sdkInputSchema,
|
|
332
|
+
outputSchema: sdkOutputSchema,
|
|
333
|
+
_meta: listing._meta,
|
|
334
|
+
});
|
|
335
|
+
const registration = {
|
|
336
|
+
[TOOL_NAME]: name,
|
|
337
|
+
[TOOL_ACCESS]: access,
|
|
338
|
+
[TOOL_STREAMING]: streaming,
|
|
339
|
+
[TOOL_LISTING]: listing,
|
|
340
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes) {
|
|
341
|
+
server.registerTool(name, metadata, async (input, context) => {
|
|
342
|
+
try {
|
|
343
|
+
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
344
|
+
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
345
|
+
const parsedInput = await inputSchema.safeParseAsync(input);
|
|
346
|
+
if (!parsedInput.success) {
|
|
347
|
+
throw new Error("Tool received input that does not match its schema");
|
|
348
|
+
}
|
|
349
|
+
signal.throwIfAborted();
|
|
350
|
+
const reporter = streaming
|
|
351
|
+
? progressReporter(context, signal, maxProgressEvents, maxProgressEventBytes)
|
|
352
|
+
: undefined;
|
|
353
|
+
let result;
|
|
354
|
+
try {
|
|
355
|
+
result = await execute(parsedInput.data, {
|
|
356
|
+
signal,
|
|
357
|
+
deadlineMs,
|
|
358
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
359
|
+
...(allowsInputRequired
|
|
360
|
+
? {
|
|
361
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
362
|
+
}
|
|
363
|
+
: {}),
|
|
364
|
+
...(reporter ? { reportProgress: reporter.report } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await reporter?.finish();
|
|
369
|
+
}
|
|
370
|
+
reporter?.throwIfFailed();
|
|
371
|
+
if (allowsInputRequired && isInputRequiredResult(result)) {
|
|
372
|
+
signal.throwIfAborted();
|
|
373
|
+
assertStatelessInputRequired(result);
|
|
374
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
if (!isRecord(result) || typeof result.text !== "string" || !("data" in result)) {
|
|
378
|
+
throw new Error("Tool returned an invalid result");
|
|
379
|
+
}
|
|
380
|
+
signal.throwIfAborted();
|
|
381
|
+
const parsedOutput = await outputSchema.safeParseAsync(result.data);
|
|
382
|
+
if (!parsedOutput.success) {
|
|
383
|
+
throw new Error("Tool returned output that does not match its schema");
|
|
384
|
+
}
|
|
385
|
+
const publicResult = {
|
|
386
|
+
content: [{ type: "text", text: result.text }],
|
|
387
|
+
structuredContent: parsedOutput.data,
|
|
388
|
+
isError: false,
|
|
389
|
+
};
|
|
390
|
+
assertResultSize(publicResult, maxApplicationResultBytes, deadlineMs, signal);
|
|
391
|
+
return publicResult;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return { content: [{ type: "text", text: "Tool execution failed" }], isError: true };
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
return Object.freeze(registration);
|
|
401
|
+
}
|
|
402
|
+
function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
403
|
+
const token = context.mcpReq._meta?.progressToken;
|
|
404
|
+
let closed = false;
|
|
405
|
+
let failure;
|
|
406
|
+
let attempts = 0;
|
|
407
|
+
let previousProgress = -Infinity;
|
|
408
|
+
const pending = new Set();
|
|
409
|
+
return {
|
|
410
|
+
report(update) {
|
|
411
|
+
if (closed)
|
|
412
|
+
return Promise.reject(new Error("Progress is no longer available"));
|
|
413
|
+
if (failure)
|
|
414
|
+
return Promise.reject(failure);
|
|
415
|
+
const operation = (async () => {
|
|
416
|
+
try {
|
|
417
|
+
signal.throwIfAborted();
|
|
418
|
+
attempts += 1;
|
|
419
|
+
if (attempts > maxEvents)
|
|
420
|
+
throw new Error("Progress event limit exceeded");
|
|
421
|
+
const parsed = progressUpdateSchema.safeParse(update);
|
|
422
|
+
if (!parsed.success || parsed.data.progress <= previousProgress ||
|
|
423
|
+
(parsed.data.total !== undefined && parsed.data.progress > parsed.data.total)) {
|
|
424
|
+
throw new Error("Progress update is invalid");
|
|
425
|
+
}
|
|
426
|
+
previousProgress = parsed.data.progress;
|
|
427
|
+
if (token !== undefined && typeof token !== "string" && typeof token !== "number") {
|
|
428
|
+
throw new Error("Progress token is invalid");
|
|
429
|
+
}
|
|
430
|
+
const notification = {
|
|
431
|
+
method: "notifications/progress",
|
|
432
|
+
params: { progressToken: token ?? 0, ...parsed.data },
|
|
433
|
+
};
|
|
434
|
+
if (Buffer.byteLength(JSON.stringify(notification), "utf8") > maxEventBytes) {
|
|
435
|
+
throw new Error("Progress event exceeds configured size limit");
|
|
436
|
+
}
|
|
437
|
+
if (token !== undefined)
|
|
438
|
+
await context.mcpReq.notify(notification);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
failure = error instanceof Error ? error : new Error("Progress emission failed");
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
})();
|
|
445
|
+
pending.add(operation);
|
|
446
|
+
void operation.then(() => pending.delete(operation), () => pending.delete(operation));
|
|
447
|
+
return operation;
|
|
448
|
+
},
|
|
449
|
+
async finish() {
|
|
450
|
+
closed = true;
|
|
451
|
+
await Promise.allSettled([...pending]);
|
|
452
|
+
},
|
|
453
|
+
throwIfFailed() { if (failure)
|
|
454
|
+
throw failure; },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function normalizeListPagination(options) {
|
|
458
|
+
const pageSize = positiveInteger("listPagination.pageSize", options.pageSize);
|
|
459
|
+
if (pageSize > 100)
|
|
460
|
+
throw new TypeError("listPagination.pageSize must not exceed 100");
|
|
461
|
+
return Object.freeze({
|
|
462
|
+
pageSize,
|
|
463
|
+
maxPageBytes: positiveInteger("listPagination.maxPageBytes", options.maxPageBytes ?? 1024 * 1024),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function compileListPagination(options, catalogues) {
|
|
467
|
+
const compiled = new Map();
|
|
468
|
+
for (const [method, entries] of catalogues) {
|
|
469
|
+
if (entries.length === 0)
|
|
470
|
+
continue;
|
|
471
|
+
const resultKey = method === "tools/list"
|
|
472
|
+
? "tools"
|
|
473
|
+
: method === "prompts/list"
|
|
474
|
+
? "prompts"
|
|
475
|
+
: method === "resources/templates/list"
|
|
476
|
+
? "resourceTemplates"
|
|
477
|
+
: "resources";
|
|
478
|
+
compiled.set(method, compileCataloguePages(method, resultKey, entries, options));
|
|
479
|
+
}
|
|
480
|
+
return compiled;
|
|
481
|
+
}
|
|
482
|
+
function compileCataloguePages(method, resultKey, entries, options) {
|
|
483
|
+
const groups = [];
|
|
484
|
+
for (let index = 0; index < entries.length;) {
|
|
485
|
+
const group = [];
|
|
486
|
+
while (group.length < options.pageSize && index < entries.length) {
|
|
487
|
+
const candidate = [...group, entries[index]];
|
|
488
|
+
const hasNext = index + 1 < entries.length;
|
|
489
|
+
if (cataloguePageBytes(resultKey, candidate, hasNext) > options.maxPageBytes) {
|
|
490
|
+
if (group.length === 0) {
|
|
491
|
+
throw new TypeError(`${method} has an entry larger than listPagination.maxPageBytes`);
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
group.push(entries[index]);
|
|
496
|
+
index += 1;
|
|
497
|
+
}
|
|
498
|
+
groups.push(group);
|
|
499
|
+
}
|
|
500
|
+
const catalogueDigest = createHash("sha256").update(JSON.stringify(entries)).digest("base64url");
|
|
501
|
+
const cursorFor = (page) => createHash("sha256").update([
|
|
502
|
+
"emseepea-list-cursor-v1",
|
|
503
|
+
method,
|
|
504
|
+
String(page),
|
|
505
|
+
String(options.pageSize),
|
|
506
|
+
String(options.maxPageBytes),
|
|
507
|
+
catalogueDigest,
|
|
508
|
+
].join("\n")).digest("base64url");
|
|
509
|
+
const pages = groups.map((group, index) => Object.freeze({
|
|
510
|
+
[resultKey]: Object.freeze([...group]),
|
|
511
|
+
...(index + 1 < groups.length ? { nextCursor: cursorFor(index + 1) } : {}),
|
|
512
|
+
}));
|
|
513
|
+
const byCursor = new Map();
|
|
514
|
+
for (let index = 1; index < pages.length; index += 1) {
|
|
515
|
+
byCursor.set(cursorFor(index), pages[index]);
|
|
516
|
+
}
|
|
517
|
+
return Object.freeze({ first: pages[0], byCursor });
|
|
518
|
+
}
|
|
519
|
+
function cataloguePageBytes(resultKey, entries, hasNext) {
|
|
520
|
+
return Buffer.byteLength(JSON.stringify({
|
|
521
|
+
[resultKey]: entries,
|
|
522
|
+
...(hasNext ? { nextCursor: "x".repeat(43) } : {}),
|
|
523
|
+
}), "utf8");
|
|
524
|
+
}
|
|
525
|
+
function paginationPage(pages, cursor) {
|
|
526
|
+
if (cursor === undefined)
|
|
527
|
+
return pages.first;
|
|
528
|
+
const page = /^[A-Za-z0-9_-]{43}$/.test(cursor) ? pages.byCursor.get(cursor) : undefined;
|
|
529
|
+
if (!page)
|
|
530
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Invalid pagination cursor");
|
|
531
|
+
return page;
|
|
532
|
+
}
|
|
533
|
+
function installListPagination(server, pagination) {
|
|
534
|
+
const tools = pagination.get("tools/list");
|
|
535
|
+
if (tools)
|
|
536
|
+
server.server.setRequestHandler("tools/list", (request) => paginationPage(tools, request.params?.cursor));
|
|
537
|
+
const resources = pagination.get("resources/list");
|
|
538
|
+
if (resources)
|
|
539
|
+
server.server.setRequestHandler("resources/list", (request) => paginationPage(resources, request.params?.cursor));
|
|
540
|
+
const templates = pagination.get("resources/templates/list");
|
|
541
|
+
if (templates)
|
|
542
|
+
server.server.setRequestHandler("resources/templates/list", (request) => paginationPage(templates, request.params?.cursor));
|
|
543
|
+
const prompts = pagination.get("prompts/list");
|
|
544
|
+
if (prompts)
|
|
545
|
+
server.server.setRequestHandler("prompts/list", (request) => paginationPage(prompts, request.params?.cursor));
|
|
546
|
+
}
|
|
547
|
+
export function createEmseepea(options) {
|
|
548
|
+
if (options.telemetry !== undefined && typeof options.telemetry !== "boolean") {
|
|
549
|
+
throw new TypeError("telemetry must be a boolean");
|
|
550
|
+
}
|
|
551
|
+
const readiness = options.readiness;
|
|
552
|
+
const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
|
|
553
|
+
const stopping = new AbortController();
|
|
554
|
+
let pendingReadiness;
|
|
555
|
+
async function ready() {
|
|
556
|
+
if (stopping.signal.aborted)
|
|
557
|
+
return false;
|
|
558
|
+
if (!readiness)
|
|
559
|
+
return true;
|
|
560
|
+
if (!pendingReadiness) {
|
|
561
|
+
const deadline = Date.now() + readinessTimeoutMs;
|
|
562
|
+
pendingReadiness = runWithDeadline(stopping.signal, deadline, async (signal) => {
|
|
563
|
+
try {
|
|
564
|
+
return await readiness({ signal }) === true && Date.now() < deadline;
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
pendingReadiness = undefined;
|
|
568
|
+
}
|
|
569
|
+
}).catch(() => false);
|
|
570
|
+
}
|
|
571
|
+
const result = await pendingReadiness;
|
|
572
|
+
return result && !stopping.signal.aborted;
|
|
573
|
+
}
|
|
574
|
+
assertNonEmpty("name", options.name);
|
|
575
|
+
assertNonEmpty("version", options.version);
|
|
576
|
+
const serverInfo = checkedProtocolValue("Implementation", {
|
|
577
|
+
name: options.name,
|
|
578
|
+
version: options.version,
|
|
579
|
+
title: options.title,
|
|
580
|
+
description: options.description,
|
|
581
|
+
icons: options.icons,
|
|
582
|
+
websiteUrl: options.websiteUrl,
|
|
583
|
+
});
|
|
584
|
+
const tools = Object.freeze([...(options.tools ?? [])]);
|
|
585
|
+
const resources = Object.freeze([...(options.resources ?? [])]);
|
|
586
|
+
const prompts = Object.freeze([...(options.prompts ?? [])]);
|
|
587
|
+
assertUniqueToolNames(tools);
|
|
588
|
+
assertUniqueResources(resources);
|
|
589
|
+
assertUniquePromptNames(prompts);
|
|
590
|
+
const paginationOptions = options.listPagination
|
|
591
|
+
? normalizeListPagination(options.listPagination)
|
|
592
|
+
: undefined;
|
|
593
|
+
const maxRequestBytes = positiveInteger("maxRequestBytes", options.maxRequestBytes ?? 1024 * 1024);
|
|
594
|
+
const maxApplicationResultBytes = positiveInteger("maxApplicationResultBytes", options.maxApplicationResultBytes ?? 1024 * 1024);
|
|
595
|
+
const maxProgressEvents = positiveInteger("maxProgressEvents", options.maxProgressEvents ?? 32);
|
|
596
|
+
const maxProgressEventBytes = positiveInteger("maxProgressEventBytes", options.maxProgressEventBytes ?? 8 * 1024);
|
|
597
|
+
const operationTimeoutMs = positiveInteger("operationTimeoutMs", options.operationTimeoutMs ?? 30_000);
|
|
598
|
+
const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
|
|
599
|
+
const oauth = options.oauth ? normalizeOAuth(options.oauth) : undefined;
|
|
600
|
+
const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
|
|
601
|
+
if (deployment.mode !== "loopback" &&
|
|
602
|
+
tools.some((tool) => tool[TOOL_STREAMING] && tool[TOOL_ACCESS] !== "public")) {
|
|
603
|
+
throw new TypeError("Protected streaming tools currently require the loopback deployment profile");
|
|
604
|
+
}
|
|
605
|
+
if (tools.some((tool) => tool[TOOL_ACCESS] !== "public") && !oauth) {
|
|
606
|
+
throw new TypeError("Protected tools require OAuth resource-server configuration");
|
|
607
|
+
}
|
|
608
|
+
const toolsByName = new Map(tools.map((tool) => [tool[TOOL_NAME], tool]));
|
|
609
|
+
const enabledMethods = new Set(["server/discover"]);
|
|
610
|
+
if (tools.length)
|
|
611
|
+
enabledMethods.add("tools/list").add("tools/call");
|
|
612
|
+
if (resources.length)
|
|
613
|
+
enabledMethods.add("resources/list").add("resources/read");
|
|
614
|
+
if (resources.some((resource) => resource[RESOURCE_KIND] === "template")) {
|
|
615
|
+
enabledMethods.add("resources/templates/list");
|
|
616
|
+
}
|
|
617
|
+
if (prompts.length)
|
|
618
|
+
enabledMethods.add("prompts/list").add("prompts/get");
|
|
619
|
+
const hasCompletion = resources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
620
|
+
prompts.some((prompt) => prompt[HAS_COMPLETION]);
|
|
621
|
+
if (hasCompletion)
|
|
622
|
+
enabledMethods.add("completion/complete");
|
|
623
|
+
const cacheHints = options.cacheHints === undefined
|
|
624
|
+
? undefined
|
|
625
|
+
: normalizeCacheHints(options.cacheHints, enabledMethods);
|
|
626
|
+
const pagination = paginationOptions
|
|
627
|
+
? compileListPagination(paginationOptions, new Map([
|
|
628
|
+
["tools/list", tools.map((tool) => tool[TOOL_LISTING])],
|
|
629
|
+
["resources/list", resources
|
|
630
|
+
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/list")
|
|
631
|
+
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
632
|
+
["resources/templates/list", resources
|
|
633
|
+
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/templates/list")
|
|
634
|
+
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
635
|
+
["prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING])],
|
|
636
|
+
]))
|
|
637
|
+
: undefined;
|
|
638
|
+
const sdkHandler = createMcpHandler(() => {
|
|
639
|
+
const server = new McpServer(serverInfo, {
|
|
640
|
+
capabilities: {
|
|
641
|
+
...(tools.length ? { tools: { listChanged: false } } : {}),
|
|
642
|
+
...(resources.length ? { resources: { subscribe: false, listChanged: false } } : {}),
|
|
643
|
+
...(prompts.length ? { prompts: { listChanged: false } } : {}),
|
|
644
|
+
...(hasCompletion ? { completions: {} } : {}),
|
|
645
|
+
},
|
|
646
|
+
instructions: options.instructions,
|
|
647
|
+
cacheHints,
|
|
648
|
+
supportedProtocolVersions: [PROTOCOL_VERSION],
|
|
649
|
+
});
|
|
650
|
+
for (const tool of tools) {
|
|
651
|
+
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes);
|
|
652
|
+
}
|
|
653
|
+
for (const resource of resources)
|
|
654
|
+
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
655
|
+
for (const prompt of prompts)
|
|
656
|
+
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
657
|
+
if (pagination)
|
|
658
|
+
installListPagination(server, pagination);
|
|
659
|
+
return server;
|
|
660
|
+
}, {
|
|
661
|
+
legacy: "reject",
|
|
662
|
+
responseMode: hasStreaming ? "auto" : "json",
|
|
663
|
+
keepAliveMs: 0,
|
|
664
|
+
});
|
|
665
|
+
const nodeHandler = toNodeHandler(sdkHandler);
|
|
666
|
+
const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
|
|
667
|
+
const finishTelemetry = options.telemetry === true ? installRequestTelemetry(app) : undefined;
|
|
668
|
+
const limiter = deployment.mode === "production-behind-proxy"
|
|
669
|
+
? new FixedWindowRateLimiter(deployment.rateLimit)
|
|
670
|
+
: undefined;
|
|
671
|
+
app.get("/healthz", health("ok\n"));
|
|
672
|
+
app.get("/readyz", async (_request, reply) => {
|
|
673
|
+
const available = await ready();
|
|
674
|
+
await reply.header("cache-control", "no-store").type("text/plain; charset=utf-8")
|
|
675
|
+
.code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
|
|
676
|
+
});
|
|
677
|
+
app.addHook("preClose", async () => { stopping.abort(); });
|
|
678
|
+
if (oauth) {
|
|
679
|
+
app.all("/.well-known/*", async (request, reply) => {
|
|
680
|
+
const response = oauthMetadataResponse(new Request(new URL(request.url, oauth.metadata.resourceServerUrl.origin), {
|
|
681
|
+
method: request.method,
|
|
682
|
+
}), oauth.metadata);
|
|
683
|
+
if (!response) {
|
|
684
|
+
await reply.code(404).send();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
await sendWebResponse(reply, response);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
app.route({
|
|
691
|
+
method: ["GET", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
692
|
+
url: "/mcp",
|
|
693
|
+
handler: async (_request, reply) => { await reply.header("allow", "POST").code(405).send(); },
|
|
694
|
+
});
|
|
695
|
+
app.post("/mcp", { bodyLimit: maxRequestBytes }, async (request, reply) => {
|
|
696
|
+
if (deployment.mode === "production-behind-proxy" &&
|
|
697
|
+
!validateProductionRequest(request, reply, deployment, limiter))
|
|
698
|
+
return;
|
|
699
|
+
if (!await validateMcpRequestHeaders(request, reply))
|
|
700
|
+
return;
|
|
701
|
+
if (isRecord(request.body) && typeof request.body.method === "string" &&
|
|
702
|
+
!enabledMethods.has(request.body.method)) {
|
|
703
|
+
await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const access = protectedAccessForCall(request.body, toolsByName);
|
|
707
|
+
if (access && oauth) {
|
|
708
|
+
try {
|
|
709
|
+
const authInfo = await verifyProtectedCall(request, reply, access, oauth);
|
|
710
|
+
request.raw.auth = authInfo;
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
await sendWebResponse(reply, bearerAuthChallengeResponse(safeOAuthError(error), {
|
|
714
|
+
requiredScopes: [...access.requiredScopes],
|
|
715
|
+
resourceMetadataUrl: oauth.resourceMetadataUrl,
|
|
716
|
+
}));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const disconnected = new AbortController();
|
|
721
|
+
const abort = () => disconnected.abort();
|
|
722
|
+
request.raw.once("aborted", abort);
|
|
723
|
+
reply.raw.once("close", abort);
|
|
724
|
+
if (requestAlreadyClosed(request, reply))
|
|
725
|
+
abort();
|
|
726
|
+
reply.hijack();
|
|
727
|
+
try {
|
|
728
|
+
await requestOperations.run({ deadlineMs: Date.now() + operationTimeoutMs, signal: disconnected.signal }, () => nodeHandler(request.raw, reply.raw, request.body));
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
request.raw.off("aborted", abort);
|
|
732
|
+
reply.raw.off("close", abort);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
app.setErrorHandler(async (error, _request, reply) => {
|
|
736
|
+
if (isFastifyError(error) && error.code === "FST_ERR_CTP_BODY_TOO_LARGE") {
|
|
737
|
+
await sendRpcError(reply, 413, -32600, "Request exceeds configured size limit");
|
|
738
|
+
}
|
|
739
|
+
else if (isJsonParseError(error)) {
|
|
740
|
+
await sendRpcError(reply, 400, -32700, "Parse error");
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
await sendRpcError(reply, 500, -32603, "Internal error");
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
app.addHook("onClose", async () => sdkHandler.close());
|
|
747
|
+
runtimes.set(app, { deployment, requestTimeoutMs: operationTimeoutMs + 5_000, stopping, finishTelemetry });
|
|
748
|
+
return app;
|
|
749
|
+
}
|
|
750
|
+
function safeOAuthError(error) {
|
|
751
|
+
if (!(error instanceof OAuthError))
|
|
752
|
+
return error;
|
|
753
|
+
const message = error.code === OAuthErrorCode.InvalidToken
|
|
754
|
+
? "Invalid access token"
|
|
755
|
+
: error.code === OAuthErrorCode.InsufficientScope
|
|
756
|
+
? "Insufficient scope"
|
|
757
|
+
: error.code === OAuthErrorCode.ServerError
|
|
758
|
+
? "Authorization service error"
|
|
759
|
+
: "Authorization request failed";
|
|
760
|
+
return new OAuthError(error.code, message);
|
|
761
|
+
}
|
|
762
|
+
async function verifyProtectedCall(request, reply, access, oauth) {
|
|
763
|
+
const disconnected = new AbortController();
|
|
764
|
+
const abort = () => disconnected.abort();
|
|
765
|
+
request.raw.once("aborted", abort);
|
|
766
|
+
reply.raw.once("close", abort);
|
|
767
|
+
if (requestAlreadyClosed(request, reply))
|
|
768
|
+
abort();
|
|
769
|
+
try {
|
|
770
|
+
const authInfo = await runWithDeadline(disconnected.signal, Date.now() + oauth.verificationTimeoutMs, () => verifyBearerToken(singleHeader(request.raw.rawHeaders, "authorization"), {
|
|
771
|
+
verifier: oauth.verifier,
|
|
772
|
+
requiredScopes: [...access.requiredScopes],
|
|
773
|
+
resourceMetadataUrl: oauth.resourceMetadataUrl,
|
|
774
|
+
}));
|
|
775
|
+
if (!authInfo.resource || authInfo.resource.hash || !checkResourceAllowed({
|
|
776
|
+
requestedResource: authInfo.resource,
|
|
777
|
+
configuredResource: oauth.metadata.resourceServerUrl,
|
|
778
|
+
})) {
|
|
779
|
+
throw new OAuthError(OAuthErrorCode.InvalidToken, "Token is not valid for this resource");
|
|
780
|
+
}
|
|
781
|
+
return authInfo;
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
request.raw.off("aborted", abort);
|
|
785
|
+
reply.raw.off("close", abort);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function requestAlreadyClosed(request, reply) {
|
|
789
|
+
return request.raw.aborted || (request.raw.destroyed && !request.raw.complete) || reply.raw.destroyed;
|
|
790
|
+
}
|
|
791
|
+
async function sendWebResponse(reply, response) {
|
|
792
|
+
for (const [name, value] of response.headers)
|
|
793
|
+
reply.header(name, value);
|
|
794
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
795
|
+
await reply.code(response.status).send(body.length ? body : undefined);
|
|
796
|
+
}
|
|
797
|
+
function protectedAccessForCall(body, toolsByName) {
|
|
798
|
+
if (!isRecord(body) || body.method !== "tools/call" || !isRecord(body.params) ||
|
|
799
|
+
typeof body.params.name !== "string")
|
|
800
|
+
return undefined;
|
|
801
|
+
const access = toolsByName.get(body.params.name)?.[TOOL_ACCESS];
|
|
802
|
+
return access === "public" ? undefined : access;
|
|
803
|
+
}
|
|
804
|
+
function principalFrom(authInfo) {
|
|
805
|
+
if (!authInfo)
|
|
806
|
+
throw new Error("Protected tool reached execution without verified authorization");
|
|
807
|
+
return {
|
|
808
|
+
clientId: authInfo.clientId,
|
|
809
|
+
scopes: [...authInfo.scopes],
|
|
810
|
+
resource: authInfo.resource?.href,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
export async function serveEmseepea(app, options = {}) {
|
|
814
|
+
const runtime = runtimes.get(app);
|
|
815
|
+
if (!runtime)
|
|
816
|
+
throw new TypeError("serveEmseepea requires an app created by createEmseepea");
|
|
817
|
+
const host = options.host ?? "127.0.0.1";
|
|
818
|
+
if (runtime.deployment.mode === "loopback" && !isLoopbackHost(host)) {
|
|
819
|
+
throw new TypeError("The loopback deployment profile cannot bind publicly");
|
|
820
|
+
}
|
|
821
|
+
const port = nonNegativePort(options.port ?? 3000);
|
|
822
|
+
const shutdownTimeoutMs = positiveInteger("shutdownTimeoutMs", options.shutdownTimeoutMs ?? 5_000);
|
|
823
|
+
const flushTelemetry = options.flushTelemetry;
|
|
824
|
+
const telemetryFlushTimeoutMs = callbackTimeout("flushTelemetry", flushTelemetry, "telemetryFlushTimeoutMs", options.telemetryFlushTimeoutMs);
|
|
825
|
+
await app.listen({ host, port });
|
|
826
|
+
app.server.headersTimeout = 10_000;
|
|
827
|
+
app.server.keepAliveTimeout = 5_000;
|
|
828
|
+
app.server.requestTimeout = runtime.requestTimeoutMs;
|
|
829
|
+
app.server.maxHeadersCount = 100;
|
|
830
|
+
app.server.maxRequestsPerSocket = 1_000;
|
|
831
|
+
const address = app.server.address();
|
|
832
|
+
if (!address || typeof address === "string") {
|
|
833
|
+
await app.close();
|
|
834
|
+
throw new Error("Fastify did not expose a TCP listening address");
|
|
835
|
+
}
|
|
836
|
+
const urlHost = host === "::1" ? "[::1]" : host;
|
|
837
|
+
let closing;
|
|
838
|
+
return {
|
|
839
|
+
url: new URL(`http://${urlHost}:${address.port}/mcp`),
|
|
840
|
+
close: () => {
|
|
841
|
+
runtime.stopping.abort();
|
|
842
|
+
return closing ??= closeApp(app, shutdownTimeoutMs, flushTelemetry, telemetryFlushTimeoutMs, runtime.finishTelemetry);
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function health(body) {
|
|
847
|
+
return async (_request, reply) => {
|
|
848
|
+
await reply.header("cache-control", "no-store").type("text/plain; charset=utf-8").send(body);
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
async function sendRpcError(reply, status, code, message, id = null, data) {
|
|
852
|
+
await reply.code(status).send({
|
|
853
|
+
jsonrpc: "2.0",
|
|
854
|
+
id,
|
|
855
|
+
error: { code, message, ...(data ? { data } : {}) },
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function isFastifyError(error) {
|
|
859
|
+
return error instanceof Error && "code" in error;
|
|
860
|
+
}
|
|
861
|
+
function isJsonParseError(error) {
|
|
862
|
+
return (error instanceof SyntaxError && "statusCode" in error && error.statusCode === 400) ||
|
|
863
|
+
(isFastifyError(error) &&
|
|
864
|
+
(error.code === "FST_ERR_CTP_INVALID_JSON_BODY" ||
|
|
865
|
+
error.code === "FST_ERR_CTP_EMPTY_JSON_BODY"));
|
|
866
|
+
}
|
|
867
|
+
function validateProductionRequest(request, reply, deployment, limiter) {
|
|
868
|
+
const reject = (status, message) => {
|
|
869
|
+
void sendRpcError(reply, status, -32004, message);
|
|
870
|
+
return false;
|
|
871
|
+
};
|
|
872
|
+
const peer = normalizeIp(request.raw.socket.remoteAddress);
|
|
873
|
+
if (!peer || !deployment.trustedProxyAddresses.has(peer)) {
|
|
874
|
+
return reject(403, "Request did not come from a trusted proxy");
|
|
875
|
+
}
|
|
876
|
+
const proto = singleHeader(request.raw.rawHeaders, "x-forwarded-proto");
|
|
877
|
+
const forwardedFor = singleHeader(request.raw.rawHeaders, "x-forwarded-for");
|
|
878
|
+
const host = singleHeader(request.raw.rawHeaders, "host");
|
|
879
|
+
const origin = singleHeader(request.raw.rawHeaders, "origin");
|
|
880
|
+
const client = forwardedFor && !forwardedFor.includes(",") ? normalizeIp(forwardedFor) : undefined;
|
|
881
|
+
if (proto !== "https" || !client)
|
|
882
|
+
return reject(403, "Invalid forwarding metadata");
|
|
883
|
+
if (!host || !deployment.allowedAuthorities.has(normalizeAuthority(host) ?? "")) {
|
|
884
|
+
return reject(403, "Authority is not allowed");
|
|
885
|
+
}
|
|
886
|
+
if (origin !== undefined && (!origin || !deployment.allowedOrigins.has(normalizeOrigin(origin) ?? ""))) {
|
|
887
|
+
return reject(403, "Origin is not allowed");
|
|
888
|
+
}
|
|
889
|
+
const result = limiter.take(client);
|
|
890
|
+
if (result === "full")
|
|
891
|
+
return reject(503, "Rate-limit capacity is exhausted");
|
|
892
|
+
if (result === "limited")
|
|
893
|
+
return reject(429, "Rate limit exceeded");
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
class FixedWindowRateLimiter {
|
|
897
|
+
options;
|
|
898
|
+
#clients = new Map();
|
|
899
|
+
constructor(options) {
|
|
900
|
+
this.options = options;
|
|
901
|
+
}
|
|
902
|
+
take(client, now = Date.now()) {
|
|
903
|
+
const current = this.#clients.get(client);
|
|
904
|
+
if (current && now - current.startedAt < this.options.windowMs) {
|
|
905
|
+
current.count += 1;
|
|
906
|
+
return current.count <= this.options.maxRequests ? "allowed" : "limited";
|
|
907
|
+
}
|
|
908
|
+
if (current) {
|
|
909
|
+
this.#clients.set(client, { count: 1, startedAt: now });
|
|
910
|
+
return "allowed";
|
|
911
|
+
}
|
|
912
|
+
for (const [key, entry] of this.#clients) {
|
|
913
|
+
if (now - entry.startedAt >= this.options.windowMs)
|
|
914
|
+
this.#clients.delete(key);
|
|
915
|
+
}
|
|
916
|
+
if (this.#clients.size >= this.options.maxClients)
|
|
917
|
+
return "full";
|
|
918
|
+
this.#clients.set(client, { count: 1, startedAt: now });
|
|
919
|
+
return "allowed";
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function normalizeToolAccess(access, requiredScopes) {
|
|
923
|
+
if (access === "public")
|
|
924
|
+
return access;
|
|
925
|
+
if (access !== "protected" || !Array.isArray(requiredScopes) || requiredScopes.length === 0) {
|
|
926
|
+
throw new TypeError('Tool access must be explicitly declared as "public" or protected with scopes');
|
|
927
|
+
}
|
|
928
|
+
const scopes = requiredScopes.map((scope) => {
|
|
929
|
+
if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
930
|
+
throw new TypeError("Protected tool scopes must be valid OAuth scope tokens");
|
|
931
|
+
}
|
|
932
|
+
return scope;
|
|
933
|
+
});
|
|
934
|
+
if (new Set(scopes).size !== scopes.length) {
|
|
935
|
+
throw new TypeError("Protected tool scopes must be unique");
|
|
936
|
+
}
|
|
937
|
+
return { type: "protected", requiredScopes: scopes };
|
|
938
|
+
}
|
|
939
|
+
function normalizeOAuth(options) {
|
|
940
|
+
if (!options.verifier || typeof options.verifier.verifyAccessToken !== "function") {
|
|
941
|
+
throw new TypeError("oauth.verifier must implement verifyAccessToken");
|
|
942
|
+
}
|
|
943
|
+
if (options.metadata.dangerouslyAllowInsecureIssuerUrl === true) {
|
|
944
|
+
throw new TypeError("Insecure OAuth issuer URLs are not supported");
|
|
945
|
+
}
|
|
946
|
+
const resourceServerUrl = options.metadata.resourceServerUrl;
|
|
947
|
+
if (resourceServerUrl.hash || resourceServerUrl.username || resourceServerUrl.password ||
|
|
948
|
+
(resourceServerUrl.protocol !== "https:" &&
|
|
949
|
+
!(resourceServerUrl.protocol === "http:" && isLoopbackUrl(resourceServerUrl)))) {
|
|
950
|
+
throw new TypeError("OAuth resource-server URL must be HTTPS, or HTTP on loopback, without credentials or a fragment");
|
|
951
|
+
}
|
|
952
|
+
buildOAuthProtectedResourceMetadata(options.metadata);
|
|
953
|
+
return {
|
|
954
|
+
verifier: options.verifier,
|
|
955
|
+
metadata: options.metadata,
|
|
956
|
+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(options.metadata.resourceServerUrl),
|
|
957
|
+
verificationTimeoutMs: positiveInteger("oauth.verificationTimeoutMs", options.verificationTimeoutMs ?? 10_000),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function isLoopbackUrl(url) {
|
|
961
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
962
|
+
}
|
|
963
|
+
function normalizeDeployment(profile) {
|
|
964
|
+
if (profile.mode === "loopback")
|
|
965
|
+
return profile;
|
|
966
|
+
if (!profile.allowedAuthorities.length || !profile.allowedOrigins.length ||
|
|
967
|
+
!profile.trustedProxyAddresses.length) {
|
|
968
|
+
throw new TypeError("Production deployment allowlists must not be empty");
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
mode: profile.mode,
|
|
972
|
+
allowedAuthorities: new Set(profile.allowedAuthorities.map((value) => requiredNormalized("allowed authority", value, normalizeAuthority))),
|
|
973
|
+
allowedOrigins: new Set(profile.allowedOrigins.map((value) => requiredNormalized("allowed HTTPS origin", value, normalizeOrigin))),
|
|
974
|
+
trustedProxyAddresses: new Set(profile.trustedProxyAddresses.map((value) => requiredNormalized("trusted proxy IP address", value, normalizeIp))),
|
|
975
|
+
rateLimit: {
|
|
976
|
+
maxRequests: positiveInteger("rateLimit.maxRequests", profile.rateLimit.maxRequests),
|
|
977
|
+
windowMs: positiveInteger("rateLimit.windowMs", profile.rateLimit.windowMs),
|
|
978
|
+
maxClients: positiveInteger("rateLimit.maxClients", profile.rateLimit.maxClients),
|
|
979
|
+
},
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function requiredNormalized(label, value, normalize) {
|
|
983
|
+
const normalized = normalize(value);
|
|
984
|
+
if (!normalized)
|
|
985
|
+
throw new TypeError(`Invalid ${label}: ${value}`);
|
|
986
|
+
return normalized;
|
|
987
|
+
}
|
|
988
|
+
function normalizeIp(value) {
|
|
989
|
+
if (!value)
|
|
990
|
+
return undefined;
|
|
991
|
+
const candidate = value.startsWith("::ffff:") ? value.slice(7) : value;
|
|
992
|
+
return isIP(candidate) ? candidate.toLowerCase() : undefined;
|
|
993
|
+
}
|
|
994
|
+
function normalizeAuthority(value) {
|
|
995
|
+
if (value.trim() !== value || /[/?#@]/.test(value))
|
|
996
|
+
return undefined;
|
|
997
|
+
try {
|
|
998
|
+
const url = new URL(`https://${value}`);
|
|
999
|
+
const port = url.port === "443" ? "" : url.port;
|
|
1000
|
+
return `${url.hostname.toLowerCase()}${port ? `:${port}` : ""}`;
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function normalizeOrigin(value) {
|
|
1007
|
+
try {
|
|
1008
|
+
const url = new URL(value);
|
|
1009
|
+
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" ||
|
|
1010
|
+
url.search || url.hash)
|
|
1011
|
+
return undefined;
|
|
1012
|
+
const authority = normalizeAuthority(url.host);
|
|
1013
|
+
return authority ? `https://${authority}` : undefined;
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
return undefined;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function singleHeader(rawHeaders, name) {
|
|
1020
|
+
const values = [];
|
|
1021
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
1022
|
+
if (rawHeaders[index]?.toLowerCase() === name)
|
|
1023
|
+
values.push(rawHeaders[index + 1] ?? "");
|
|
1024
|
+
}
|
|
1025
|
+
return values.length === 0 ? undefined : values.length === 1 ? values[0] : null;
|
|
1026
|
+
}
|
|
1027
|
+
async function validateMcpRequestHeaders(request, reply) {
|
|
1028
|
+
const body = request.body;
|
|
1029
|
+
const id = isRecord(body) ? requestId(body.id) : null;
|
|
1030
|
+
if (!acceptsMcpResponses(request.headers.accept)) {
|
|
1031
|
+
await sendRpcError(reply, 406, -32000, "Not Acceptable: Client must accept both application/json and text/event-stream", id);
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (!isRecord(body) || typeof body.method !== "string")
|
|
1035
|
+
return true;
|
|
1036
|
+
const header = (name) => singleHeader(request.raw.rawHeaders, name);
|
|
1037
|
+
const rejectMismatch = async (name) => {
|
|
1038
|
+
await sendRpcError(reply, 400, -32020, `Missing or mismatched ${name} header`, id);
|
|
1039
|
+
return false;
|
|
1040
|
+
};
|
|
1041
|
+
const protocolVersion = header("mcp-protocol-version");
|
|
1042
|
+
if (!protocolVersion)
|
|
1043
|
+
return rejectMismatch("MCP-Protocol-Version");
|
|
1044
|
+
const params = isRecord(body.params) ? body.params : undefined;
|
|
1045
|
+
const meta = params && isRecord(params._meta) ? params._meta : undefined;
|
|
1046
|
+
const bodyVersion = meta?.["io.modelcontextprotocol/protocolVersion"];
|
|
1047
|
+
if (typeof bodyVersion !== "string" || protocolVersion !== bodyVersion) {
|
|
1048
|
+
return rejectMismatch("MCP-Protocol-Version");
|
|
1049
|
+
}
|
|
1050
|
+
if (protocolVersion !== PROTOCOL_VERSION) {
|
|
1051
|
+
await sendRpcError(reply, 400, ProtocolErrorCode.UnsupportedProtocolVersion, `Unsupported protocol version: ${protocolVersion}`, id, { requested: protocolVersion, supported: [PROTOCOL_VERSION] });
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
if (header("mcp-method") !== body.method)
|
|
1055
|
+
return rejectMismatch("Mcp-Method");
|
|
1056
|
+
const nameField = body.method === "tools/call" || body.method === "prompts/get"
|
|
1057
|
+
? "name"
|
|
1058
|
+
: body.method === "resources/read"
|
|
1059
|
+
? "uri"
|
|
1060
|
+
: undefined;
|
|
1061
|
+
if (!nameField)
|
|
1062
|
+
return true;
|
|
1063
|
+
if (!params || typeof params[nameField] !== "string")
|
|
1064
|
+
return rejectMismatch("Mcp-Name");
|
|
1065
|
+
const name = header("mcp-name");
|
|
1066
|
+
if (!name || decodeMcpHeaderValue(name) !== params[nameField])
|
|
1067
|
+
return rejectMismatch("Mcp-Name");
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
function acceptsMcpResponses(value) {
|
|
1071
|
+
const types = (Array.isArray(value) ? value : value ? [value] : [])
|
|
1072
|
+
.flatMap((header) => header.split(","))
|
|
1073
|
+
.filter((part) => !part.split(";").slice(1).some((parameter) => /^\s*q\s*=\s*0(?:\.0*)?\s*$/i.test(parameter)))
|
|
1074
|
+
.map((part) => part.split(";", 1)[0]?.trim().toLowerCase());
|
|
1075
|
+
return types.includes("application/json") && types.includes("text/event-stream");
|
|
1076
|
+
}
|
|
1077
|
+
function decodeMcpHeaderValue(value) {
|
|
1078
|
+
const normalized = value.replace(/^[\t ]+|[\t ]+$/g, "");
|
|
1079
|
+
const prefix = "=?base64?";
|
|
1080
|
+
if (!normalized.startsWith(prefix) || !normalized.endsWith("?=")) {
|
|
1081
|
+
return /^[\x20-\x7E\t]*$/.test(normalized) ? normalized : undefined;
|
|
1082
|
+
}
|
|
1083
|
+
const encoded = normalized.slice(prefix.length, -2);
|
|
1084
|
+
if (!encoded || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(encoded, "base64"));
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
return undefined;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function isRecord(value) {
|
|
1095
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1096
|
+
}
|
|
1097
|
+
function normalizeCacheHint(value, field) {
|
|
1098
|
+
if (!isRecord(value))
|
|
1099
|
+
throw new TypeError(`${field} cacheHint must be an object`);
|
|
1100
|
+
const unknownField = Object.keys(value).find((key) => key !== "ttlMs" && key !== "cacheScope");
|
|
1101
|
+
if (unknownField)
|
|
1102
|
+
throw new TypeError(`${field} cacheHint has an unknown field: ${unknownField}`);
|
|
1103
|
+
const { ttlMs, cacheScope } = value;
|
|
1104
|
+
if (ttlMs !== undefined && (!Number.isSafeInteger(ttlMs) || ttlMs < 0)) {
|
|
1105
|
+
throw new TypeError(`${field} cacheHint.ttlMs must be a non-negative safe integer`);
|
|
1106
|
+
}
|
|
1107
|
+
if (cacheScope !== undefined && cacheScope !== "public" && cacheScope !== "private") {
|
|
1108
|
+
throw new TypeError(`${field} cacheHint.cacheScope must be public or private`);
|
|
1109
|
+
}
|
|
1110
|
+
return Object.freeze({
|
|
1111
|
+
...(ttlMs === undefined ? {} : { ttlMs: ttlMs }),
|
|
1112
|
+
...(cacheScope === undefined ? {} : { cacheScope }),
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
function normalizeCacheHints(value, enabledMethods) {
|
|
1116
|
+
if (!isRecord(value))
|
|
1117
|
+
throw new TypeError("cacheHints must be an object");
|
|
1118
|
+
const allowed = new Set(CACHEABLE_METHODS);
|
|
1119
|
+
const normalized = {};
|
|
1120
|
+
for (const [method, hint] of Object.entries(value)) {
|
|
1121
|
+
if (!allowed.has(method))
|
|
1122
|
+
throw new TypeError(`cacheHints has an unknown method: ${method}`);
|
|
1123
|
+
if (hint === undefined)
|
|
1124
|
+
continue;
|
|
1125
|
+
if (!enabledMethods.has(method))
|
|
1126
|
+
throw new TypeError(`cacheHints cannot configure disabled method: ${method}`);
|
|
1127
|
+
normalized[method] = normalizeCacheHint(hint, `cacheHints.${method}`);
|
|
1128
|
+
}
|
|
1129
|
+
return Object.freeze(normalized);
|
|
1130
|
+
}
|
|
1131
|
+
function checkedInputResponses(responses) {
|
|
1132
|
+
if (!responses)
|
|
1133
|
+
return undefined;
|
|
1134
|
+
const checked = {};
|
|
1135
|
+
for (const [key, value] of Object.entries(responses)) {
|
|
1136
|
+
const elicitation = specTypeSchemas.ElicitResult["~standard"].validate(value);
|
|
1137
|
+
if ("value" in elicitation) {
|
|
1138
|
+
checked[key] = elicitation.value;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return checked;
|
|
1143
|
+
}
|
|
1144
|
+
function checkedProtocolValue(type, value) {
|
|
1145
|
+
let copy;
|
|
1146
|
+
try {
|
|
1147
|
+
assertJsonValue(value, new WeakSet(), true);
|
|
1148
|
+
const encoded = JSON.stringify(structuredClone(value));
|
|
1149
|
+
if (encoded === undefined)
|
|
1150
|
+
throw new TypeError("value is not JSON data");
|
|
1151
|
+
copy = JSON.parse(encoded);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
throw new TypeError(`${type} metadata must be JSON data`);
|
|
1155
|
+
}
|
|
1156
|
+
const schema = specTypeSchemas[type];
|
|
1157
|
+
const result = schema["~standard"].validate(copy);
|
|
1158
|
+
if (result instanceof Promise || "issues" in result) {
|
|
1159
|
+
throw new TypeError(`${type} metadata does not match the MCP schema`);
|
|
1160
|
+
}
|
|
1161
|
+
return deepFreeze(result.value);
|
|
1162
|
+
}
|
|
1163
|
+
function assertJsonValue(value, seen, allowUndefinedProperties = false) {
|
|
1164
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
1165
|
+
return;
|
|
1166
|
+
if (typeof value === "number") {
|
|
1167
|
+
if (!Number.isFinite(value))
|
|
1168
|
+
throw new TypeError("number must be finite");
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof value !== "object")
|
|
1172
|
+
throw new TypeError("value is not JSON data");
|
|
1173
|
+
if (seen.has(value))
|
|
1174
|
+
throw new TypeError("value is cyclic");
|
|
1175
|
+
seen.add(value);
|
|
1176
|
+
if (Array.isArray(value)) {
|
|
1177
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) {
|
|
1178
|
+
throw new TypeError("JSON arrays must contain only indexed values");
|
|
1179
|
+
}
|
|
1180
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1181
|
+
if (!(index in value))
|
|
1182
|
+
throw new TypeError("array must not be sparse");
|
|
1183
|
+
assertJsonValue(value[index], seen);
|
|
1184
|
+
}
|
|
1185
|
+
seen.delete(value);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1189
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1190
|
+
throw new TypeError("object must be a plain JSON object");
|
|
1191
|
+
}
|
|
1192
|
+
if (Reflect.ownKeys(value).length !== Object.keys(value).length) {
|
|
1193
|
+
throw new TypeError("JSON object keys must be enumerable strings");
|
|
1194
|
+
}
|
|
1195
|
+
for (const child of Object.values(value)) {
|
|
1196
|
+
if (child === undefined && allowUndefinedProperties)
|
|
1197
|
+
continue;
|
|
1198
|
+
assertJsonValue(child, seen);
|
|
1199
|
+
}
|
|
1200
|
+
seen.delete(value);
|
|
1201
|
+
}
|
|
1202
|
+
function deepFreeze(value) {
|
|
1203
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value))
|
|
1204
|
+
return value;
|
|
1205
|
+
for (const child of Object.values(value))
|
|
1206
|
+
deepFreeze(child);
|
|
1207
|
+
return Object.freeze(value);
|
|
1208
|
+
}
|
|
1209
|
+
function jsonMetadataSchema(schema, io) {
|
|
1210
|
+
return { ...z.toJSONSchema(schema, { target: "draft-2020-12", io }) };
|
|
1211
|
+
}
|
|
1212
|
+
function promptArguments(schema) {
|
|
1213
|
+
const jsonSchema = jsonMetadataSchema(schema, "input");
|
|
1214
|
+
const properties = isRecord(jsonSchema.properties) ? jsonSchema.properties : {};
|
|
1215
|
+
const required = new Set(Array.isArray(jsonSchema.required) ? jsonSchema.required : []);
|
|
1216
|
+
return Object.freeze(Object.entries(properties).map(([name, property]) => Object.freeze({
|
|
1217
|
+
name,
|
|
1218
|
+
...(isRecord(property) && typeof property.description === "string"
|
|
1219
|
+
? { description: property.description }
|
|
1220
|
+
: {}),
|
|
1221
|
+
required: required.has(name),
|
|
1222
|
+
})));
|
|
1223
|
+
}
|
|
1224
|
+
function sdkMetadataSchema(schema) {
|
|
1225
|
+
return {
|
|
1226
|
+
"~standard": {
|
|
1227
|
+
version: 1,
|
|
1228
|
+
vendor: "emseepea",
|
|
1229
|
+
validate: (value) => ({ value }),
|
|
1230
|
+
jsonSchema: {
|
|
1231
|
+
input: () => jsonMetadataSchema(schema, "input"),
|
|
1232
|
+
output: () => jsonMetadataSchema(schema, "output"),
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
const MCP_HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
1238
|
+
function assertValidMcpHeaderAnnotations(schema) {
|
|
1239
|
+
const jsonSchema = z.toJSONSchema(schema, { target: "draft-2020-12", io: "input" });
|
|
1240
|
+
const names = new Set();
|
|
1241
|
+
function visit(value, path, reachable) {
|
|
1242
|
+
if (!isRecord(value))
|
|
1243
|
+
return;
|
|
1244
|
+
if ("x-mcp-header" in value) {
|
|
1245
|
+
const name = value["x-mcp-header"];
|
|
1246
|
+
const location = path.join(".") || "the schema root";
|
|
1247
|
+
if (!reachable || path.length === 0) {
|
|
1248
|
+
throw new TypeError(`Tool x-mcp-header at ${location} is not reachable through properties`);
|
|
1249
|
+
}
|
|
1250
|
+
if (typeof name !== "string" || !MCP_HEADER_NAME.test(name)) {
|
|
1251
|
+
throw new TypeError(`Tool x-mcp-header at ${location} must be a non-empty HTTP header token`);
|
|
1252
|
+
}
|
|
1253
|
+
if (value.type !== "string" && value.type !== "integer" && value.type !== "boolean") {
|
|
1254
|
+
throw new TypeError(`Tool x-mcp-header at ${location} must describe a string, integer, or boolean`);
|
|
1255
|
+
}
|
|
1256
|
+
const normalized = name.toLowerCase();
|
|
1257
|
+
if (names.has(normalized)) {
|
|
1258
|
+
throw new TypeError(`Tool x-mcp-header name is duplicated: ${name}`);
|
|
1259
|
+
}
|
|
1260
|
+
names.add(normalized);
|
|
1261
|
+
}
|
|
1262
|
+
if (isRecord(value.properties)) {
|
|
1263
|
+
for (const [property, propertySchema] of Object.entries(value.properties)) {
|
|
1264
|
+
visit(propertySchema, [...path, property], reachable);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const key of ["prefixItems", "oneOf", "anyOf", "allOf"]) {
|
|
1268
|
+
const schemas = value[key];
|
|
1269
|
+
if (Array.isArray(schemas))
|
|
1270
|
+
for (const child of schemas)
|
|
1271
|
+
visit(child, path, false);
|
|
1272
|
+
}
|
|
1273
|
+
for (const key of [
|
|
1274
|
+
"items", "contains", "additionalProperties", "unevaluatedProperties",
|
|
1275
|
+
"unevaluatedItems", "propertyNames", "not", "if", "then", "else",
|
|
1276
|
+
]) {
|
|
1277
|
+
visit(value[key], path, false);
|
|
1278
|
+
}
|
|
1279
|
+
for (const key of ["patternProperties", "dependentSchemas", "$defs", "definitions"]) {
|
|
1280
|
+
const schemas = value[key];
|
|
1281
|
+
if (isRecord(schemas))
|
|
1282
|
+
for (const child of Object.values(schemas))
|
|
1283
|
+
visit(child, path, false);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
visit(jsonSchema, [], true);
|
|
1287
|
+
}
|
|
1288
|
+
function checkedCompletionHandlers(kind, complete, allowedNames) {
|
|
1289
|
+
if (complete === undefined)
|
|
1290
|
+
return new Map();
|
|
1291
|
+
if (!isRecord(complete))
|
|
1292
|
+
throw new TypeError(`${kind} completion map must be an object`);
|
|
1293
|
+
const allowed = new Set(allowedNames);
|
|
1294
|
+
const handlers = new Map();
|
|
1295
|
+
for (const [name, handler] of Object.entries(complete)) {
|
|
1296
|
+
if (!allowed.has(name))
|
|
1297
|
+
throw new TypeError(`${kind} completion key is not registered: ${name}`);
|
|
1298
|
+
if (typeof handler !== "function") {
|
|
1299
|
+
throw new TypeError(`${kind} completion handler must be a function: ${name}`);
|
|
1300
|
+
}
|
|
1301
|
+
handlers.set(name, handler);
|
|
1302
|
+
}
|
|
1303
|
+
return handlers;
|
|
1304
|
+
}
|
|
1305
|
+
function sdkPromptMetadataSchema(schema, completions, argumentNames, timeoutMs, maxApplicationResultBytes) {
|
|
1306
|
+
const metadataSchema = sdkMetadataSchema(schema);
|
|
1307
|
+
if (completions.size === 0)
|
|
1308
|
+
return metadataSchema;
|
|
1309
|
+
const shape = Object.fromEntries([...completions].map(([argument, complete]) => [
|
|
1310
|
+
argument,
|
|
1311
|
+
completable(z.string(), completionCallback(complete, argument, argumentNames, timeoutMs, maxApplicationResultBytes)),
|
|
1312
|
+
]));
|
|
1313
|
+
Object.defineProperty(metadataSchema, "shape", {
|
|
1314
|
+
value: Object.freeze(shape),
|
|
1315
|
+
enumerable: false,
|
|
1316
|
+
writable: false,
|
|
1317
|
+
configurable: false,
|
|
1318
|
+
});
|
|
1319
|
+
return metadataSchema;
|
|
1320
|
+
}
|
|
1321
|
+
function completionCallback(complete, currentName, allowedNames, timeoutMs, maxApplicationResultBytes) {
|
|
1322
|
+
const allowedArguments = new Set(allowedNames.filter((name) => name !== currentName));
|
|
1323
|
+
return async (value, context) => {
|
|
1324
|
+
try {
|
|
1325
|
+
const request = requestOperations.getStore();
|
|
1326
|
+
const deadlineMs = request?.deadlineMs ?? Date.now() + timeoutMs;
|
|
1327
|
+
const requestSignal = request?.signal ?? new AbortController().signal;
|
|
1328
|
+
return await runWithDeadline(requestSignal, deadlineMs, async (signal) => {
|
|
1329
|
+
signal.throwIfAborted();
|
|
1330
|
+
const values = await complete(value, {
|
|
1331
|
+
signal,
|
|
1332
|
+
deadlineMs,
|
|
1333
|
+
arguments: completionArguments(context?.arguments, allowedArguments),
|
|
1334
|
+
});
|
|
1335
|
+
signal.throwIfAborted();
|
|
1336
|
+
if (!Array.isArray(values)) {
|
|
1337
|
+
throw new Error("Completion returned an invalid result");
|
|
1338
|
+
}
|
|
1339
|
+
if (values.length > Math.floor((maxApplicationResultBytes - 1) / 3)) {
|
|
1340
|
+
throw new Error("Completion returned an invalid result");
|
|
1341
|
+
}
|
|
1342
|
+
const candidates = [];
|
|
1343
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1344
|
+
const descriptor = Object.getOwnPropertyDescriptor(values, String(index));
|
|
1345
|
+
if (!descriptor || !("value" in descriptor) || typeof descriptor.value !== "string") {
|
|
1346
|
+
throw new Error("Completion returned an invalid result");
|
|
1347
|
+
}
|
|
1348
|
+
candidates.push(descriptor.value);
|
|
1349
|
+
}
|
|
1350
|
+
assertResultSize(candidates, maxApplicationResultBytes, deadlineMs, signal);
|
|
1351
|
+
const publicResult = {
|
|
1352
|
+
completion: {
|
|
1353
|
+
values: candidates.slice(0, 100),
|
|
1354
|
+
total: candidates.length,
|
|
1355
|
+
hasMore: candidates.length > 100,
|
|
1356
|
+
},
|
|
1357
|
+
};
|
|
1358
|
+
assertResultSize(publicResult, maxApplicationResultBytes, deadlineMs, signal);
|
|
1359
|
+
return candidates;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
throw new Error("Completion failed");
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function completionArguments(candidate, allowed) {
|
|
1368
|
+
const result = Object.create(null);
|
|
1369
|
+
if (isRecord(candidate)) {
|
|
1370
|
+
for (const [name, value] of Object.entries(candidate)) {
|
|
1371
|
+
if (allowed.has(name) && typeof value === "string")
|
|
1372
|
+
result[name] = value;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return Object.freeze(result);
|
|
1376
|
+
}
|
|
1377
|
+
function requestId(value) {
|
|
1378
|
+
return typeof value === "string" || typeof value === "number" ? value : null;
|
|
1379
|
+
}
|
|
1380
|
+
async function runWithDeadline(requestSignal, deadlineMs, work) {
|
|
1381
|
+
const remainingMs = deadlineMs - Date.now();
|
|
1382
|
+
const deadlineSignal = remainingMs > 0 ? AbortSignal.timeout(remainingMs) : AbortSignal.abort();
|
|
1383
|
+
const signal = AbortSignal.any([requestSignal, deadlineSignal]);
|
|
1384
|
+
return new Promise((resolve, reject) => {
|
|
1385
|
+
const onAbort = () => reject(new Error("Tool execution cancelled"));
|
|
1386
|
+
if (signal.aborted)
|
|
1387
|
+
return onAbort();
|
|
1388
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1389
|
+
Promise.resolve().then(() => work(signal)).then(resolve, reject)
|
|
1390
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
function assertUniqueToolNames(tools) {
|
|
1394
|
+
const names = new Set();
|
|
1395
|
+
for (const tool of tools) {
|
|
1396
|
+
const name = tool[TOOL_NAME];
|
|
1397
|
+
if (names.has(name))
|
|
1398
|
+
throw new TypeError(`Duplicate tool name: ${name}`);
|
|
1399
|
+
names.add(name);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function assertRegistrationName(kind, name) {
|
|
1403
|
+
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(name)) {
|
|
1404
|
+
throw new TypeError(`${kind} name must contain 1 to 128 ASCII letters, digits, underscores, dots, or hyphens`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function assertUniqueResources(resources) {
|
|
1408
|
+
const names = new Set();
|
|
1409
|
+
const uris = new Set();
|
|
1410
|
+
const templateRoutes = [];
|
|
1411
|
+
for (const resource of resources) {
|
|
1412
|
+
const name = resource[RESOURCE_NAME];
|
|
1413
|
+
const uri = resource[RESOURCE_URI];
|
|
1414
|
+
if (names.has(name))
|
|
1415
|
+
throw new TypeError(`Duplicate resource name: ${name}`);
|
|
1416
|
+
if (uris.has(uri)) {
|
|
1417
|
+
const label = resource[RESOURCE_KIND] === "template" ? "template" : "URI";
|
|
1418
|
+
throw new TypeError(`Duplicate resource ${label}: ${uri}`);
|
|
1419
|
+
}
|
|
1420
|
+
const route = resource[RESOURCE_ROUTE];
|
|
1421
|
+
if (route && templateRoutes.some((candidate) => resourceTemplateRoutesOverlap(route, candidate))) {
|
|
1422
|
+
throw new TypeError(`Ambiguous resource template: ${uri}`);
|
|
1423
|
+
}
|
|
1424
|
+
names.add(name);
|
|
1425
|
+
uris.add(uri);
|
|
1426
|
+
if (route)
|
|
1427
|
+
templateRoutes.push(route);
|
|
1428
|
+
}
|
|
1429
|
+
for (const resource of resources) {
|
|
1430
|
+
if (resource[RESOURCE_KIND] !== "static")
|
|
1431
|
+
continue;
|
|
1432
|
+
for (const candidate of resources) {
|
|
1433
|
+
if (candidate[RESOURCE_MATCHES]?.(resource[RESOURCE_URI])) {
|
|
1434
|
+
throw new TypeError(`Ambiguous resource registration: ${resource[RESOURCE_URI]}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function assertUniquePromptNames(prompts) {
|
|
1440
|
+
const names = new Set();
|
|
1441
|
+
for (const prompt of prompts) {
|
|
1442
|
+
const name = prompt[PROMPT_NAME];
|
|
1443
|
+
if (names.has(name))
|
|
1444
|
+
throw new TypeError(`Duplicate prompt name: ${name}`);
|
|
1445
|
+
names.add(name);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function canonicalResourceUri(uri) {
|
|
1449
|
+
let parsed;
|
|
1450
|
+
try {
|
|
1451
|
+
parsed = new URL(uri);
|
|
1452
|
+
}
|
|
1453
|
+
catch {
|
|
1454
|
+
throw new TypeError("Resource URI must be an absolute canonical URI");
|
|
1455
|
+
}
|
|
1456
|
+
if (parsed.href !== uri)
|
|
1457
|
+
throw new TypeError("Resource URI must be an absolute canonical URI");
|
|
1458
|
+
return uri;
|
|
1459
|
+
}
|
|
1460
|
+
function checkedResourceTemplate(uriTemplate) {
|
|
1461
|
+
let template;
|
|
1462
|
+
try {
|
|
1463
|
+
if (/[{}]/.test(uriTemplate.replace(/\{[A-Za-z][A-Za-z0-9_]*\}/g, ""))) {
|
|
1464
|
+
throw new Error("unsupported expression");
|
|
1465
|
+
}
|
|
1466
|
+
const expressionNames = [...uriTemplate.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)]
|
|
1467
|
+
.map((match) => match[1]);
|
|
1468
|
+
if (new Set(expressionNames).size !== expressionNames.length) {
|
|
1469
|
+
throw new Error("repeated variable");
|
|
1470
|
+
}
|
|
1471
|
+
template = new ResourceTemplate(uriTemplate, { list: undefined });
|
|
1472
|
+
const variableNames = template.uriTemplate.variableNames;
|
|
1473
|
+
if (variableNames.length === 0)
|
|
1474
|
+
throw new Error("missing variable");
|
|
1475
|
+
const sentinels = variableNames.map((_, index) => {
|
|
1476
|
+
let sentinel = `emseepea-variable-${index}`;
|
|
1477
|
+
while (uriTemplate.includes(sentinel))
|
|
1478
|
+
sentinel = `_${sentinel}`;
|
|
1479
|
+
return sentinel;
|
|
1480
|
+
});
|
|
1481
|
+
const expanded = template.uriTemplate.expand(Object.fromEntries(variableNames.map((name, index) => [name, sentinels[index]])));
|
|
1482
|
+
canonicalResourceUri(expanded);
|
|
1483
|
+
const parsed = new URL(expanded);
|
|
1484
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
1485
|
+
throw new Error("unsupported URI component");
|
|
1486
|
+
}
|
|
1487
|
+
const segments = parsed.pathname.split("/").map((segment) => {
|
|
1488
|
+
const variableIndex = sentinels.indexOf(segment);
|
|
1489
|
+
if (variableIndex >= 0)
|
|
1490
|
+
return undefined;
|
|
1491
|
+
if (sentinels.some((sentinel) => segment.includes(sentinel))) {
|
|
1492
|
+
throw new Error("variable must occupy a path segment");
|
|
1493
|
+
}
|
|
1494
|
+
return segment;
|
|
1495
|
+
});
|
|
1496
|
+
if (segments.filter((segment) => segment === undefined).length !== variableNames.length) {
|
|
1497
|
+
throw new Error("variable must occupy a path segment");
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
template,
|
|
1501
|
+
route: { protocol: parsed.protocol, host: parsed.host, segments },
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
catch {
|
|
1505
|
+
throw new TypeError("Resource template must use a fixed scheme and authority with unique whole path-segment variables");
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function resourceTemplateRoutesOverlap(left, right) {
|
|
1509
|
+
return left.protocol === right.protocol && left.host === right.host &&
|
|
1510
|
+
left.segments.length === right.segments.length &&
|
|
1511
|
+
left.segments.every((segment, index) => segment === undefined || right.segments[index] === undefined || segment === right.segments[index]);
|
|
1512
|
+
}
|
|
1513
|
+
function assertNonEmpty(field, value) {
|
|
1514
|
+
if (!value.trim())
|
|
1515
|
+
throw new TypeError(`${field} must not be empty`);
|
|
1516
|
+
}
|
|
1517
|
+
function positiveInteger(field, value) {
|
|
1518
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
1519
|
+
throw new TypeError(`${field} must be a positive safe integer`);
|
|
1520
|
+
return value;
|
|
1521
|
+
}
|
|
1522
|
+
function callbackTimeout(callbackName, callback, timeoutName, value) {
|
|
1523
|
+
if (callback !== undefined && typeof callback !== "function")
|
|
1524
|
+
throw new TypeError(`${callbackName} must be a function`);
|
|
1525
|
+
if (value !== undefined && callback === undefined)
|
|
1526
|
+
throw new TypeError(`${timeoutName} requires ${callbackName}`);
|
|
1527
|
+
const timeout = positiveInteger(timeoutName, value === undefined ? 1_000 : value);
|
|
1528
|
+
if (timeout > 60_000)
|
|
1529
|
+
throw new TypeError(`${timeoutName} must not exceed 60000`);
|
|
1530
|
+
return timeout;
|
|
1531
|
+
}
|
|
1532
|
+
function nonNegativePort(value) {
|
|
1533
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) {
|
|
1534
|
+
throw new TypeError("port must be an integer from 0 to 65535");
|
|
1535
|
+
}
|
|
1536
|
+
return value;
|
|
1537
|
+
}
|
|
1538
|
+
function assertStatelessInputRequired(result) {
|
|
1539
|
+
if (!result.inputRequests || result.requestState !== undefined) {
|
|
1540
|
+
throw new Error("Client-input requests must be stateless");
|
|
1541
|
+
}
|
|
1542
|
+
if (Object.values(result.inputRequests).some((request) => request.method !== "elicitation/create")) {
|
|
1543
|
+
throw new Error("Client-input request kind is not supported");
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal) {
|
|
1547
|
+
let serialized;
|
|
1548
|
+
try {
|
|
1549
|
+
serialized = JSON.stringify(result);
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
throw new Error("Result cannot be serialized");
|
|
1553
|
+
}
|
|
1554
|
+
if (serialized === undefined || Buffer.byteLength(serialized) > maxApplicationResultBytes) {
|
|
1555
|
+
throw new Error("Result exceeds configured size limit");
|
|
1556
|
+
}
|
|
1557
|
+
signal.throwIfAborted();
|
|
1558
|
+
if (Date.now() >= deadlineMs)
|
|
1559
|
+
throw new Error("Result preparation exceeded its deadline");
|
|
1560
|
+
}
|
|
1561
|
+
function isLoopbackHost(host) {
|
|
1562
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1563
|
+
}
|
|
1564
|
+
async function closeApp(app, timeoutMs, flushTelemetry, flushTimeoutMs, finishTelemetry) {
|
|
1565
|
+
let timer;
|
|
1566
|
+
let onClosed;
|
|
1567
|
+
const httpClosed = new Promise((resolve) => { onClosed = resolve; });
|
|
1568
|
+
app.server.once("close", onClosed);
|
|
1569
|
+
const timeout = new Promise((resolve) => {
|
|
1570
|
+
timer = setTimeout(() => {
|
|
1571
|
+
app.server.close();
|
|
1572
|
+
app.server.closeAllConnections();
|
|
1573
|
+
resolve();
|
|
1574
|
+
}, timeoutMs);
|
|
1575
|
+
timer.unref();
|
|
1576
|
+
});
|
|
1577
|
+
try {
|
|
1578
|
+
await Promise.race([app.close(), timeout]);
|
|
1579
|
+
}
|
|
1580
|
+
finally {
|
|
1581
|
+
if (timer)
|
|
1582
|
+
clearTimeout(timer);
|
|
1583
|
+
if (flushTelemetry) {
|
|
1584
|
+
const controller = new AbortController();
|
|
1585
|
+
const flushDeadline = Date.now() + flushTimeoutMs;
|
|
1586
|
+
const expired = new Promise((resolve) => {
|
|
1587
|
+
// Keep the process alive long enough to finish flushing after sockets close.
|
|
1588
|
+
timer = setTimeout(() => { controller.abort(); resolve(); }, flushTimeoutMs);
|
|
1589
|
+
});
|
|
1590
|
+
try {
|
|
1591
|
+
await Promise.race([
|
|
1592
|
+
httpClosed.then(async () => {
|
|
1593
|
+
await finishTelemetry?.();
|
|
1594
|
+
if (!controller.signal.aborted && Date.now() < flushDeadline) {
|
|
1595
|
+
await flushTelemetry({ signal: controller.signal });
|
|
1596
|
+
}
|
|
1597
|
+
}).catch(() => { }),
|
|
1598
|
+
expired,
|
|
1599
|
+
]);
|
|
1600
|
+
}
|
|
1601
|
+
finally {
|
|
1602
|
+
if (timer)
|
|
1603
|
+
clearTimeout(timer);
|
|
1604
|
+
app.server.off("close", onClosed);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
app.server.off("close", onClosed);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
//# sourceMappingURL=index.js.map
|