@emseepea/server 0.0.2 → 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/index.d.ts +86 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +659 -47
- package/dist/index.js.map +1 -1
- 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/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
1
|
import { createMcpFastifyApp } from "@modelcontextprotocol/fastify";
|
|
2
2
|
import { GetPromptResultSchema, ReadResourceResultSchema, } from "@modelcontextprotocol/core";
|
|
3
3
|
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
4
|
-
import { McpServer, OAuthError, OAuthErrorCode, ResourceTemplate, bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, checkResourceAllowed, completable, createMcpHandler, getOAuthProtectedResourceMetadataUrl, oauthMetadataResponse, verifyBearerToken, } from "@modelcontextprotocol/server";
|
|
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
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { isIP } from "node:net";
|
|
7
8
|
import { z } from "zod";
|
|
9
|
+
import { installRequestTelemetry } from "./telemetry.js";
|
|
8
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
|
+
}
|
|
9
22
|
const PROTOCOL_VERSION = "2026-07-28";
|
|
10
23
|
const REGISTER = Symbol("register");
|
|
11
24
|
const TOOL_NAME = Symbol("toolName");
|
|
12
25
|
const TOOL_ACCESS = Symbol("toolAccess");
|
|
13
26
|
const TOOL_STREAMING = Symbol("toolStreaming");
|
|
27
|
+
const TOOL_LISTING = Symbol("toolListing");
|
|
14
28
|
const RESOURCE_NAME = Symbol("resourceName");
|
|
15
29
|
const RESOURCE_URI = Symbol("resourceUri");
|
|
16
30
|
const RESOURCE_KIND = Symbol("resourceKind");
|
|
17
31
|
const RESOURCE_MATCHES = Symbol("resourceMatches");
|
|
18
32
|
const RESOURCE_ROUTE = Symbol("resourceRoute");
|
|
33
|
+
const RESOURCE_LISTING = Symbol("resourceListing");
|
|
19
34
|
const PROMPT_NAME = Symbol("promptName");
|
|
20
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
|
+
];
|
|
21
45
|
const runtimes = new WeakMap();
|
|
22
46
|
const requestOperations = new AsyncLocalStorage();
|
|
23
47
|
export function defineTool(definition) {
|
|
24
48
|
const handler = definition.handler;
|
|
25
|
-
return createCheckedTool(definition, handler, false);
|
|
49
|
+
return createCheckedTool(definition, handler, false, true);
|
|
26
50
|
}
|
|
27
51
|
export function defineStreamingTool(definition) {
|
|
28
52
|
const handler = definition.handler;
|
|
29
|
-
return createCheckedTool(definition, handler, true);
|
|
53
|
+
return createCheckedTool(definition, handler, true, false);
|
|
30
54
|
}
|
|
31
55
|
export function defineMappedTool(definition) {
|
|
32
56
|
const { backendInputSchema, backendOutputSchema, isAvailable, adapter } = definition;
|
|
@@ -54,29 +78,61 @@ export function defineMappedTool(definition) {
|
|
|
54
78
|
context.signal.throwIfAborted();
|
|
55
79
|
return mapOutput(parsedBackendResult.data);
|
|
56
80
|
};
|
|
57
|
-
return createCheckedTool(definition, execute, false);
|
|
81
|
+
return createCheckedTool(definition, execute, false, false);
|
|
58
82
|
}
|
|
59
83
|
export function defineResource(definition) {
|
|
60
|
-
const { name,
|
|
84
|
+
const { name, handler } = definition;
|
|
61
85
|
assertRegistrationName("Resource", name);
|
|
62
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}`);
|
|
63
110
|
const registration = {
|
|
64
111
|
[RESOURCE_NAME]: name,
|
|
65
112
|
[RESOURCE_URI]: uri,
|
|
66
113
|
[RESOURCE_KIND]: "static",
|
|
114
|
+
[RESOURCE_LISTING]: Object.freeze({
|
|
115
|
+
method: "resources/list",
|
|
116
|
+
value: listing,
|
|
117
|
+
}),
|
|
67
118
|
[HAS_COMPLETION]: false,
|
|
68
119
|
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
69
|
-
server.registerResource(name, uri, {
|
|
70
|
-
title,
|
|
71
|
-
description,
|
|
72
|
-
mimeType,
|
|
73
|
-
}, async (_requestedUri, context) => {
|
|
120
|
+
server.registerResource(name, uri, cacheHint ? { ...metadata, cacheHint } : metadata, async (_requestedUri, context) => {
|
|
74
121
|
try {
|
|
75
122
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
76
123
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
77
124
|
signal.throwIfAborted();
|
|
78
|
-
const result = await handler({
|
|
125
|
+
const result = await handler({
|
|
126
|
+
signal,
|
|
127
|
+
deadlineMs,
|
|
128
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
129
|
+
});
|
|
79
130
|
signal.throwIfAborted();
|
|
131
|
+
if (isInputRequiredResult(result)) {
|
|
132
|
+
assertStatelessInputRequired(result);
|
|
133
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
80
136
|
const parsed = await ReadResourceResultSchema.safeParseAsync(result);
|
|
81
137
|
if (!parsed.success || parsed.data.contents.some((content) => content.uri !== uri)) {
|
|
82
138
|
throw new Error("Resource returned an invalid result");
|
|
@@ -95,10 +151,31 @@ export function defineResource(definition) {
|
|
|
95
151
|
return Object.freeze(registration);
|
|
96
152
|
}
|
|
97
153
|
export function defineResourceTemplate(definition) {
|
|
98
|
-
const { name,
|
|
154
|
+
const { name, handler } = definition;
|
|
99
155
|
assertRegistrationName("Resource template", name);
|
|
100
156
|
const { template, route } = checkedResourceTemplate(definition.uriTemplate);
|
|
101
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}`);
|
|
102
179
|
const variableNames = [...template.uriTemplate.variableNames];
|
|
103
180
|
const completions = checkedCompletionHandlers("Resource template", definition.complete, variableNames);
|
|
104
181
|
const registration = {
|
|
@@ -107,6 +184,10 @@ export function defineResourceTemplate(definition) {
|
|
|
107
184
|
[RESOURCE_KIND]: "template",
|
|
108
185
|
[RESOURCE_MATCHES]: (uri) => template.uriTemplate.match(uri) !== null,
|
|
109
186
|
[RESOURCE_ROUTE]: route,
|
|
187
|
+
[RESOURCE_LISTING]: Object.freeze({
|
|
188
|
+
method: "resources/templates/list",
|
|
189
|
+
value: listing,
|
|
190
|
+
}),
|
|
110
191
|
[HAS_COMPLETION]: completions.size > 0,
|
|
111
192
|
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
112
193
|
const registeredTemplate = completions.size === 0
|
|
@@ -118,13 +199,22 @@ export function defineResourceTemplate(definition) {
|
|
|
118
199
|
completionCallback(complete, variable, variableNames, timeoutMs, maxApplicationResultBytes),
|
|
119
200
|
])),
|
|
120
201
|
});
|
|
121
|
-
server.registerResource(name, registeredTemplate, {
|
|
202
|
+
server.registerResource(name, registeredTemplate, cacheHint ? { ...metadata, cacheHint } : metadata, async (requestedUri, variables, context) => {
|
|
122
203
|
try {
|
|
123
204
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
124
205
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
125
206
|
signal.throwIfAborted();
|
|
126
|
-
const result = await handler({ uri: requestedUri.href, variables }, {
|
|
207
|
+
const result = await handler({ uri: requestedUri.href, variables }, {
|
|
208
|
+
signal,
|
|
209
|
+
deadlineMs,
|
|
210
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
211
|
+
});
|
|
127
212
|
signal.throwIfAborted();
|
|
213
|
+
if (isInputRequiredResult(result)) {
|
|
214
|
+
assertStatelessInputRequired(result);
|
|
215
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
128
218
|
const parsed = await ReadResourceResultSchema.safeParseAsync(result);
|
|
129
219
|
if (!parsed.success ||
|
|
130
220
|
parsed.data.contents.some((content) => content.uri !== requestedUri.href)) {
|
|
@@ -144,17 +234,31 @@ export function defineResourceTemplate(definition) {
|
|
|
144
234
|
return Object.freeze(registration);
|
|
145
235
|
}
|
|
146
236
|
export function definePrompt(definition) {
|
|
147
|
-
const { name,
|
|
237
|
+
const { name, argsSchema, handler } = definition;
|
|
148
238
|
assertRegistrationName("Prompt", name);
|
|
149
239
|
const argumentNames = Object.keys(argsSchema.shape);
|
|
150
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
|
+
});
|
|
151
255
|
const registration = {
|
|
152
256
|
[PROMPT_NAME]: name,
|
|
153
257
|
[HAS_COMPLETION]: completions.size > 0,
|
|
258
|
+
[PROMPT_LISTING]: listing,
|
|
154
259
|
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
155
260
|
server.registerPrompt(name, {
|
|
156
|
-
|
|
157
|
-
description,
|
|
261
|
+
...metadata,
|
|
158
262
|
argsSchema: sdkPromptMetadataSchema(argsSchema, completions, argumentNames, timeoutMs, maxApplicationResultBytes),
|
|
159
263
|
}, async (args, context) => {
|
|
160
264
|
try {
|
|
@@ -164,8 +268,17 @@ export function definePrompt(definition) {
|
|
|
164
268
|
if (!parsedArgs.success)
|
|
165
269
|
throw new Error("Prompt received invalid arguments");
|
|
166
270
|
signal.throwIfAborted();
|
|
167
|
-
const result = await handler(parsedArgs.data, {
|
|
271
|
+
const result = await handler(parsedArgs.data, {
|
|
272
|
+
signal,
|
|
273
|
+
deadlineMs,
|
|
274
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
275
|
+
});
|
|
168
276
|
signal.throwIfAborted();
|
|
277
|
+
if (isInputRequiredResult(result)) {
|
|
278
|
+
assertStatelessInputRequired(result);
|
|
279
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
169
282
|
const parsedResult = await GetPromptResultSchema.safeParseAsync(result);
|
|
170
283
|
if (!parsedResult.success)
|
|
171
284
|
throw new Error("Prompt returned an invalid result");
|
|
@@ -187,26 +300,45 @@ const progressUpdateSchema = z.strictObject({
|
|
|
187
300
|
total: z.number().positive().optional(),
|
|
188
301
|
message: z.string().optional(),
|
|
189
302
|
});
|
|
190
|
-
function createCheckedTool(definition, execute, streaming) {
|
|
191
|
-
const { name,
|
|
303
|
+
function createCheckedTool(definition, execute, streaming, allowsInputRequired) {
|
|
304
|
+
const { name, inputSchema, outputSchema } = definition;
|
|
192
305
|
assertRegistrationName("Tool", name);
|
|
306
|
+
assertValidMcpHeaderAnnotations(inputSchema);
|
|
193
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
|
+
});
|
|
194
335
|
const registration = {
|
|
195
336
|
[TOOL_NAME]: name,
|
|
196
337
|
[TOOL_ACCESS]: access,
|
|
197
338
|
[TOOL_STREAMING]: streaming,
|
|
339
|
+
[TOOL_LISTING]: listing,
|
|
198
340
|
[REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes) {
|
|
199
|
-
server.registerTool(name, {
|
|
200
|
-
title,
|
|
201
|
-
description,
|
|
202
|
-
inputSchema: sdkMetadataSchema(inputSchema),
|
|
203
|
-
outputSchema: sdkMetadataSchema(outputSchema),
|
|
204
|
-
_meta: {
|
|
205
|
-
"io.emseepea/access": access === "public"
|
|
206
|
-
? { type: "public" }
|
|
207
|
-
: { type: "protected", requiredScopes: [...access.requiredScopes] },
|
|
208
|
-
},
|
|
209
|
-
}, async (input, context) => {
|
|
341
|
+
server.registerTool(name, metadata, async (input, context) => {
|
|
210
342
|
try {
|
|
211
343
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
212
344
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
@@ -224,6 +356,11 @@ function createCheckedTool(definition, execute, streaming) {
|
|
|
224
356
|
signal,
|
|
225
357
|
deadlineMs,
|
|
226
358
|
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
359
|
+
...(allowsInputRequired
|
|
360
|
+
? {
|
|
361
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
362
|
+
}
|
|
363
|
+
: {}),
|
|
227
364
|
...(reporter ? { reportProgress: reporter.report } : {}),
|
|
228
365
|
});
|
|
229
366
|
}
|
|
@@ -231,6 +368,12 @@ function createCheckedTool(definition, execute, streaming) {
|
|
|
231
368
|
await reporter?.finish();
|
|
232
369
|
}
|
|
233
370
|
reporter?.throwIfFailed();
|
|
371
|
+
if (allowsInputRequired && isInputRequiredResult(result)) {
|
|
372
|
+
signal.throwIfAborted();
|
|
373
|
+
assertStatelessInputRequired(result);
|
|
374
|
+
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
234
377
|
if (!isRecord(result) || typeof result.text !== "string" || !("data" in result)) {
|
|
235
378
|
throw new Error("Tool returned an invalid result");
|
|
236
379
|
}
|
|
@@ -311,15 +454,142 @@ function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
|
311
454
|
throw failure; },
|
|
312
455
|
};
|
|
313
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
|
+
}
|
|
314
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
|
+
}
|
|
315
574
|
assertNonEmpty("name", options.name);
|
|
316
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
|
+
});
|
|
317
584
|
const tools = Object.freeze([...(options.tools ?? [])]);
|
|
318
585
|
const resources = Object.freeze([...(options.resources ?? [])]);
|
|
319
586
|
const prompts = Object.freeze([...(options.prompts ?? [])]);
|
|
320
587
|
assertUniqueToolNames(tools);
|
|
321
588
|
assertUniqueResources(resources);
|
|
322
589
|
assertUniquePromptNames(prompts);
|
|
590
|
+
const paginationOptions = options.listPagination
|
|
591
|
+
? normalizeListPagination(options.listPagination)
|
|
592
|
+
: undefined;
|
|
323
593
|
const maxRequestBytes = positiveInteger("maxRequestBytes", options.maxRequestBytes ?? 1024 * 1024);
|
|
324
594
|
const maxApplicationResultBytes = positiveInteger("maxApplicationResultBytes", options.maxApplicationResultBytes ?? 1024 * 1024);
|
|
325
595
|
const maxProgressEvents = positiveInteger("maxProgressEvents", options.maxProgressEvents ?? 32);
|
|
@@ -328,8 +598,9 @@ export function createEmseepea(options) {
|
|
|
328
598
|
const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
|
|
329
599
|
const oauth = options.oauth ? normalizeOAuth(options.oauth) : undefined;
|
|
330
600
|
const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
|
|
331
|
-
if (
|
|
332
|
-
|
|
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");
|
|
333
604
|
}
|
|
334
605
|
if (tools.some((tool) => tool[TOOL_ACCESS] !== "public") && !oauth) {
|
|
335
606
|
throw new TypeError("Protected tools require OAuth resource-server configuration");
|
|
@@ -349,8 +620,23 @@ export function createEmseepea(options) {
|
|
|
349
620
|
prompts.some((prompt) => prompt[HAS_COMPLETION]);
|
|
350
621
|
if (hasCompletion)
|
|
351
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;
|
|
352
638
|
const sdkHandler = createMcpHandler(() => {
|
|
353
|
-
const server = new McpServer(
|
|
639
|
+
const server = new McpServer(serverInfo, {
|
|
354
640
|
capabilities: {
|
|
355
641
|
...(tools.length ? { tools: { listChanged: false } } : {}),
|
|
356
642
|
...(resources.length ? { resources: { subscribe: false, listChanged: false } } : {}),
|
|
@@ -358,6 +644,7 @@ export function createEmseepea(options) {
|
|
|
358
644
|
...(hasCompletion ? { completions: {} } : {}),
|
|
359
645
|
},
|
|
360
646
|
instructions: options.instructions,
|
|
647
|
+
cacheHints,
|
|
361
648
|
supportedProtocolVersions: [PROTOCOL_VERSION],
|
|
362
649
|
});
|
|
363
650
|
for (const tool of tools) {
|
|
@@ -367,6 +654,8 @@ export function createEmseepea(options) {
|
|
|
367
654
|
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
368
655
|
for (const prompt of prompts)
|
|
369
656
|
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
657
|
+
if (pagination)
|
|
658
|
+
installListPagination(server, pagination);
|
|
370
659
|
return server;
|
|
371
660
|
}, {
|
|
372
661
|
legacy: "reject",
|
|
@@ -375,11 +664,17 @@ export function createEmseepea(options) {
|
|
|
375
664
|
});
|
|
376
665
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
377
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;
|
|
378
668
|
const limiter = deployment.mode === "production-behind-proxy"
|
|
379
669
|
? new FixedWindowRateLimiter(deployment.rateLimit)
|
|
380
670
|
: undefined;
|
|
381
671
|
app.get("/healthz", health("ok\n"));
|
|
382
|
-
app.get("/readyz",
|
|
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(); });
|
|
383
678
|
if (oauth) {
|
|
384
679
|
app.all("/.well-known/*", async (request, reply) => {
|
|
385
680
|
const response = oauthMetadataResponse(new Request(new URL(request.url, oauth.metadata.resourceServerUrl.origin), {
|
|
@@ -401,6 +696,8 @@ export function createEmseepea(options) {
|
|
|
401
696
|
if (deployment.mode === "production-behind-proxy" &&
|
|
402
697
|
!validateProductionRequest(request, reply, deployment, limiter))
|
|
403
698
|
return;
|
|
699
|
+
if (!await validateMcpRequestHeaders(request, reply))
|
|
700
|
+
return;
|
|
404
701
|
if (isRecord(request.body) && typeof request.body.method === "string" &&
|
|
405
702
|
!enabledMethods.has(request.body.method)) {
|
|
406
703
|
await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
|
|
@@ -424,7 +721,7 @@ export function createEmseepea(options) {
|
|
|
424
721
|
const abort = () => disconnected.abort();
|
|
425
722
|
request.raw.once("aborted", abort);
|
|
426
723
|
reply.raw.once("close", abort);
|
|
427
|
-
if (request
|
|
724
|
+
if (requestAlreadyClosed(request, reply))
|
|
428
725
|
abort();
|
|
429
726
|
reply.hijack();
|
|
430
727
|
try {
|
|
@@ -447,7 +744,7 @@ export function createEmseepea(options) {
|
|
|
447
744
|
}
|
|
448
745
|
});
|
|
449
746
|
app.addHook("onClose", async () => sdkHandler.close());
|
|
450
|
-
runtimes.set(app, { deployment, requestTimeoutMs: operationTimeoutMs + 5_000 });
|
|
747
|
+
runtimes.set(app, { deployment, requestTimeoutMs: operationTimeoutMs + 5_000, stopping, finishTelemetry });
|
|
451
748
|
return app;
|
|
452
749
|
}
|
|
453
750
|
function safeOAuthError(error) {
|
|
@@ -467,6 +764,8 @@ async function verifyProtectedCall(request, reply, access, oauth) {
|
|
|
467
764
|
const abort = () => disconnected.abort();
|
|
468
765
|
request.raw.once("aborted", abort);
|
|
469
766
|
reply.raw.once("close", abort);
|
|
767
|
+
if (requestAlreadyClosed(request, reply))
|
|
768
|
+
abort();
|
|
470
769
|
try {
|
|
471
770
|
const authInfo = await runWithDeadline(disconnected.signal, Date.now() + oauth.verificationTimeoutMs, () => verifyBearerToken(singleHeader(request.raw.rawHeaders, "authorization"), {
|
|
472
771
|
verifier: oauth.verifier,
|
|
@@ -486,6 +785,9 @@ async function verifyProtectedCall(request, reply, access, oauth) {
|
|
|
486
785
|
reply.raw.off("close", abort);
|
|
487
786
|
}
|
|
488
787
|
}
|
|
788
|
+
function requestAlreadyClosed(request, reply) {
|
|
789
|
+
return request.raw.aborted || (request.raw.destroyed && !request.raw.complete) || reply.raw.destroyed;
|
|
790
|
+
}
|
|
489
791
|
async function sendWebResponse(reply, response) {
|
|
490
792
|
for (const [name, value] of response.headers)
|
|
491
793
|
reply.header(name, value);
|
|
@@ -518,6 +820,8 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
518
820
|
}
|
|
519
821
|
const port = nonNegativePort(options.port ?? 3000);
|
|
520
822
|
const shutdownTimeoutMs = positiveInteger("shutdownTimeoutMs", options.shutdownTimeoutMs ?? 5_000);
|
|
823
|
+
const flushTelemetry = options.flushTelemetry;
|
|
824
|
+
const telemetryFlushTimeoutMs = callbackTimeout("flushTelemetry", flushTelemetry, "telemetryFlushTimeoutMs", options.telemetryFlushTimeoutMs);
|
|
521
825
|
await app.listen({ host, port });
|
|
522
826
|
app.server.headersTimeout = 10_000;
|
|
523
827
|
app.server.keepAliveTimeout = 5_000;
|
|
@@ -533,7 +837,10 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
533
837
|
let closing;
|
|
534
838
|
return {
|
|
535
839
|
url: new URL(`http://${urlHost}:${address.port}/mcp`),
|
|
536
|
-
close: () =>
|
|
840
|
+
close: () => {
|
|
841
|
+
runtime.stopping.abort();
|
|
842
|
+
return closing ??= closeApp(app, shutdownTimeoutMs, flushTelemetry, telemetryFlushTimeoutMs, runtime.finishTelemetry);
|
|
843
|
+
},
|
|
537
844
|
};
|
|
538
845
|
}
|
|
539
846
|
function health(body) {
|
|
@@ -541,8 +848,12 @@ function health(body) {
|
|
|
541
848
|
await reply.header("cache-control", "no-store").type("text/plain; charset=utf-8").send(body);
|
|
542
849
|
};
|
|
543
850
|
}
|
|
544
|
-
async function sendRpcError(reply, status, code, message, id = null) {
|
|
545
|
-
await reply.code(status).send({
|
|
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
|
+
});
|
|
546
857
|
}
|
|
547
858
|
function isFastifyError(error) {
|
|
548
859
|
return error instanceof Error && "code" in error;
|
|
@@ -713,9 +1024,203 @@ function singleHeader(rawHeaders, name) {
|
|
|
713
1024
|
}
|
|
714
1025
|
return values.length === 0 ? undefined : values.length === 1 ? values[0] : null;
|
|
715
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
|
+
}
|
|
716
1094
|
function isRecord(value) {
|
|
717
1095
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
718
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
|
+
}
|
|
719
1224
|
function sdkMetadataSchema(schema) {
|
|
720
1225
|
return {
|
|
721
1226
|
"~standard": {
|
|
@@ -723,12 +1228,63 @@ function sdkMetadataSchema(schema) {
|
|
|
723
1228
|
vendor: "emseepea",
|
|
724
1229
|
validate: (value) => ({ value }),
|
|
725
1230
|
jsonSchema: {
|
|
726
|
-
input: () =>
|
|
727
|
-
output: () =>
|
|
1231
|
+
input: () => jsonMetadataSchema(schema, "input"),
|
|
1232
|
+
output: () => jsonMetadataSchema(schema, "output"),
|
|
728
1233
|
},
|
|
729
1234
|
},
|
|
730
1235
|
};
|
|
731
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
|
+
}
|
|
732
1288
|
function checkedCompletionHandlers(kind, complete, allowedNames) {
|
|
733
1289
|
if (complete === undefined)
|
|
734
1290
|
return new Map();
|
|
@@ -963,12 +1519,30 @@ function positiveInteger(field, value) {
|
|
|
963
1519
|
throw new TypeError(`${field} must be a positive safe integer`);
|
|
964
1520
|
return value;
|
|
965
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
|
+
}
|
|
966
1532
|
function nonNegativePort(value) {
|
|
967
1533
|
if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) {
|
|
968
1534
|
throw new TypeError("port must be an integer from 0 to 65535");
|
|
969
1535
|
}
|
|
970
1536
|
return value;
|
|
971
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
|
+
}
|
|
972
1546
|
function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal) {
|
|
973
1547
|
let serialized;
|
|
974
1548
|
try {
|
|
@@ -987,14 +1561,52 @@ function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal)
|
|
|
987
1561
|
function isLoopbackHost(host) {
|
|
988
1562
|
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
989
1563
|
}
|
|
990
|
-
async function closeApp(app, timeoutMs) {
|
|
1564
|
+
async function closeApp(app, timeoutMs, flushTelemetry, flushTimeoutMs, finishTelemetry) {
|
|
991
1565
|
let timer;
|
|
1566
|
+
let onClosed;
|
|
1567
|
+
const httpClosed = new Promise((resolve) => { onClosed = resolve; });
|
|
1568
|
+
app.server.once("close", onClosed);
|
|
992
1569
|
const timeout = new Promise((resolve) => {
|
|
993
|
-
timer = setTimeout(() => {
|
|
1570
|
+
timer = setTimeout(() => {
|
|
1571
|
+
app.server.close();
|
|
1572
|
+
app.server.closeAllConnections();
|
|
1573
|
+
resolve();
|
|
1574
|
+
}, timeoutMs);
|
|
994
1575
|
timer.unref();
|
|
995
1576
|
});
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
+
}
|
|
999
1611
|
}
|
|
1000
1612
|
//# sourceMappingURL=index.js.map
|