@emseepea/server 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,9 +8,13 @@ import { readFileSync } from "node:fs";
8
8
  import { readdir, realpath } from "node:fs/promises";
9
9
  import { isIP } from "node:net";
10
10
  import { isAbsolute, join } from "node:path";
11
+ import { performance } from "node:perf_hooks";
11
12
  import { fileURLToPath, pathToFileURL } from "node:url";
12
13
  import { z } from "zod";
14
+ import { prepareAppShutdown, remainingAppShutdown, startAppShutdown, registerAppShutdownOwner } from "./app-shutdown.js";
13
15
  import { createMcpEventRuntime, McpEventInputError } from "./events.js";
16
+ import { createExternalMcpEventRuntime } from "./events-external.js";
17
+ export { ExternalMcpEventError } from "./events-external.js";
14
18
  import { createResourceInventory, ResourceInventoryCursorError } from "./resource-inventory.js";
15
19
  import { installObservability, observabilityState, } from "./telemetry.js";
16
20
  export { openTelemetry, structuredLogging, } from "./telemetry.js";
@@ -1093,6 +1097,8 @@ export function createEmseepea(options) {
1093
1097
  const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
1094
1098
  const stopping = new AbortController();
1095
1099
  let eventRuntime;
1100
+ let internalEventRuntime;
1101
+ let externalEventRuntime;
1096
1102
  let pendingReadiness;
1097
1103
  async function ready() {
1098
1104
  if (stopping.signal.aborted)
@@ -1173,7 +1179,14 @@ export function createEmseepea(options) {
1173
1179
  if (options.events && !authentication) {
1174
1180
  throw new TypeError("MCP Events require authentication configuration");
1175
1181
  }
1176
- eventRuntime = options.events ? createMcpEventRuntime(options.events) : undefined;
1182
+ if (options.events) {
1183
+ if ("mode" in options.events) {
1184
+ eventRuntime = externalEventRuntime = createExternalMcpEventRuntime(options.events);
1185
+ }
1186
+ else {
1187
+ eventRuntime = internalEventRuntime = createMcpEventRuntime(options.events);
1188
+ }
1189
+ }
1177
1190
  const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
1178
1191
  const hasProgress = hasStreaming || resources.length > 0 || prompts.length > 0;
1179
1192
  if ([
@@ -1332,6 +1345,22 @@ export function createEmseepea(options) {
1332
1345
  });
1333
1346
  const nodeHandler = toNodeHandler(sdkHandler);
1334
1347
  const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
1348
+ const unstartedSockets = new Set();
1349
+ app.server.on("connection", (socket) => {
1350
+ if (stopping.signal.aborted) {
1351
+ socket.destroy();
1352
+ return;
1353
+ }
1354
+ unstartedSockets.add(socket);
1355
+ socket.once("close", () => unstartedSockets.delete(socket));
1356
+ });
1357
+ app.server.on("request", (request, response) => {
1358
+ unstartedSockets.delete(request.socket);
1359
+ response.once("finish", () => {
1360
+ if (stopping.signal.aborted)
1361
+ queueMicrotask(() => app.server.closeIdleConnections());
1362
+ });
1363
+ });
1335
1364
  async function serveResourceSubscription(request, reply, uri) {
1336
1365
  if (!resourceSubscriptions || !resourceEvents)
1337
1366
  return;
@@ -1421,7 +1450,11 @@ export function createEmseepea(options) {
1421
1450
  .code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
1422
1451
  });
1423
1452
  app.addHook("preClose", async () => {
1453
+ prepareAppShutdown(app);
1454
+ void startAppShutdown(app);
1424
1455
  stopping.abort();
1456
+ for (const socket of unstartedSockets)
1457
+ socket.destroy();
1425
1458
  await Promise.all([...activeSubscriptions].map((handler) => handler.close()));
1426
1459
  });
1427
1460
  if (authentication) {
@@ -1442,6 +1475,7 @@ export function createEmseepea(options) {
1442
1475
  handler: async (_request, reply) => { await reply.header("allow", "POST").code(405).send(); },
1443
1476
  });
1444
1477
  app.post("/mcp", { bodyLimit: maxRequestBytes }, async (request, reply) => {
1478
+ const admittedAt = performance.now();
1445
1479
  if (deployment.mode === "production-behind-proxy" &&
1446
1480
  !validateProductionRequest(request, reply, deployment, limiter))
1447
1481
  return;
@@ -1538,95 +1572,111 @@ export function createEmseepea(options) {
1538
1572
  }
1539
1573
  if (eventRuntime && !legacy && isRecord(request.body) &&
1540
1574
  (request.body.method === "server/discover" || eventMethod)) {
1541
- if (!await eventRuntime.ready()) {
1542
- await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1543
- return;
1544
- }
1545
- if (request.body.method === "server/discover") {
1546
- const activeTools = protectsCatalogue
1547
- ? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], principal)) : tools;
1548
- const activeResources = protectsCatalogue
1549
- ? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], principal)) : resources;
1550
- const activePrompts = protectsCatalogue
1551
- ? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], principal)) : prompts;
1552
- const capabilities = {
1553
- ...(activeTools.length ? { tools: { listChanged: false } } : {}),
1554
- ...(activeResources.length ? { resources: {
1555
- subscribe: Boolean(resourceSubscriptions), listChanged: false,
1556
- } } : {}),
1557
- ...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
1558
- ...(activeResources.some((resource) => resource[HAS_COMPLETION]) ||
1559
- activePrompts.some((prompt) => prompt[HAS_COMPLETION]) ? { completions: {} } : {}),
1560
- ...(clientLogging ? { logging: {} } : {}),
1561
- events: {},
1562
- };
1563
- await reply.code(200).send({
1564
- jsonrpc: "2.0",
1565
- id: requestId(request.body.id),
1566
- result: {
1567
- supportedVersions: [PROTOCOL_VERSION],
1568
- capabilities,
1569
- resultType: "complete",
1570
- ttlMs: protectsCatalogue ? 0 : cacheHints?.["server/discover"]?.ttlMs ?? 0,
1571
- cacheScope: protectsCatalogue ? "private" : cacheHints?.["server/discover"]?.cacheScope ?? "private",
1572
- ...(options.instructions ? { instructions: options.instructions } : {}),
1573
- _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
1574
- },
1575
- });
1576
- return;
1577
- }
1578
- if (!authInfo)
1579
- throw new Error("Events reached execution without verified authorization");
1580
- let ownerKey;
1575
+ const disconnectedEvents = new AbortController();
1576
+ const abortEvents = () => disconnectedEvents.abort();
1577
+ request.raw.once("aborted", abortEvents);
1578
+ reply.raw.once("close", abortEvents);
1579
+ if (requestAlreadyClosed(request, reply))
1580
+ abortEvents();
1581
1581
  try {
1582
- ownerKey = await eventRuntime.ownerKey(authInfo);
1583
- }
1584
- catch {
1585
- await sendRpcError(reply, 500, -32603, "Event owner identity unavailable", requestId(request.body.id));
1586
- return;
1587
- }
1588
- if (request.body.method === "events/list") {
1589
- try {
1590
- const events = await eventRuntime.list(ownerKey);
1582
+ const requestEvents = externalEventRuntime ? externalEventRuntime.forInvocation({
1583
+ signal: AbortSignal.any([stopping.signal, disconnectedEvents.signal]),
1584
+ deadlineMs: Date.now() + Math.max(0, operationTimeoutMs - (performance.now() - admittedAt)),
1585
+ }) : eventRuntime;
1586
+ if (!await requestEvents.ready()) {
1587
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1588
+ return;
1589
+ }
1590
+ if (request.body.method === "server/discover") {
1591
+ const activeTools = protectsCatalogue
1592
+ ? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], principal)) : tools;
1593
+ const activeResources = protectsCatalogue
1594
+ ? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], principal)) : resources;
1595
+ const activePrompts = protectsCatalogue
1596
+ ? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], principal)) : prompts;
1597
+ const capabilities = {
1598
+ ...(activeTools.length ? { tools: { listChanged: false } } : {}),
1599
+ ...(activeResources.length ? { resources: {
1600
+ subscribe: Boolean(resourceSubscriptions), listChanged: false,
1601
+ } } : {}),
1602
+ ...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
1603
+ ...(activeResources.some((resource) => resource[HAS_COMPLETION]) ||
1604
+ activePrompts.some((prompt) => prompt[HAS_COMPLETION]) ? { completions: {} } : {}),
1605
+ ...(clientLogging ? { logging: {} } : {}),
1606
+ events: {},
1607
+ };
1591
1608
  await reply.code(200).send({
1592
1609
  jsonrpc: "2.0",
1593
1610
  id: requestId(request.body.id),
1594
- result: { events, _meta: { "io.modelcontextprotocol/serverInfo": serverInfo } },
1611
+ result: {
1612
+ supportedVersions: [PROTOCOL_VERSION],
1613
+ capabilities,
1614
+ resultType: "complete",
1615
+ ttlMs: protectsCatalogue ? 0 : cacheHints?.["server/discover"]?.ttlMs ?? 0,
1616
+ cacheScope: protectsCatalogue ? "private" : cacheHints?.["server/discover"]?.cacheScope ?? "private",
1617
+ ...(options.instructions ? { instructions: options.instructions } : {}),
1618
+ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
1619
+ },
1595
1620
  });
