@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/README.md +46 -0
- package/dist/events-webhook.d.ts +8 -0
- package/dist/events-webhook.d.ts.map +1 -0
- package/dist/events-webhook.js +83 -0
- package/dist/events-webhook.js.map +1 -0
- package/dist/events.d.ts +97 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +322 -0
- package/dist/events.js.map +1 -0
- package/dist/external-approval.d.ts +66 -0
- package/dist/external-approval.d.ts.map +1 -0
- package/dist/external-approval.js +215 -0
- package/dist/external-approval.js.map +1 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +1 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -6
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
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 &&
|
|
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
|
-
|
|
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;
|