@emseepea/server 0.19.1 → 0.21.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
@@ -10,6 +10,7 @@ import { isIP } from "node:net";
10
10
  import { isAbsolute, join } from "node:path";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { z } from "zod";
13
+ import { createMcpEventRuntime, McpEventInputError } from "./events.js";
13
14
  import { installObservability, observabilityState, } from "./telemetry.js";
14
15
  export { openTelemetry, structuredLogging, } from "./telemetry.js";
15
16
  export * from "./ui.js";
@@ -1084,17 +1085,19 @@ export function createEmseepea(options) {
1084
1085
  const readiness = options.readiness;
1085
1086
  const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
1086
1087
  const stopping = new AbortController();
1088
+ let eventRuntime;
1087
1089
  let pendingReadiness;
1088
1090
  async function ready() {
1089
1091
  if (stopping.signal.aborted)
1090
1092
  return false;
1091
- if (!readiness)
1093
+ if (!readiness && !eventRuntime)
1092
1094
  return true;
1093
1095
  if (!pendingReadiness) {
1094
1096
  const deadline = Date.now() + readinessTimeoutMs;
1095
1097
  pendingReadiness = runWithDeadline(stopping.signal, deadline, async (signal) => {
1096
1098
  try {
1097
- return await readiness({ signal }) === true && Date.now() < deadline;
1099
+ return (!readiness || await readiness({ signal }) === true) &&
1100
+ (!eventRuntime || await eventRuntime.ready()) && Date.now() < deadline;
1098
1101
  }
1099
1102
  finally {
1100
1103
  pendingReadiness = undefined;
@@ -1160,6 +1163,10 @@ export function createEmseepea(options) {
1160
1163
  const authentication = options.authentication
1161
1164
  ? normalizeAuthentication(options.authentication)
1162
1165
  : undefined;
1166
+ if (options.events && !authentication) {
1167
+ throw new TypeError("MCP Events require authentication configuration");
1168
+ }
1169
+ eventRuntime = options.events ? createMcpEventRuntime(options.events) : undefined;
1163
1170
  const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
1164
1171
  const hasProgress = hasStreaming || resources.length > 0 || prompts.length > 0;
1165
1172
  if ([
@@ -1176,6 +1183,8 @@ export function createEmseepea(options) {
1176
1183
  const resourceTemplates = resources.filter((resource) => resource[RESOURCE_KIND] === "template");
1177
1184
  const promptsByName = new Map(prompts.map((prompt) => [prompt[PROMPT_NAME], prompt]));
1178
1185
  const enabledMethods = new Set(["server/discover"]);
1186
+ if (eventRuntime)
1187
+ enabledMethods.add("events/list").add("events/subscribe").add("events/unsubscribe");
1179
1188
  if (tools.length)
1180
1189
  enabledMethods.add("tools/list").add("tools/call");
1181
1190
  if (resources.length)
@@ -1408,6 +1417,9 @@ export function createEmseepea(options) {
1408
1417
  return;
1409
1418
  }
1410
1419
  const legacy = classification.kind === "legacy";
1420
+ const eventMethod = !legacy && isRecord(request.body) &&
1421
+ (request.body.method === "events/list" || request.body.method === "events/subscribe" ||
1422
+ request.body.method === "events/unsubscribe");
1411
1423
  if (legacy && classification.requestedVersion &&
1412
1424
  !LEGACY_PROTOCOL_VERSIONS.includes(classification.requestedVersion)) {
1413
1425
  await sendRpcError(reply, 400, ProtocolErrorCode.UnsupportedProtocolVersion, `Unsupported protocol version for the initialize handshake: ` +
@@ -1444,14 +1456,14 @@ export function createEmseepea(options) {
1444
1456
  markObservabilityProtocolError(request);
1445
1457
  }
1446
1458
  let authInfo;
1447
- if (authentication && (protectsCatalogue || access && access !== "public")) {
1459
+ if (authentication && (eventMethod || protectsCatalogue || access && access !== "public")) {
1448
1460
  try {
1449
- authInfo = await verifyAuthenticatedRequest(request, reply, protectsCatalogue ? [] : access.requiredScopes, authentication);
1461
+ authInfo = await verifyAuthenticatedRequest(request, reply, eventMethod || protectsCatalogue ? [] : access.requiredScopes, authentication);
1450
1462
  request.raw.auth = authInfo;
1451
1463
  }
1452
1464
  catch (error) {
1453
1465
  await sendWebResponse(reply, bearerAuthChallengeResponse(safeOAuthError(error), {
1454
- requiredScopes: protectsCatalogue
1466
+ requiredScopes: eventMethod || protectsCatalogue
1455
1467
  ? []
1456
1468
  : [...access.requiredScopes],
1457
1469
  resourceMetadataUrl: authentication.resourceMetadataUrl,
@@ -1467,6 +1479,99 @@ export function createEmseepea(options) {
1467
1479
  await sendRpcError(reply, 200, -32602, "Capability not found", requestId(isRecord(request.body) ? request.body.id : undefined));
1468
1480
  return;
1469
1481
  }
1482
+ if (eventRuntime && !legacy && isRecord(request.body) &&
1483
+ (request.body.method === "server/discover" || eventMethod)) {
1484
+ if (!await eventRuntime.ready()) {
1485
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1486
+ return;
1487
+ }
1488
+ if (request.body.method === "server/discover") {
1489
+ const activeTools = protectsCatalogue
1490
+ ? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], principal)) : tools;
1491
+ const activeResources = protectsCatalogue
1492
+ ? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], principal)) : resources;
1493
+ const activePrompts = protectsCatalogue
1494
+ ? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], principal)) : prompts;
1495
+ const capabilities = {
1496
+ ...(activeTools.length ? { tools: { listChanged: false } } : {}),
1497
+ ...(activeResources.length ? { resources: {
1498
+ subscribe: Boolean(resourceSubscriptions), listChanged: false,
1499
+ } } : {}),
1500
+ ...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
1501
+ ...(activeResources.some((resource) => resource[HAS_COMPLETION]) ||
1502
+ activePrompts.some((prompt) => prompt[HAS_COMPLETION]) ? { completions: {} } : {}),
1503
+ ...(clientLogging ? { logging: {} } : {}),
1504
+ events: {},
1505
+ };
1506
+ await reply.code(200).send({
1507
+ jsonrpc: "2.0",
1508
+ id: requestId(request.body.id),
1509
+ result: {
1510
+ supportedVersions: [PROTOCOL_VERSION],
1511
+ capabilities,
1512
+ resultType: "complete",
1513
+ ttlMs: protectsCatalogue ? 0 : cacheHints?.["server/discover"]?.ttlMs ?? 0,
1514
+ cacheScope: protectsCatalogue ? "private" : cacheHints?.["server/discover"]?.cacheScope ?? "private",
1515
+ ...(options.instructions ? { instructions: options.instructions } : {}),
1516
+ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
1517
+ },
1518
+ });
1519
+ return;
1520
+ }
1521
+ if (!authInfo)
1522
+ throw new Error("Events reached execution without verified authorization");
1523
+ let ownerKey;
1524
+ try {
1525
+ ownerKey = await eventRuntime.ownerKey(authInfo);
1526
+ }
1527
+ catch {
1528
+ await sendRpcError(reply, 500, -32603, "Event owner identity unavailable", requestId(request.body.id));
1529
+ return;
1530
+ }
1531
+ if (request.body.method === "events/list") {
1532
+ try {
1533
+ const events = await eventRuntime.list(ownerKey);
1534
+ await reply.code(200).send({
1535
+ jsonrpc: "2.0",
1536
+ id: requestId(request.body.id),
1537
+ result: { events, _meta: { "io.modelcontextprotocol/serverInfo": serverInfo } },
1538
+ });
1539
+ }
1540
+ catch {
1541
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1542
+ }
1543
+ return;
1544
+ }
1545
+ if (request.body.method === "events/subscribe") {
1546
+ try {
1547
+ const result = await eventRuntime.subscribe(ownerKey, request.body.params);
1548
+ await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result });
1549
+ }
1550
+ catch (error) {
1551
+ if (error instanceof McpEventInputError) {
1552
+ await sendRpcError(reply, error.code === -32015 ? 200 : 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
1553
+ return;
1554
+ }
1555
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1556
+ }
1557
+ return;
1558
+ }
1559
+ if (request.body.method === "events/unsubscribe") {
1560
+ try {
1561
+ await eventRuntime.unsubscribe(ownerKey, request.body.params);
1562
+ await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result: {} });
1563
+ }
1564
+ catch (error) {
1565
+ if (error instanceof McpEventInputError) {
1566
+ await sendRpcError(reply, 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
1567
+ }
1568
+ else {
1569
+ await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
1570
+ }
1571
+ }
1572
+ return;
1573
+ }
1574
+ }
1470
1575
  if (subscriptionTarget) {
1471
1576
  await serveResourceSubscription(request, reply, subscriptionTarget.uri);
1472
1577
  return;
@@ -1507,13 +1612,26 @@ export function createEmseepea(options) {
1507
1612
  await sendRpcError(reply, 500, -32603, "Internal error");
1508
1613
  }
1509
1614
  });
1510
- app.addHook("onClose", async () => sdkHandler.close());
1615
+ let drainingEvents = false;
1616
+ const eventTimer = eventRuntime ? setInterval(() => {
1617
+ if (drainingEvents || stopping.signal.aborted)
1618
+ return;
1619
+ drainingEvents = true;
1620
+ void eventRuntime.drain().catch(() => { }).finally(() => { drainingEvents = false; });
1621
+ }, 1000) : undefined;
1622
+ eventTimer?.unref();
1623
+ app.addHook("onClose", async () => {
1624
+ if (eventTimer)
1625
+ clearInterval(eventTimer);
1626
+ await sdkHandler.close();
1627
+ });
1511
1628
  runtimes.set(app, {
1512
1629
  deployment,
1513
1630
  requestTimeoutMs: operationTimeoutMs + 5_000,
1514
1631
  stopping,
1515
1632
  observabilityLimits,
1516
1633
  finishObservability,
1634
+ ...(eventRuntime ? { publishMcpEvent: (name, data) => eventRuntime.publish(name, data) } : {}),
1517
1635
  ...(resourceEvents ? {
1518
1636
  notifyResourceUpdated(uri) {
1519
1637
  if (stopping.signal.aborted)
@@ -1534,6 +1652,12 @@ export function notifyResourceUpdated(app, uri) {
1534
1652
  throw new TypeError("notifyResourceUpdated requires resourceSubscriptions");
1535
1653
  notify(uri);
1536
1654
  }
1655
+ export async function publishMcpEvent(app, name, data) {
1656
+ const runtime = runtimes.get(app);
1657
+ if (!runtime?.publishMcpEvent || runtime.stopping.signal.aborted)
1658
+ throw new TypeError("publishMcpEvent requires an active MCP Events server");
1659
+ await runtime.publishMcpEvent(name, data);
1660
+ }
1537
1661
  function safeOAuthError(error) {
1538
1662
  if (!(error instanceof OAuthError))
1539
1663
  return error;