@emseepea/server 0.6.1 → 0.8.1
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 +85 -10
- package/dist/index.d.ts +42 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +292 -56
- package/dist/index.js.map +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, 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";
|
|
4
|
+
import { McpServer, InMemoryServerEventBus, OAuthError, OAuthErrorCode, ProtocolError, ProtocolErrorCode, ResourceTemplate, bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, checkResourceAllowed, classifyInboundRequest, completable, createMcpHandler, createRequestStateCodec, 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
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { readdir, realpath } from "node:fs/promises";
|
|
@@ -12,7 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import { installObservability, } from "./telemetry.js";
|
|
13
13
|
export { openTelemetry, structuredLogging, } from "./telemetry.js";
|
|
14
14
|
export * from "./ui.js";
|
|
15
|
-
export const inputRequired = Object.assign((spec) => sdkInputRequired(
|
|
15
|
+
export const inputRequired = Object.assign((spec) => sdkInputRequired(spec), {
|
|
16
16
|
elicit: sdkInputRequired.elicit,
|
|
17
17
|
elicitUrl: sdkInputRequired.elicitUrl,
|
|
18
18
|
});
|
|
@@ -24,6 +24,14 @@ export function inputResponse(responses, key) {
|
|
|
24
24
|
return response.kind === "elicit" ? response : { kind: "missing" };
|
|
25
25
|
}
|
|
26
26
|
const PROTOCOL_VERSION = "2026-07-28";
|
|
27
|
+
const LEGACY_PROTOCOL_VERSIONS = Object.freeze([
|
|
28
|
+
"2025-11-25",
|
|
29
|
+
"2025-06-18",
|
|
30
|
+
"2025-03-26",
|
|
31
|
+
"2024-11-05",
|
|
32
|
+
"2024-10-07",
|
|
33
|
+
]);
|
|
34
|
+
const SUPPORTED_PROTOCOLS = Object.freeze([PROTOCOL_VERSION, ...LEGACY_PROTOCOL_VERSIONS]);
|
|
27
35
|
const REGISTER = Symbol("register");
|
|
28
36
|
const DISCOVERABLE = Symbol("discoverable");
|
|
29
37
|
const TOOL_NAME = Symbol("toolName");
|
|
@@ -51,6 +59,45 @@ const CACHEABLE_METHODS = [
|
|
|
51
59
|
];
|
|
52
60
|
const runtimes = new WeakMap();
|
|
53
61
|
const requestOperations = new AsyncLocalStorage();
|
|
62
|
+
function createRequestStateRuntime(options) {
|
|
63
|
+
if (typeof options.key !== "string" && !(options.key instanceof Uint8Array)) {
|
|
64
|
+
throw new TypeError("requestState.key must be a string or Uint8Array");
|
|
65
|
+
}
|
|
66
|
+
const ttlSeconds = positiveInteger("requestState.ttlSeconds", options.ttlSeconds);
|
|
67
|
+
const maxBytes = positiveInteger("requestState.maxBytes", options.maxBytes ?? 4 * 1024);
|
|
68
|
+
const codec = createRequestStateCodec({
|
|
69
|
+
key: typeof options.key === "string" ? options.key : Uint8Array.from(options.key),
|
|
70
|
+
ttlSeconds,
|
|
71
|
+
bind(context) {
|
|
72
|
+
const operation = requestOperations.getStore();
|
|
73
|
+
if (!operation?.capability)
|
|
74
|
+
throw new Error("request state has no capability binding");
|
|
75
|
+
const principal = operation.principal;
|
|
76
|
+
return JSON.stringify([
|
|
77
|
+
context.mcpReq.method,
|
|
78
|
+
operation.capability,
|
|
79
|
+
principal
|
|
80
|
+
? [principal.clientId, [...principal.permissions].sort(), principal.resource ?? null]
|
|
81
|
+
: null,
|
|
82
|
+
]);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const assertWireSize = (state) => {
|
|
86
|
+
if (Buffer.byteLength(state, "utf8") > maxBytes)
|
|
87
|
+
throw new Error("request state is oversized");
|
|
88
|
+
};
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
async mint(payload, context) {
|
|
91
|
+
const state = await codec.mint(payload, context);
|
|
92
|
+
assertWireSize(state);
|
|
93
|
+
return state;
|
|
94
|
+
},
|
|
95
|
+
async verify(state, context) {
|
|
96
|
+
assertWireSize(state);
|
|
97
|
+
return codec.verify(state, context);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
54
101
|
export function defineTool(definition) {
|
|
55
102
|
const handler = definition.handler;
|
|
56
103
|
return createCheckedTool(definition, handler, false, true);
|
|
@@ -126,21 +173,16 @@ export function defineResource(definition) {
|
|
|
126
173
|
value: listing,
|
|
127
174
|
}),
|
|
128
175
|
[HAS_COMPLETION]: false,
|
|
129
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
176
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState) {
|
|
130
177
|
server.registerResource(name, uri, cacheHint ? { ...metadata, cacheHint } : metadata, async (_requestedUri, context) => {
|
|
131
178
|
try {
|
|
132
179
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
133
180
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
134
181
|
signal.throwIfAborted();
|
|
135
|
-
const result = await handler(
|
|
136
|
-
signal,
|
|
137
|
-
deadlineMs,
|
|
138
|
-
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
139
|
-
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
140
|
-
});
|
|
182
|
+
const result = await handler(directHandlerContext(access, context, signal, deadlineMs, requestState));
|
|
141
183
|
signal.throwIfAborted();
|
|
142
184
|
if (isInputRequiredResult(result)) {
|
|
143
|
-
|
|
185
|
+
await assertInputRequired(result, requestState, context);
|
|
144
186
|
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
145
187
|
return result;
|
|
146
188
|
}
|
|
@@ -203,7 +245,7 @@ export function defineResourceTemplate(definition) {
|
|
|
203
245
|
value: listing,
|
|
204
246
|
}),
|
|
205
247
|
[HAS_COMPLETION]: completions.size > 0,
|
|
206
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
248
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState) {
|
|
207
249
|
const registeredTemplate = completions.size === 0
|
|
208
250
|
? template
|
|
209
251
|
: new ResourceTemplate(uriTemplate, {
|
|
@@ -218,15 +260,10 @@ export function defineResourceTemplate(definition) {
|
|
|
218
260
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
219
261
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
220
262
|
signal.throwIfAborted();
|
|
221
|
-
const result = await handler({ uri: requestedUri.href, variables },
|
|
222
|
-
signal,
|
|
223
|
-
deadlineMs,
|
|
224
|
-
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
225
|
-
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
226
|
-
});
|
|
263
|
+
const result = await handler({ uri: requestedUri.href, variables }, directHandlerContext(access, context, signal, deadlineMs, requestState));
|
|
227
264
|
signal.throwIfAborted();
|
|
228
265
|
if (isInputRequiredResult(result)) {
|
|
229
|
-
|
|
266
|
+
await assertInputRequired(result, requestState, context);
|
|
230
267
|
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
231
268
|
return result;
|
|
232
269
|
}
|
|
@@ -274,7 +311,7 @@ export function definePrompt(definition) {
|
|
|
274
311
|
[PROMPT_ACCESS]: access,
|
|
275
312
|
[HAS_COMPLETION]: completions.size > 0,
|
|
276
313
|
[PROMPT_LISTING]: listing,
|
|
277
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
314
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState) {
|
|
278
315
|
server.registerPrompt(name, {
|
|
279
316
|
...metadata,
|
|
280
317
|
argsSchema: sdkPromptMetadataSchema(argsSchema, completions, argumentNames, timeoutMs, maxApplicationResultBytes),
|
|
@@ -286,15 +323,10 @@ export function definePrompt(definition) {
|
|
|
286
323
|
if (!parsedArgs.success)
|
|
287
324
|
throw new Error("Prompt received invalid arguments");
|
|
288
325
|
signal.throwIfAborted();
|
|
289
|
-
const result = await handler(parsedArgs.data,
|
|
290
|
-
signal,
|
|
291
|
-
deadlineMs,
|
|
292
|
-
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
293
|
-
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
294
|
-
});
|
|
326
|
+
const result = await handler(parsedArgs.data, directHandlerContext(access, context, signal, deadlineMs, requestState));
|
|
295
327
|
signal.throwIfAborted();
|
|
296
328
|
if (isInputRequiredResult(result)) {
|
|
297
|
-
|
|
329
|
+
await assertInputRequired(result, requestState, context);
|
|
298
330
|
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
299
331
|
return result;
|
|
300
332
|
}
|
|
@@ -464,7 +496,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
464
496
|
[TOOL_ACCESS]: access,
|
|
465
497
|
[TOOL_STREAMING]: streaming,
|
|
466
498
|
[TOOL_LISTING]: listing,
|
|
467
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes) {
|
|
499
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes, requestState) {
|
|
468
500
|
server.registerTool(name, metadata, async (input, context) => {
|
|
469
501
|
try {
|
|
470
502
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
@@ -480,14 +512,15 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
480
512
|
let result;
|
|
481
513
|
try {
|
|
482
514
|
result = await execute(parsedInput.data, {
|
|
483
|
-
signal,
|
|
484
|
-
deadlineMs,
|
|
485
|
-
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
486
515
|
...(allowsInputRequired
|
|
487
|
-
?
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
516
|
+
? directHandlerContext(access, context, signal, deadlineMs, requestState)
|
|
517
|
+
: {
|
|
518
|
+
signal,
|
|
519
|
+
deadlineMs,
|
|
520
|
+
principal: access === "public"
|
|
521
|
+
? undefined
|
|
522
|
+
: principalFrom(context.http?.authInfo),
|
|
523
|
+
}),
|
|
491
524
|
...(reporter ? { reportProgress: reporter.report } : {}),
|
|
492
525
|
});
|
|
493
526
|
}
|
|
@@ -497,7 +530,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
497
530
|
reporter?.throwIfFailed();
|
|
498
531
|
if (allowsInputRequired && isInputRequiredResult(result)) {
|
|
499
532
|
signal.throwIfAborted();
|
|
500
|
-
|
|
533
|
+
await assertInputRequired(result, requestState, context);
|
|
501
534
|
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
502
535
|
return result;
|
|
503
536
|
}
|
|
@@ -745,10 +778,20 @@ export function createEmseepea(options) {
|
|
|
745
778
|
? normalizeListPagination(options.listPagination)
|
|
746
779
|
: undefined;
|
|
747
780
|
const maxRequestBytes = positiveInteger("maxRequestBytes", options.maxRequestBytes ?? 1024 * 1024);
|
|
781
|
+
const requestState = options.requestState
|
|
782
|
+
? createRequestStateRuntime(options.requestState)
|
|
783
|
+
: undefined;
|
|
748
784
|
const maxApplicationResultBytes = positiveInteger("maxApplicationResultBytes", options.maxApplicationResultBytes ?? 1024 * 1024);
|
|
749
785
|
const maxProgressEvents = positiveInteger("maxProgressEvents", options.maxProgressEvents ?? 32);
|
|
750
786
|
const maxProgressEventBytes = positiveInteger("maxProgressEventBytes", options.maxProgressEventBytes ?? 8 * 1024);
|
|
751
787
|
const operationTimeoutMs = positiveInteger("operationTimeoutMs", options.operationTimeoutMs ?? 30_000);
|
|
788
|
+
const resourceSubscriptions = options.resourceSubscriptions && Object.freeze({
|
|
789
|
+
maxActive: positiveInteger("resourceSubscriptions.maxActive", options.resourceSubscriptions.maxActive ?? 16),
|
|
790
|
+
maxEvents: positiveInteger("resourceSubscriptions.maxEvents", options.resourceSubscriptions.maxEvents ?? 256),
|
|
791
|
+
maxEventBytes: positiveInteger("resourceSubscriptions.maxEventBytes", options.resourceSubscriptions.maxEventBytes ?? 8 * 1024),
|
|
792
|
+
maxUriBytes: positiveInteger("resourceSubscriptions.maxUriBytes", options.resourceSubscriptions.maxUriBytes ?? 512),
|
|
793
|
+
lifetimeMs: positiveInteger("resourceSubscriptions.lifetimeMs", options.resourceSubscriptions.lifetimeMs ?? 30_000),
|
|
794
|
+
});
|
|
752
795
|
const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
|
|
753
796
|
const authentication = options.authentication
|
|
754
797
|
? normalizeAuthentication(options.authentication)
|
|
@@ -775,6 +818,8 @@ export function createEmseepea(options) {
|
|
|
775
818
|
if (resources.some((resource) => resource[RESOURCE_KIND] === "template")) {
|
|
776
819
|
enabledMethods.add("resources/templates/list");
|
|
777
820
|
}
|
|
821
|
+
if (resourceSubscriptions && resources.length)
|
|
822
|
+
enabledMethods.add("subscriptions/listen");
|
|
778
823
|
if (prompts.length)
|
|
779
824
|
enabledMethods.add("prompts/list").add("prompts/get");
|
|
780
825
|
const hasCompletion = resources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
@@ -792,6 +837,8 @@ export function createEmseepea(options) {
|
|
|
792
837
|
: hasSuppressedCapabilities
|
|
793
838
|
? compileUnpaginatedCatalogue(baseCatalogues)
|
|
794
839
|
: undefined;
|
|
840
|
+
const resourceEvents = resourceSubscriptions ? new InMemoryServerEventBus() : undefined;
|
|
841
|
+
const activeSubscriptions = new Set();
|
|
795
842
|
const sdkHandler = createMcpHandler(() => {
|
|
796
843
|
const request = requestOperations.getStore();
|
|
797
844
|
const activeTools = request?.filterCatalogues
|
|
@@ -817,21 +864,29 @@ export function createEmseepea(options) {
|
|
|
817
864
|
const server = new McpServer(serverInfo, {
|
|
818
865
|
capabilities: {
|
|
819
866
|
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
820
|
-
...(activeResources.length ? {
|
|
867
|
+
...(activeResources.length ? {
|
|
868
|
+
resources: {
|
|
869
|
+
subscribe: Boolean(resourceSubscriptions) && !request?.legacy,
|
|
870
|
+
listChanged: false,
|
|
871
|
+
},
|
|
872
|
+
} : {}),
|
|
821
873
|
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
822
874
|
...(activeHasCompletion ? { completions: {} } : {}),
|
|
823
875
|
},
|
|
824
876
|
instructions: options.instructions,
|
|
825
|
-
cacheHints: request?.filterCatalogues ? undefined : cacheHints,
|
|
826
|
-
supportedProtocolVersions: [
|
|
877
|
+
cacheHints: request?.filterCatalogues || request?.legacy ? undefined : cacheHints,
|
|
878
|
+
supportedProtocolVersions: [...SUPPORTED_PROTOCOLS],
|
|
879
|
+
...(requestState && !request?.legacy ? { requestState: { verify: requestState.verify } } : {}),
|
|
827
880
|
});
|
|
828
881
|
for (const tool of activeTools) {
|
|
829
|
-
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes);
|
|
882
|
+
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes, requestState);
|
|
883
|
+
}
|
|
884
|
+
for (const resource of activeResources) {
|
|
885
|
+
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, requestState);
|
|
886
|
+
}
|
|
887
|
+
for (const prompt of activePrompts) {
|
|
888
|
+
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, requestState);
|
|
830
889
|
}
|
|
831
|
-
for (const resource of activeResources)
|
|
832
|
-
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
833
|
-
for (const prompt of activePrompts)
|
|
834
|
-
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
835
890
|
const filteredCatalogues = request?.filterCatalogues
|
|
836
891
|
? catalogueListings(activeDiscoverableTools, activeDiscoverableResources, activeDiscoverablePrompts, activeTools, activeResources, activePrompts)
|
|
837
892
|
: undefined;
|
|
@@ -848,12 +903,85 @@ export function createEmseepea(options) {
|
|
|
848
903
|
installListPagination(server, pagination);
|
|
849
904
|
return server;
|
|
850
905
|
}, {
|
|
851
|
-
|
|
852
|
-
responseMode: hasStreaming ? "auto" : "json",
|
|
906
|
+
responseMode: hasStreaming || resourceSubscriptions ? "auto" : "json",
|
|
853
907
|
keepAliveMs: 0,
|
|
854
908
|
});
|
|
855
909
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
856
910
|
const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
|
|
911
|
+
async function serveResourceSubscription(request, reply, uri) {
|
|
912
|
+
if (!resourceSubscriptions || !resourceEvents)
|
|
913
|
+
return;
|
|
914
|
+
if (activeSubscriptions.size >= resourceSubscriptions.maxActive) {
|
|
915
|
+
await sendRpcError(reply, 503, -32603, "Subscription capacity is exhausted", requestId(isRecord(request.body) ? request.body.id : undefined));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const subscriptionId = requestId(isRecord(request.body) ? request.body.id : undefined);
|
|
919
|
+
const totalByteLimit = Math.min(Number.MAX_SAFE_INTEGER, resourceSubscriptions.maxEvents * resourceSubscriptions.maxEventBytes);
|
|
920
|
+
let admitted = 0;
|
|
921
|
+
let admittedBytes = 0;
|
|
922
|
+
let detach = () => { };
|
|
923
|
+
let ended = false;
|
|
924
|
+
const boundedBus = {
|
|
925
|
+
publish: (event) => resourceEvents.publish(event),
|
|
926
|
+
subscribe(listener) {
|
|
927
|
+
detach = resourceEvents.subscribe((event) => {
|
|
928
|
+
if (ended || event.kind !== "resource_updated" || event.uri !== uri)
|
|
929
|
+
return;
|
|
930
|
+
const frameBytes = Buffer.byteLength(`event: message\ndata: ${JSON.stringify({
|
|
931
|
+
jsonrpc: "2.0",
|
|
932
|
+
method: "notifications/resources/updated",
|
|
933
|
+
params: {
|
|
934
|
+
uri,
|
|
935
|
+
_meta: { "io.modelcontextprotocol/subscriptionId": subscriptionId },
|
|
936
|
+
},
|
|
937
|
+
})}\n\n`, "utf8");
|
|
938
|
+
if (frameBytes > resourceSubscriptions.maxEventBytes ||
|
|
939
|
+
admitted >= resourceSubscriptions.maxEvents ||
|
|
940
|
+
admittedBytes + frameBytes > totalByteLimit) {
|
|
941
|
+
ended = true;
|
|
942
|
+
detach();
|
|
943
|
+
reply.raw.destroy();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
admitted += 1;
|
|
947
|
+
admittedBytes += frameBytes;
|
|
948
|
+
listener(event);
|
|
949
|
+
});
|
|
950
|
+
return () => {
|
|
951
|
+
ended = true;
|
|
952
|
+
detach();
|
|
953
|
+
};
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
const handler = createMcpHandler(() => new McpServer(serverInfo, {
|
|
957
|
+
capabilities: { resources: { subscribe: true, listChanged: false } },
|
|
958
|
+
supportedProtocolVersions: [PROTOCOL_VERSION],
|
|
959
|
+
}), {
|
|
960
|
+
legacy: "reject",
|
|
961
|
+
responseMode: "sse",
|
|
962
|
+
bus: boundedBus,
|
|
963
|
+
maxSubscriptions: 1,
|
|
964
|
+
keepAliveMs: 0,
|
|
965
|
+
});
|
|
966
|
+
activeSubscriptions.add(handler);
|
|
967
|
+
const lifetime = setTimeout(() => { void handler.close(); }, resourceSubscriptions.lifetimeMs);
|
|
968
|
+
lifetime.unref();
|
|
969
|
+
const closeDisconnected = () => {
|
|
970
|
+
activeSubscriptions.delete(handler);
|
|
971
|
+
void handler.close();
|
|
972
|
+
};
|
|
973
|
+
reply.raw.once("close", closeDisconnected);
|
|
974
|
+
reply.hijack();
|
|
975
|
+
try {
|
|
976
|
+
await toNodeHandler(handler)(request.raw, reply.raw, request.body);
|
|
977
|
+
}
|
|
978
|
+
finally {
|
|
979
|
+
reply.raw.off("close", closeDisconnected);
|
|
980
|
+
clearTimeout(lifetime);
|
|
981
|
+
activeSubscriptions.delete(handler);
|
|
982
|
+
await handler.close();
|
|
983
|
+
}
|
|
984
|
+
}
|
|
857
985
|
const observabilityLimits = observability.length ? { deliveryTimeoutMs: 1_000 } : undefined;
|
|
858
986
|
const finishObservability = observabilityLimits
|
|
859
987
|
? installObservability(app, observability, (body) => capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName), observabilityLimits)
|
|
@@ -867,7 +995,10 @@ export function createEmseepea(options) {
|
|
|
867
995
|
await reply.header("cache-control", "no-store").type("text/plain; charset=utf-8")
|
|
868
996
|
.code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
|
|
869
997
|
});
|
|
870
|
-
app.addHook("preClose", async () => {
|
|
998
|
+
app.addHook("preClose", async () => {
|
|
999
|
+
stopping.abort();
|
|
1000
|
+
await Promise.all([...activeSubscriptions].map((handler) => handler.close()));
|
|
1001
|
+
});
|
|
871
1002
|
if (authentication) {
|
|
872
1003
|
app.all("/.well-known/*", async (request, reply) => {
|
|
873
1004
|
const response = oauthMetadataResponse(new Request(new URL(request.url, authentication.metadata.resourceServerUrl.origin), {
|
|
@@ -889,15 +1020,53 @@ export function createEmseepea(options) {
|
|
|
889
1020
|
if (deployment.mode === "production-behind-proxy" &&
|
|
890
1021
|
!validateProductionRequest(request, reply, deployment, limiter))
|
|
891
1022
|
return;
|
|
892
|
-
|
|
1023
|
+
const protocolVersionHeader = singleHeader(request.raw.rawHeaders, "mcp-protocol-version");
|
|
1024
|
+
const mcpMethodHeader = singleHeader(request.raw.rawHeaders, "mcp-method");
|
|
1025
|
+
const mcpNameHeader = singleHeader(request.raw.rawHeaders, "mcp-name");
|
|
1026
|
+
if (protocolVersionHeader === null || mcpMethodHeader === null || mcpNameHeader === null) {
|
|
1027
|
+
await sendRpcError(reply, 400, -32020, "Conflicting MCP headers", requestId(isRecord(request.body) ? request.body.id : undefined));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const classification = classifyInboundRequest({
|
|
1031
|
+
httpMethod: request.method,
|
|
1032
|
+
...(protocolVersionHeader ? { protocolVersionHeader } : {}),
|
|
1033
|
+
...(mcpMethodHeader ? { mcpMethodHeader } : {}),
|
|
1034
|
+
...(mcpNameHeader ? { mcpNameHeader } : {}),
|
|
1035
|
+
body: request.body,
|
|
1036
|
+
});
|
|
1037
|
+
if (classification.kind === "reject") {
|
|
1038
|
+
await sendRpcError(reply, classification.httpStatus, classification.code, classification.message, requestId(isRecord(request.body) ? request.body.id : undefined), isRecord(classification.data) ? classification.data : undefined);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const legacy = classification.kind === "legacy";
|
|
1042
|
+
if (legacy && classification.requestedVersion &&
|
|
1043
|
+
!LEGACY_PROTOCOL_VERSIONS.includes(classification.requestedVersion)) {
|
|
1044
|
+
await sendRpcError(reply, 400, ProtocolErrorCode.UnsupportedProtocolVersion, `Unsupported protocol version: ${classification.requestedVersion}`, requestId(isRecord(request.body) ? request.body.id : undefined), { requested: classification.requestedVersion, supported: [...SUPPORTED_PROTOCOLS] });
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (!await validateMcpRequestHeaders(request, reply, legacy))
|
|
893
1048
|
return;
|
|
894
|
-
if (isRecord(request.body) && typeof request.body.method === "string" &&
|
|
1049
|
+
if (!legacy && isRecord(request.body) && typeof request.body.method === "string" &&
|
|
895
1050
|
!enabledMethods.has(request.body.method)) {
|
|
896
1051
|
await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
|
|
897
1052
|
return;
|
|
898
1053
|
}
|
|
899
|
-
const access = capabilityAccessForRequest(request.body, toolsByName, resourcesByUri, resourceTemplates, promptsByName);
|
|
900
1054
|
const protectsCatalogue = authentication?.discovery === "protected";
|
|
1055
|
+
let subscriptionTarget;
|
|
1056
|
+
if (!legacy && isRecord(request.body) && request.body.method === "subscriptions/listen") {
|
|
1057
|
+
try {
|
|
1058
|
+
subscriptionTarget = resourceSubscriptionTarget(request.body, resourceSubscriptions.maxUriBytes, resourcesByUri, resourceTemplates);
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
await sendRpcError(reply, 400, -32602, "Invalid resource subscription", requestId(request.body.id));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (!subscriptionTarget.resource && !protectsCatalogue) {
|
|
1065
|
+
await sendRpcError(reply, 200, -32602, "Capability not found", requestId(request.body.id));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const access = subscriptionTarget?.resource?.[RESOURCE_ACCESS] ?? capabilityAccessForRequest(request.body, toolsByName, resourcesByUri, resourceTemplates, promptsByName);
|
|
901
1070
|
let authInfo;
|
|
902
1071
|
if (authentication && (protectsCatalogue || access && access !== "public")) {
|
|
903
1072
|
try {
|
|
@@ -922,6 +1091,11 @@ export function createEmseepea(options) {
|
|
|
922
1091
|
await sendRpcError(reply, 200, -32602, "Capability not found", requestId(isRecord(request.body) ? request.body.id : undefined));
|
|
923
1092
|
return;
|
|
924
1093
|
}
|
|
1094
|
+
if (subscriptionTarget) {
|
|
1095
|
+
await serveResourceSubscription(request, reply, subscriptionTarget.uri);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const capability = capabilityNameForRequest(request.body, toolsByName, resourcesByUri, resourceTemplates, promptsByName);
|
|
925
1099
|
const disconnected = new AbortController();
|
|
926
1100
|
const abort = () => disconnected.abort();
|
|
927
1101
|
request.raw.once("aborted", abort);
|
|
@@ -935,6 +1109,8 @@ export function createEmseepea(options) {
|
|
|
935
1109
|
signal: disconnected.signal,
|
|
936
1110
|
principal,
|
|
937
1111
|
filterCatalogues: protectsCatalogue,
|
|
1112
|
+
capability,
|
|
1113
|
+
legacy,
|
|
938
1114
|
}, () => nodeHandler(request.raw, reply.raw, request.body));
|
|
939
1115
|
}
|
|
940
1116
|
finally {
|
|
@@ -960,9 +1136,26 @@ export function createEmseepea(options) {
|
|
|
960
1136
|
stopping,
|
|
961
1137
|
observabilityLimits,
|
|
962
1138
|
finishObservability,
|
|
1139
|
+
...(resourceEvents ? {
|
|
1140
|
+
notifyResourceUpdated(uri) {
|
|
1141
|
+
if (stopping.signal.aborted)
|
|
1142
|
+
throw new Error("Cannot notify from a stopped Em See Pea app");
|
|
1143
|
+
if (typeof uri !== "string" || Buffer.byteLength(uri, "utf8") > resourceSubscriptions.maxUriBytes ||
|
|
1144
|
+
!(resourcesByUri.has(uri) || resourceTemplates.some((resource) => resource[RESOURCE_MATCHES]?.(uri)))) {
|
|
1145
|
+
throw new TypeError("Resource update URI must match a registered resource");
|
|
1146
|
+
}
|
|
1147
|
+
resourceEvents.publish({ kind: "resource_updated", uri });
|
|
1148
|
+
},
|
|
1149
|
+
} : {}),
|
|
963
1150
|
});
|
|
964
1151
|
return app;
|
|
965
1152
|
}
|
|
1153
|
+
export function notifyResourceUpdated(app, uri) {
|
|
1154
|
+
const notify = runtimes.get(app)?.notifyResourceUpdated;
|
|
1155
|
+
if (!notify)
|
|
1156
|
+
throw new TypeError("notifyResourceUpdated requires resourceSubscriptions");
|
|
1157
|
+
notify(uri);
|
|
1158
|
+
}
|
|
966
1159
|
function safeOAuthError(error) {
|
|
967
1160
|
if (!(error instanceof OAuthError))
|
|
968
1161
|
return error;
|
|
@@ -1040,9 +1233,29 @@ function capabilityAccessForRequest(body, toolsByName, resourcesByUri, resourceT
|
|
|
1040
1233
|
}
|
|
1041
1234
|
return undefined;
|
|
1042
1235
|
}
|
|
1236
|
+
function resourceSubscriptionTarget(body, maxUriBytes, resourcesByUri, resourceTemplates) {
|
|
1237
|
+
if (!isRecord(body) || body.method !== "subscriptions/listen" || !isRecord(body.params) ||
|
|
1238
|
+
!isRecord(body.params.notifications)) {
|
|
1239
|
+
throw new TypeError("Invalid resource subscription");
|
|
1240
|
+
}
|
|
1241
|
+
const notifications = body.params.notifications;
|
|
1242
|
+
if (Object.keys(notifications).some((key) => key !== "resourceSubscriptions") ||
|
|
1243
|
+
!Array.isArray(notifications.resourceSubscriptions) ||
|
|
1244
|
+
notifications.resourceSubscriptions.length !== 1 ||
|
|
1245
|
+
typeof notifications.resourceSubscriptions[0] !== "string" ||
|
|
1246
|
+
Buffer.byteLength(notifications.resourceSubscriptions[0], "utf8") > maxUriBytes) {
|
|
1247
|
+
throw new TypeError("Invalid resource subscription");
|
|
1248
|
+
}
|
|
1249
|
+
const uri = notifications.resourceSubscriptions[0];
|
|
1250
|
+
return {
|
|
1251
|
+
uri,
|
|
1252
|
+
resource: resourcesByUri.get(uri) ?? resourceTemplates.find((candidate) => candidate[RESOURCE_MATCHES]?.(uri)),
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1043
1255
|
function isCapabilityInvocation(body) {
|
|
1044
1256
|
return isRecord(body) && (body.method === "tools/call" || body.method === "resources/read" ||
|
|
1045
|
-
body.method === "prompts/get" || body.method === "completion/complete"
|
|
1257
|
+
body.method === "prompts/get" || body.method === "completion/complete" ||
|
|
1258
|
+
body.method === "subscriptions/listen");
|
|
1046
1259
|
}
|
|
1047
1260
|
function capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName) {
|
|
1048
1261
|
if (!isRecord(body) || !isRecord(body.params))
|
|
@@ -1055,6 +1268,12 @@ function capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTem
|
|
|
1055
1268
|
const resource = resourcesByUri.get(params.uri) ?? resourceTemplates.find((candidate) => candidate[RESOURCE_MATCHES]?.(params.uri));
|
|
1056
1269
|
return resource?.[RESOURCE_NAME];
|
|
1057
1270
|
}
|
|
1271
|
+
if (body.method === "subscriptions/listen" && isRecord(params.notifications) &&
|
|
1272
|
+
Array.isArray(params.notifications.resourceSubscriptions) &&
|
|
1273
|
+
typeof params.notifications.resourceSubscriptions[0] === "string") {
|
|
1274
|
+
const uri = params.notifications.resourceSubscriptions[0];
|
|
1275
|
+
return (resourcesByUri.get(uri) ?? resourceTemplates.find((candidate) => candidate[RESOURCE_MATCHES]?.(uri)))?.[RESOURCE_NAME];
|
|
1276
|
+
}
|
|
1058
1277
|
if (body.method === "prompts/get" && typeof params.name === "string") {
|
|
1059
1278
|
return promptsByName.has(params.name) ? params.name : undefined;
|
|
1060
1279
|
}
|
|
@@ -1352,14 +1571,14 @@ function singleHeader(rawHeaders, name) {
|
|
|
1352
1571
|
}
|
|
1353
1572
|
return values.length === 0 ? undefined : values.length === 1 ? values[0] : null;
|
|
1354
1573
|
}
|
|
1355
|
-
async function validateMcpRequestHeaders(request, reply) {
|
|
1574
|
+
async function validateMcpRequestHeaders(request, reply, legacy) {
|
|
1356
1575
|
const body = request.body;
|
|
1357
1576
|
const id = isRecord(body) ? requestId(body.id) : null;
|
|
1358
1577
|
if (!acceptsMcpResponses(request.headers.accept)) {
|
|
1359
1578
|
await sendRpcError(reply, 406, -32000, "Not Acceptable: Client must accept both application/json and text/event-stream", id);
|
|
1360
1579
|
return false;
|
|
1361
1580
|
}
|
|
1362
|
-
if (!isRecord(body) || typeof body.method !== "string")
|
|
1581
|
+
if (legacy || !isRecord(body) || typeof body.method !== "string")
|
|
1363
1582
|
return true;
|
|
1364
1583
|
const header = (name) => singleHeader(request.raw.rawHeaders, name);
|
|
1365
1584
|
const rejectMismatch = async (name) => {
|
|
@@ -1864,13 +2083,30 @@ function nonNegativePort(value) {
|
|
|
1864
2083
|
}
|
|
1865
2084
|
return value;
|
|
1866
2085
|
}
|
|
1867
|
-
function
|
|
1868
|
-
|
|
1869
|
-
|
|
2086
|
+
function directHandlerContext(access, context, signal, deadlineMs, requestState) {
|
|
2087
|
+
return {
|
|
2088
|
+
signal,
|
|
2089
|
+
deadlineMs,
|
|
2090
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
2091
|
+
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
2092
|
+
...(requestState ? {
|
|
2093
|
+
requestState: context.mcpReq.requestState(),
|
|
2094
|
+
mintRequestState: (payload) => requestState.mint(payload, context),
|
|
2095
|
+
} : {}),
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
async function assertInputRequired(result, requestState, context) {
|
|
2099
|
+
if (!result.inputRequests && result.requestState === undefined) {
|
|
2100
|
+
throw new Error("Client-input result must include requests or state");
|
|
1870
2101
|
}
|
|
1871
|
-
if (Object.values(result.inputRequests).some((request) => request.method !== "elicitation/create")) {
|
|
2102
|
+
if (result.inputRequests && Object.values(result.inputRequests).some((request) => request.method !== "elicitation/create")) {
|
|
1872
2103
|
throw new Error("Client-input request kind is not supported");
|
|
1873
2104
|
}
|
|
2105
|
+
if (result.requestState !== undefined) {
|
|
2106
|
+
if (!requestState)
|
|
2107
|
+
throw new Error("Client-input request state is not configured");
|
|
2108
|
+
await requestState.verify(result.requestState, context);
|
|
2109
|
+
}
|
|
1874
2110
|
}
|
|
1875
2111
|
function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal) {
|
|
1876
2112
|
let serialized;
|