@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/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({ inputRequests: spec.inputRequests }), {
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
- assertStatelessInputRequired(result);
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
- assertStatelessInputRequired(result);
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
- assertStatelessInputRequired(result);
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
- inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
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
- assertStatelessInputRequired(result);
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 ? { resources: { subscribe: false, listChanged: false } } : {}),
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: [PROTOCOL_VERSION],
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
- legacy: "reject",
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 () => { stopping.abort(); });
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
- if (!await validateMcpRequestHeaders(request, reply))
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 assertStatelessInputRequired(result) {
1868
- if (!result.inputRequests || result.requestState !== undefined) {
1869
- throw new Error("Client-input requests must be stateless");
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;