1621
+ return;
1596
1622
  }
1597
- catch {
1598
- await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1599
- }
1600
- return;
1601
- }
1602
- if (request.body.method === "events/subscribe") {
1623
+ if (!authInfo)
1624
+ throw new Error("Events reached execution without verified authorization");
1625
+ let ownerKey;
1603
1626
  try {
1604
- const result = await eventRuntime.subscribe(ownerKey, request.body.params);
1605
- await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result });
1627
+ ownerKey = await requestEvents.ownerKey(authInfo);
1606
1628
  }
1607
- catch (error) {
1608
- if (error instanceof McpEventInputError) {
1609
- await sendRpcError(reply, error.code === -32015 ? 200 : 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
1610
- return;
1611
- }
1612
- await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1629
+ catch {
1630
+ await sendRpcError(reply, 500, -32603, "Event owner identity unavailable", requestId(request.body.id));
1631
+ return;
1613
1632
  }
1614
- return;
1615
- }
1616
- if (request.body.method === "events/unsubscribe") {
1617
- try {
1618
- await eventRuntime.unsubscribe(ownerKey, request.body.params);
1619
- await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result: {} });
1633
+ if (request.body.method === "events/list") {
1634
+ try {
1635
+ const events = await requestEvents.list(ownerKey);
1636
+ await reply.code(200).send({
1637
+ jsonrpc: "2.0",
1638
+ id: requestId(request.body.id),
1639
+ result: { events, _meta: { "io.modelcontextprotocol/serverInfo": serverInfo } },
1640
+ });
1641
+ }
1642
+ catch {
1643
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1644
+ }
1645
+ return;
1620
1646
  }
1621
- catch (error) {
1622
- if (error instanceof McpEventInputError) {
1623
- await sendRpcError(reply, 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
1647
+ if (request.body.method === "events/subscribe") {
1648
+ try {
1649
+ const result = await requestEvents.subscribe(ownerKey, request.body.params);
1650
+ await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result });
1624
1651
  }
1625
- else {
1652
+ catch (error) {
1653
+ if (error instanceof McpEventInputError) {
1654
+ await sendRpcError(reply, error.code === -32015 ? 200 : 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
1655
+ return;
1656
+ }
1626
1657
  await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1627
1658
  }
1659
+ return;
1628
1660
  }
1629
- return;
1661
+ if (request.body.method === "events/unsubscribe") {
1662
+ try {
1663
+ await requestEvents.unsubscribe(ownerKey, request.body.params);
1664
+ await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result: {} });
1665
+ }
1666
+ catch (error) {
1667
+ if (error instanceof McpEventInputError) {
1668
+ await sendRpcError(reply, 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
1669
+ }
1670
+ else {
1671
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1672
+ }
1673
+ }
1674
+ return;
1675
+ }
1676
+ }
1677
+ finally {
1678
+ request.raw.off("aborted", abortEvents);
1679
+ reply.raw.off("close", abortEvents);
1630
1680
  }
1631
1681
  }
1632
1682
  if (subscriptionTarget) {
@@ -1644,7 +1694,7 @@ export function createEmseepea(options) {
1644
1694
  try {
1645
1695
  await requestOperations.run({
1646
1696
  deadlineMs: Date.now() + operationTimeoutMs,
1647
- signal: disconnected.signal,
1697
+ signal: AbortSignal.any([disconnected.signal, stopping.signal]),
1648
1698
  principal,
1649
1699
  filterCatalogues: protectsCatalogue,
1650
1700
  capability,
@@ -1670,17 +1720,32 @@ export function createEmseepea(options) {
1670
1720
  }
1671
1721
  });
1672
1722
  let drainingEvents = false;
1673
- const eventTimer = eventRuntime ? setInterval(() => {
1723
+ const eventTimer = internalEventRuntime ? setInterval(() => {
1674
1724
  if (drainingEvents || stopping.signal.aborted)
1675
1725
  return;
1676
1726
  drainingEvents = true;
1677
- void eventRuntime.drain().catch(() => { }).finally(() => { drainingEvents = false; });
1727
+ void internalEventRuntime.drain().catch(() => { }).finally(() => { drainingEvents = false; });
1678
1728
  }, 1000) : undefined;
1679
1729
  eventTimer?.unref();
1730
+ let externalSettled = true;
1731
+ if (externalEventRuntime)
1732
+ registerAppShutdownOwner(app, async (invocation) => {
1733
+ try {
1734
+ return externalSettled = await externalEventRuntime.settle(invocation);
1735
+ }
1736
+ catch {
1737
+ return externalSettled = false;
1738
+ }
1739
+ });
1680
1740
  app.addHook("onClose", async () => {
1681
1741
  if (eventTimer)
1682
1742
  clearInterval(eventTimer);
1743
+ const ownersSettled = await startAppShutdown(app);
1683
1744
  await sdkHandler.close();
1745
+ if (!externalSettled)
1746
+ throw new Error("External Events shutdown has unfinished operations");
1747
+ if (!ownersSettled)
1748
+ throw new Error("APP_SHUTDOWN_INCOMPLETE");
1684
1749
  });
1685
1750
  runtimes.set(app, {
1686
1751
  deployment,
@@ -1912,8 +1977,12 @@ export async function serveEmseepea(app, options = {}) {
1912
1977
  return {
1913
1978
  url: new URL(`http://${urlHost}:${address.port}/mcp`),
1914
1979
  close: () => {
1980
+ if (closing)
1981
+ return closing;
1982
+ prepareAppShutdown(app, shutdownTimeoutMs);
1915
1983
  runtime.stopping.abort();
1916
- return closing ??= closeApp(app, shutdownTimeoutMs, observabilityFlushTimeoutMs, runtime.finishObservability);
1984
+ void startAppShutdown(app);
1985
+ return closing = closeApp(app, remainingAppShutdown(app), observabilityFlushTimeoutMs, runtime.finishObservability);
1917
1986
  },
1918
1987
  };
1919
1988
  }
@@ -2524,14 +2593,27 @@ function promptArguments(schema) {
2524
2593
  })));
2525
2594
  }
2526
2595
  function sdkMetadataSchema(schema) {
2596
+ // The declaration is captured once, but the SDK reads metadata again for
2597
+ // subsequent registrations and requests. Keep output lazy: an input
2598
+ // transform can be valid even when it has no representable output schema.
2599
+ const documents = new Map();
2600
+ const document = (io) => {
2601
+ let captured = documents.get(io);
2602
+ if (!captured) {
2603
+ captured = deepFreeze(structuredClone(jsonMetadataSchema(schema, io)));
2604
+ documents.set(io, captured);
2605
+ }
2606
+ return captured;
2607
+ };
2608
+ document("input");
2527
2609
  return {
2528
2610
  "~standard": {
2529
2611
  version: 1,
2530
2612
  vendor: "emseepea",
2531
2613
  validate: (value) => ({ value }),
2532
2614
  jsonSchema: {
2533
- input: () => jsonMetadataSchema(schema, "input"),
2534
- output: () => jsonMetadataSchema(schema, "output"),
2615
+ input: () => document("input"),
2616
+ output: () => document("output"),
2535
2617
  },
2536
2618
  },
2537
2619
  };
@@ -2702,7 +2784,12 @@ function requestId(value) {
2702
2784
  async function runWithDeadline(requestSignal, deadlineMs, work) {
2703
2785
  const remainingMs = deadlineMs - Date.now();
2704
2786
  const deadlineSignal = remainingMs > 0 ? AbortSignal.timeout(remainingMs) : AbortSignal.abort();
2705
- const signal = AbortSignal.any([requestSignal, deadlineSignal]);
2787
+ const invocationSignal = requestOperations.getStore()?.signal;
2788
+ const signal = AbortSignal.any([
2789
+ requestSignal,
2790
+ deadlineSignal,
2791
+ ...(invocationSignal ? [invocationSignal] : []),
2792
+ ]);
2706
2793
  return new Promise((resolve, reject) => {
2707
2794
  const onAbort = () => reject(new Error("Tool execution cancelled"));
2708
2795
  if (signal.aborted)
@@ -2968,13 +3055,19 @@ async function closeApp(app, timeoutMs, flushTimeoutMs, finishObservability) {
2968
3055
  let onClosed;
2969
3056
  const httpClosed = new Promise((resolve) => { onClosed = resolve; });
2970
3057
  app.server.once("close", onClosed);
2971
- const timeout = new Promise((resolve) => {
3058
+ const timeout = new Promise((_resolve, reject) => {
2972
3059
  timer = setTimeout(() => {
2973
- app.server.close();
2974
- app.server.closeAllConnections();
2975
- resolve();
3060
+ try {
3061
+ app.server.close();
3062
+ app.server.closeAllConnections();
3063
+ }
3064
+ catch {
3065
+ // Cleanup failure must not hide the incomplete shutdown outcome.
3066
+ }
3067
+ finally {
3068
+ reject(new Error("SERVER_SHUTDOWN_INCOMPLETE"));
3069
+ }
2976
3070
  }, timeoutMs);
2977
- timer.unref();
2978
3071
  });
2979
3072
  try {
2980
3073
  await Promise.race([app.close(), timeout]);