@emseepea/server 0.3.3 → 0.4.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 +69 -67
- package/dist/index.d.ts +68 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +243 -85
- package/dist/index.js.map +1 -1
- package/dist/telemetry.d.ts +21 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +77 -63
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { isIP } from "node:net";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
-
import {
|
|
12
|
+
import { installObservability, } from "./telemetry.js";
|
|
13
|
+
export { openTelemetry, structuredLogging, } from "./telemetry.js";
|
|
13
14
|
export * from "./ui.js";
|
|
14
15
|
export const inputRequired = Object.assign((spec) => sdkInputRequired({ inputRequests: spec.inputRequests }), {
|
|
15
16
|
elicit: sdkInputRequired.elicit,
|
|
@@ -34,7 +35,9 @@ const RESOURCE_KIND = Symbol("resourceKind");
|
|
|
34
35
|
const RESOURCE_MATCHES = Symbol("resourceMatches");
|
|
35
36
|
const RESOURCE_ROUTE = Symbol("resourceRoute");
|
|
36
37
|
const RESOURCE_LISTING = Symbol("resourceListing");
|
|
38
|
+
const RESOURCE_ACCESS = Symbol("resourceAccess");
|
|
37
39
|
const PROMPT_NAME = Symbol("promptName");
|
|
40
|
+
const PROMPT_ACCESS = Symbol("promptAccess");
|
|
38
41
|
const HAS_COMPLETION = Symbol("hasCompletion");
|
|
39
42
|
const PROMPT_LISTING = Symbol("promptListing");
|
|
40
43
|
const CACHEABLE_METHODS = [
|
|
@@ -86,6 +89,7 @@ export function defineMappedTool(definition) {
|
|
|
86
89
|
export function defineResource(definition) {
|
|
87
90
|
const { name, handler } = definition;
|
|
88
91
|
assertRegistrationName("Resource", name);
|
|
92
|
+
const access = normalizeCapabilityAccess("Resource", definition.access, definition.requiredScopes);
|
|
89
93
|
const uri = canonicalResourceUri(definition.uri);
|
|
90
94
|
const listing = checkedProtocolValue("Resource", {
|
|
91
95
|
name,
|
|
@@ -96,7 +100,7 @@ export function defineResource(definition) {
|
|
|
96
100
|
icons: definition.icons,
|
|
97
101
|
annotations: definition.annotations,
|
|
98
102
|
size: definition.size,
|
|
99
|
-
_meta: definition._meta,
|
|
103
|
+
_meta: accessMetadata(definition._meta, access),
|
|
100
104
|
});
|
|
101
105
|
const metadata = Object.freeze({
|
|
102
106
|
title: listing.title,
|
|
@@ -114,6 +118,7 @@ export function defineResource(definition) {
|
|
|
114
118
|
[RESOURCE_NAME]: name,
|
|
115
119
|
[RESOURCE_URI]: uri,
|
|
116
120
|
[RESOURCE_KIND]: "static",
|
|
121
|
+
[RESOURCE_ACCESS]: access,
|
|
117
122
|
[RESOURCE_LISTING]: Object.freeze({
|
|
118
123
|
method: "resources/list",
|
|
119
124
|
value: listing,
|
|
@@ -128,6 +133,7 @@ export function defineResource(definition) {
|
|
|
128
133
|
const result = await handler({
|
|
129
134
|
signal,
|
|
130
135
|
deadlineMs,
|
|
136
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
131
137
|
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
132
138
|
});
|
|
133
139
|
signal.throwIfAborted();
|
|
@@ -156,6 +162,7 @@ export function defineResource(definition) {
|
|
|
156
162
|
export function defineResourceTemplate(definition) {
|
|
157
163
|
const { name, handler } = definition;
|
|
158
164
|
assertRegistrationName("Resource template", name);
|
|
165
|
+
const access = normalizeCapabilityAccess("Resource template", definition.access, definition.requiredScopes);
|
|
159
166
|
const { template, route } = checkedResourceTemplate(definition.uriTemplate);
|
|
160
167
|
const uriTemplate = template.uriTemplate.toString();
|
|
161
168
|
const listing = checkedProtocolValue("ResourceTemplate", {
|
|
@@ -166,7 +173,7 @@ export function defineResourceTemplate(definition) {
|
|
|
166
173
|
mimeType: definition.mimeType,
|
|
167
174
|
icons: definition.icons,
|
|
168
175
|
annotations: definition.annotations,
|
|
169
|
-
_meta: definition._meta,
|
|
176
|
+
_meta: accessMetadata(definition._meta, access),
|
|
170
177
|
});
|
|
171
178
|
const metadata = Object.freeze({
|
|
172
179
|
title: listing.title,
|
|
@@ -185,6 +192,7 @@ export function defineResourceTemplate(definition) {
|
|
|
185
192
|
[RESOURCE_NAME]: name,
|
|
186
193
|
[RESOURCE_URI]: uriTemplate,
|
|
187
194
|
[RESOURCE_KIND]: "template",
|
|
195
|
+
[RESOURCE_ACCESS]: access,
|
|
188
196
|
[RESOURCE_MATCHES]: (uri) => template.uriTemplate.match(uri) !== null,
|
|
189
197
|
[RESOURCE_ROUTE]: route,
|
|
190
198
|
[RESOURCE_LISTING]: Object.freeze({
|
|
@@ -210,6 +218,7 @@ export function defineResourceTemplate(definition) {
|
|
|
210
218
|
const result = await handler({ uri: requestedUri.href, variables }, {
|
|
211
219
|
signal,
|
|
212
220
|
deadlineMs,
|
|
221
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
213
222
|
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
214
223
|
});
|
|
215
224
|
signal.throwIfAborted();
|
|
@@ -239,6 +248,7 @@ export function defineResourceTemplate(definition) {
|
|
|
239
248
|
export function definePrompt(definition) {
|
|
240
249
|
const { name, argsSchema, handler } = definition;
|
|
241
250
|
assertRegistrationName("Prompt", name);
|
|
251
|
+
const access = normalizeCapabilityAccess("Prompt", definition.access, definition.requiredScopes);
|
|
242
252
|
const argumentNames = Object.keys(argsSchema.shape);
|
|
243
253
|
const completions = checkedCompletionHandlers("Prompt", definition.complete, argumentNames);
|
|
244
254
|
const listing = checkedProtocolValue("Prompt", {
|
|
@@ -246,7 +256,7 @@ export function definePrompt(definition) {
|
|
|
246
256
|
title: definition.title,
|
|
247
257
|
description: definition.description,
|
|
248
258
|
icons: definition.icons,
|
|
249
|
-
_meta: definition._meta,
|
|
259
|
+
_meta: accessMetadata(definition._meta, access),
|
|
250
260
|
arguments: promptArguments(argsSchema),
|
|
251
261
|
});
|
|
252
262
|
const metadata = Object.freeze({
|
|
@@ -257,6 +267,7 @@ export function definePrompt(definition) {
|
|
|
257
267
|
});
|
|
258
268
|
const registration = {
|
|
259
269
|
[PROMPT_NAME]: name,
|
|
270
|
+
[PROMPT_ACCESS]: access,
|
|
260
271
|
[HAS_COMPLETION]: completions.size > 0,
|
|
261
272
|
[PROMPT_LISTING]: listing,
|
|
262
273
|
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
@@ -274,6 +285,7 @@ export function definePrompt(definition) {
|
|
|
274
285
|
const result = await handler(parsedArgs.data, {
|
|
275
286
|
signal,
|
|
276
287
|
deadlineMs,
|
|
288
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
277
289
|
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
278
290
|
});
|
|
279
291
|
signal.throwIfAborted();
|
|
@@ -420,10 +432,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
420
432
|
const { name, inputSchema, outputSchema } = definition;
|
|
421
433
|
assertRegistrationName("Tool", name);
|
|
422
434
|
assertValidMcpHeaderAnnotations(inputSchema);
|
|
423
|
-
const access =
|
|
424
|
-
const publicAccess = access === "public"
|
|
425
|
-
? Object.freeze({ type: "public" })
|
|
426
|
-
: Object.freeze({ type: "protected", requiredScopes: [...access.requiredScopes] });
|
|
435
|
+
const access = normalizeCapabilityAccess("Tool", definition.access, definition.requiredScopes);
|
|
427
436
|
const sdkInputSchema = sdkMetadataSchema(inputSchema);
|
|
428
437
|
const sdkOutputSchema = sdkMetadataSchema(outputSchema);
|
|
429
438
|
const listing = checkedProtocolValue("Tool", {
|
|
@@ -434,10 +443,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
434
443
|
annotations: definition.annotations,
|
|
435
444
|
inputSchema: jsonMetadataSchema(inputSchema, "input"),
|
|
436
445
|
outputSchema: jsonMetadataSchema(outputSchema, "output"),
|
|
437
|
-
_meta:
|
|
438
|
-
...definition._meta,
|
|
439
|
-
"io.emseepea/access": publicAccess,
|
|
440
|
-
},
|
|
446
|
+
_meta: accessMetadata(definition._meta, access),
|
|
441
447
|
});
|
|
442
448
|
const metadata = Object.freeze({
|
|
443
449
|
title: listing.title,
|
|
@@ -566,12 +572,24 @@ function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
|
566
572
|
},
|
|
567
573
|
async finish() {
|
|
568
574
|
closed = true;
|
|
569
|
-
await Promise.allSettled(
|
|
575
|
+
await Promise.allSettled(pending);
|
|
570
576
|
},
|
|
571
577
|
throwIfFailed() { if (failure)
|
|
572
578
|
throw failure; },
|
|
573
579
|
};
|
|
574
580
|
}
|
|
581
|
+
function catalogueListings(tools, resources, prompts) {
|
|
582
|
+
return new Map([
|
|
583
|
+
["tools/list", tools.map((tool) => tool[TOOL_LISTING])],
|
|
584
|
+
["resources/list", resources
|
|
585
|
+
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/list")
|
|
586
|
+
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
587
|
+
["resources/templates/list", resources
|
|
588
|
+
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/templates/list")
|
|
589
|
+
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
590
|
+
["prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING])],
|
|
591
|
+
]);
|
|
592
|
+
}
|
|
575
593
|
function normalizeListPagination(options) {
|
|
576
594
|
const pageSize = positiveInteger("listPagination.pageSize", options.pageSize);
|
|
577
595
|
if (pageSize > 100)
|
|
@@ -663,9 +681,7 @@ function installListPagination(server, pagination) {
|
|
|
663
681
|
server.server.setRequestHandler("prompts/list", (request) => paginationPage(prompts, request.params?.cursor));
|
|
664
682
|
}
|
|
665
683
|
export function createEmseepea(options) {
|
|
666
|
-
|
|
667
|
-
throw new TypeError("telemetry must be a boolean");
|
|
668
|
-
}
|
|
684
|
+
const observability = normalizeObservability(options.observability);
|
|
669
685
|
const readiness = options.readiness;
|
|
670
686
|
const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
|
|
671
687
|
const stopping = new AbortController();
|
|
@@ -714,16 +730,27 @@ export function createEmseepea(options) {
|
|
|
714
730
|
const maxProgressEventBytes = positiveInteger("maxProgressEventBytes", options.maxProgressEventBytes ?? 8 * 1024);
|
|
715
731
|
const operationTimeoutMs = positiveInteger("operationTimeoutMs", options.operationTimeoutMs ?? 30_000);
|
|
716
732
|
const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
|
|
717
|
-
const
|
|
733
|
+
const authentication = options.authentication
|
|
734
|
+
? normalizeAuthentication(options.authentication)
|
|
735
|
+
: undefined;
|
|
718
736
|
const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
|
|
719
737
|
if (deployment.mode !== "loopback" &&
|
|
720
738
|
tools.some((tool) => tool[TOOL_STREAMING] && tool[TOOL_ACCESS] !== "public")) {
|
|
721
739
|
throw new TypeError("Protected streaming tools currently require the loopback deployment profile");
|
|
722
740
|
}
|
|
723
|
-
if (
|
|
724
|
-
|
|
741
|
+
if ([
|
|
742
|
+
...tools.map((tool) => tool[TOOL_ACCESS]),
|
|
743
|
+
...resources.map((resource) => resource[RESOURCE_ACCESS]),
|
|
744
|
+
...prompts.map((prompt) => prompt[PROMPT_ACCESS]),
|
|
745
|
+
].some((access) => access !== "public") && !authentication) {
|
|
746
|
+
throw new TypeError("Protected capabilities require authentication configuration");
|
|
725
747
|
}
|
|
726
748
|
const toolsByName = new Map(tools.map((tool) => [tool[TOOL_NAME], tool]));
|
|
749
|
+
const resourcesByUri = new Map(resources
|
|
750
|
+
.filter((resource) => resource[RESOURCE_KIND] === "static")
|
|
751
|
+
.map((resource) => [resource[RESOURCE_URI], resource]));
|
|
752
|
+
const resourceTemplates = resources.filter((resource) => resource[RESOURCE_KIND] === "template");
|
|
753
|
+
const promptsByName = new Map(prompts.map((prompt) => [prompt[PROMPT_NAME], prompt]));
|
|
727
754
|
const enabledMethods = new Set(["server/discover"]);
|
|
728
755
|
if (tools.length)
|
|
729
756
|
enabledMethods.add("tools/list").add("tools/call");
|
|
@@ -741,37 +768,43 @@ export function createEmseepea(options) {
|
|
|
741
768
|
const cacheHints = options.cacheHints === undefined
|
|
742
769
|
? undefined
|
|
743
770
|
: normalizeCacheHints(options.cacheHints, enabledMethods);
|
|
744
|
-
const
|
|
745
|
-
? compileListPagination(paginationOptions,
|
|
746
|
-
["tools/list", tools.map((tool) => tool[TOOL_LISTING])],
|
|
747
|
-
["resources/list", resources
|
|
748
|
-
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/list")
|
|
749
|
-
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
750
|
-
["resources/templates/list", resources
|
|
751
|
-
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/templates/list")
|
|
752
|
-
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
753
|
-
["prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING])],
|
|
754
|
-
]))
|
|
771
|
+
const basePagination = paginationOptions
|
|
772
|
+
? compileListPagination(paginationOptions, catalogueListings(tools, resources, prompts))
|
|
755
773
|
: undefined;
|
|
756
774
|
const sdkHandler = createMcpHandler(() => {
|
|
775
|
+
const request = requestOperations.getStore();
|
|
776
|
+
const activeTools = request?.filterCatalogues
|
|
777
|
+
? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], request.principal))
|
|
778
|
+
: tools;
|
|
779
|
+
const activeResources = request?.filterCatalogues
|
|
780
|
+
? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], request.principal))
|
|
781
|
+
: resources;
|
|
782
|
+
const activePrompts = request?.filterCatalogues
|
|
783
|
+
? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], request.principal))
|
|
784
|
+
: prompts;
|
|
785
|
+
const activeHasCompletion = activeResources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
786
|
+
activePrompts.some((prompt) => prompt[HAS_COMPLETION]);
|
|
757
787
|
const server = new McpServer(serverInfo, {
|
|
758
788
|
capabilities: {
|
|
759
|
-
...(
|
|
760
|
-
...(
|
|
761
|
-
...(
|
|
762
|
-
...(
|
|
789
|
+
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
790
|
+
...(activeResources.length ? { resources: { subscribe: false, listChanged: false } } : {}),
|
|
791
|
+
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
792
|
+
...(activeHasCompletion ? { completions: {} } : {}),
|
|
763
793
|
},
|
|
764
794
|
instructions: options.instructions,
|
|
765
|
-
cacheHints,
|
|
795
|
+
cacheHints: request?.filterCatalogues ? undefined : cacheHints,
|
|
766
796
|
supportedProtocolVersions: [PROTOCOL_VERSION],
|
|
767
797
|
});
|
|
768
|
-
for (const tool of
|
|
798
|
+
for (const tool of activeTools) {
|
|
769
799
|
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes);
|
|
770
800
|
}
|
|
771
|
-
for (const resource of
|
|
801
|
+
for (const resource of activeResources)
|
|
772
802
|
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
773
|
-
for (const prompt of
|
|
803
|
+
for (const prompt of activePrompts)
|
|
774
804
|
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
805
|
+
const pagination = request?.filterCatalogues && paginationOptions
|
|
806
|
+
? compileListPagination(paginationOptions, catalogueListings(activeTools, activeResources, activePrompts))
|
|
807
|
+
: basePagination;
|
|
775
808
|
if (pagination)
|
|
776
809
|
installListPagination(server, pagination);
|
|
777
810
|
return server;
|
|
@@ -782,7 +815,10 @@ export function createEmseepea(options) {
|
|
|
782
815
|
});
|
|
783
816
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
784
817
|
const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
|
|
785
|
-
const
|
|
818
|
+
const observabilityLimits = observability.length ? { deliveryTimeoutMs: 1_000 } : undefined;
|
|
819
|
+
const finishObservability = observabilityLimits
|
|
820
|
+
? installObservability(app, observability, (body) => capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName), observabilityLimits)
|
|
821
|
+
: undefined;
|
|
786
822
|
const limiter = deployment.mode === "production-behind-proxy"
|
|
787
823
|
? new FixedWindowRateLimiter(deployment.rateLimit)
|
|
788
824
|
: undefined;
|
|
@@ -793,11 +829,11 @@ export function createEmseepea(options) {
|
|
|
793
829
|
.code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
|
|
794
830
|
});
|
|
795
831
|
app.addHook("preClose", async () => { stopping.abort(); });
|
|
796
|
-
if (
|
|
832
|
+
if (authentication) {
|
|
797
833
|
app.all("/.well-known/*", async (request, reply) => {
|
|
798
|
-
const response = oauthMetadataResponse(new Request(new URL(request.url,
|
|
834
|
+
const response = oauthMetadataResponse(new Request(new URL(request.url, authentication.metadata.resourceServerUrl.origin), {
|
|
799
835
|
method: request.method,
|
|
800
|
-
}),
|
|
836
|
+
}), authentication.metadata);
|
|
801
837
|
if (!response) {
|
|
802
838
|
await reply.code(404).send();
|
|
803
839
|
return;
|
|
@@ -821,20 +857,32 @@ export function createEmseepea(options) {
|
|
|
821
857
|
await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
|
|
822
858
|
return;
|
|
823
859
|
}
|
|
824
|
-
const access =
|
|
825
|
-
|
|
860
|
+
const access = capabilityAccessForRequest(request.body, toolsByName, resourcesByUri, resourceTemplates, promptsByName);
|
|
861
|
+
const protectsCatalogue = authentication?.discovery === "protected";
|
|
862
|
+
let authInfo;
|
|
863
|
+
if (authentication && (protectsCatalogue || access && access !== "public")) {
|
|
826
864
|
try {
|
|
827
|
-
|
|
865
|
+
authInfo = await verifyAuthenticatedRequest(request, reply, protectsCatalogue ? [] : access.requiredScopes, authentication);
|
|
828
866
|
request.raw.auth = authInfo;
|
|
829
867
|
}
|
|
830
868
|
catch (error) {
|
|
831
869
|
await sendWebResponse(reply, bearerAuthChallengeResponse(safeOAuthError(error), {
|
|
832
|
-
requiredScopes:
|
|
833
|
-
|
|
870
|
+
requiredScopes: protectsCatalogue
|
|
871
|
+
? []
|
|
872
|
+
: [...access.requiredScopes],
|
|
873
|
+
resourceMetadataUrl: authentication.resourceMetadataUrl,
|
|
834
874
|
}));
|
|
835
875
|
return;
|
|
836
876
|
}
|
|
837
877
|
}
|
|
878
|
+
const principal = authInfo ? principalFrom(authInfo) : undefined;
|
|
879
|
+
if (protectsCatalogue)
|
|
880
|
+
reply.raw.setHeader("cache-control", "private, no-store");
|
|
881
|
+
if (protectsCatalogue && isCapabilityInvocation(request.body) &&
|
|
882
|
+
(access === undefined || !accessAllows(access, principal))) {
|
|
883
|
+
await sendRpcError(reply, 200, -32602, "Capability not found", requestId(isRecord(request.body) ? request.body.id : undefined));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
838
886
|
const disconnected = new AbortController();
|
|
839
887
|
const abort = () => disconnected.abort();
|
|
840
888
|
request.raw.once("aborted", abort);
|
|
@@ -843,7 +891,12 @@ export function createEmseepea(options) {
|
|
|
843
891
|
abort();
|
|
844
892
|
reply.hijack();
|
|
845
893
|
try {
|
|
846
|
-
await requestOperations.run({
|
|
894
|
+
await requestOperations.run({
|
|
895
|
+
deadlineMs: Date.now() + operationTimeoutMs,
|
|
896
|
+
signal: disconnected.signal,
|
|
897
|
+
principal,
|
|
898
|
+
filterCatalogues: protectsCatalogue,
|
|
899
|
+
}, () => nodeHandler(request.raw, reply.raw, request.body));
|
|
847
900
|
}
|
|
848
901
|
finally {
|
|
849
902
|
request.raw.off("aborted", abort);
|
|
@@ -862,7 +915,13 @@ export function createEmseepea(options) {
|
|
|
862
915
|
}
|
|
863
916
|
});
|
|
864
917
|
app.addHook("onClose", async () => sdkHandler.close());
|
|
865
|
-
runtimes.set(app, {
|
|
918
|
+
runtimes.set(app, {
|
|
919
|
+
deployment,
|
|
920
|
+
requestTimeoutMs: operationTimeoutMs + 5_000,
|
|
921
|
+
stopping,
|
|
922
|
+
observabilityLimits,
|
|
923
|
+
finishObservability,
|
|
924
|
+
});
|
|
866
925
|
return app;
|
|
867
926
|
}
|
|
868
927
|
function safeOAuthError(error) {
|
|
@@ -877,7 +936,7 @@ function safeOAuthError(error) {
|
|
|
877
936
|
: "Authorization request failed";
|
|
878
937
|
return new OAuthError(error.code, message);
|
|
879
938
|
}
|
|
880
|
-
async function
|
|
939
|
+
async function verifyAuthenticatedRequest(request, reply, requiredScopes, authentication) {
|
|
881
940
|
const disconnected = new AbortController();
|
|
882
941
|
const abort = () => disconnected.abort();
|
|
883
942
|
request.raw.once("aborted", abort);
|
|
@@ -885,14 +944,20 @@ async function verifyProtectedCall(request, reply, access, oauth) {
|
|
|
885
944
|
if (requestAlreadyClosed(request, reply))
|
|
886
945
|
abort();
|
|
887
946
|
try {
|
|
888
|
-
const authInfo = await runWithDeadline(disconnected.signal, Date.now() +
|
|
889
|
-
verifier:
|
|
890
|
-
|
|
891
|
-
|
|
947
|
+
const authInfo = await runWithDeadline(disconnected.signal, Date.now() + authentication.verificationTimeoutMs, () => verifyBearerToken(singleHeader(request.raw.rawHeaders, "authorization"), {
|
|
948
|
+
verifier: {
|
|
949
|
+
async verifyAccessToken(token) {
|
|
950
|
+
const authInfo = await authentication.verifier.verifyAccessToken(token);
|
|
951
|
+
principalFrom(authInfo);
|
|
952
|
+
return authInfo;
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
requiredScopes: [...requiredScopes],
|
|
956
|
+
resourceMetadataUrl: authentication.resourceMetadataUrl,
|
|
892
957
|
}));
|
|
893
958
|
if (!authInfo.resource || authInfo.resource.hash || !checkResourceAllowed({
|
|
894
959
|
requestedResource: authInfo.resource,
|
|
895
|
-
configuredResource:
|
|
960
|
+
configuredResource: authentication.metadata.resourceServerUrl,
|
|
896
961
|
})) {
|
|
897
962
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "Token is not valid for this resource");
|
|
898
963
|
}
|
|
@@ -912,21 +977,79 @@ async function sendWebResponse(reply, response) {
|
|
|
912
977
|
const body = Buffer.from(await response.arrayBuffer());
|
|
913
978
|
await reply.code(response.status).send(body.length ? body : undefined);
|
|
914
979
|
}
|
|
915
|
-
function
|
|
916
|
-
if (!isRecord(body) ||
|
|
917
|
-
typeof body.params.name !== "string")
|
|
980
|
+
function capabilityAccessForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName) {
|
|
981
|
+
if (!isRecord(body) || !isRecord(body.params))
|
|
918
982
|
return undefined;
|
|
919
|
-
const
|
|
920
|
-
|
|
983
|
+
const params = body.params;
|
|
984
|
+
if (body.method === "tools/call" && typeof params.name === "string") {
|
|
985
|
+
return toolsByName.get(params.name)?.[TOOL_ACCESS];
|
|
986
|
+
}
|
|
987
|
+
if (body.method === "resources/read" && typeof params.uri === "string") {
|
|
988
|
+
return resourcesByUri.get(params.uri)?.[RESOURCE_ACCESS] ?? resourceTemplates.find((resource) => resource[RESOURCE_MATCHES]?.(params.uri))?.[RESOURCE_ACCESS];
|
|
989
|
+
}
|
|
990
|
+
if (body.method === "prompts/get" && typeof params.name === "string") {
|
|
991
|
+
return promptsByName.get(params.name)?.[PROMPT_ACCESS];
|
|
992
|
+
}
|
|
993
|
+
if (body.method === "completion/complete" && isRecord(params.ref)) {
|
|
994
|
+
const reference = params.ref;
|
|
995
|
+
if (reference.type === "ref/prompt" && typeof reference.name === "string") {
|
|
996
|
+
return promptsByName.get(reference.name)?.[PROMPT_ACCESS];
|
|
997
|
+
}
|
|
998
|
+
if (reference.type === "ref/resource" && typeof reference.uri === "string") {
|
|
999
|
+
return resourceTemplates.find((resource) => resource[RESOURCE_URI] === reference.uri)?.[RESOURCE_ACCESS];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return undefined;
|
|
1003
|
+
}
|
|
1004
|
+
function isCapabilityInvocation(body) {
|
|
1005
|
+
return isRecord(body) && (body.method === "tools/call" || body.method === "resources/read" ||
|
|
1006
|
+
body.method === "prompts/get" || body.method === "completion/complete");
|
|
1007
|
+
}
|
|
1008
|
+
function capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName) {
|
|
1009
|
+
if (!isRecord(body) || !isRecord(body.params))
|
|
1010
|
+
return undefined;
|
|
1011
|
+
const params = body.params;
|
|
1012
|
+
if (body.method === "tools/call" && typeof params.name === "string") {
|
|
1013
|
+
return toolsByName.has(params.name) ? params.name : undefined;
|
|
1014
|
+
}
|
|
1015
|
+
if (body.method === "resources/read" && typeof params.uri === "string") {
|
|
1016
|
+
const resource = resourcesByUri.get(params.uri) ?? resourceTemplates.find((candidate) => candidate[RESOURCE_MATCHES]?.(params.uri));
|
|
1017
|
+
return resource?.[RESOURCE_NAME];
|
|
1018
|
+
}
|
|
1019
|
+
if (body.method === "prompts/get" && typeof params.name === "string") {
|
|
1020
|
+
return promptsByName.has(params.name) ? params.name : undefined;
|
|
1021
|
+
}
|
|
1022
|
+
if (body.method === "completion/complete" && isRecord(params.ref)) {
|
|
1023
|
+
const reference = params.ref;
|
|
1024
|
+
return reference.type === "ref/prompt" && typeof reference.name === "string"
|
|
1025
|
+
? (promptsByName.has(reference.name) ? reference.name : undefined)
|
|
1026
|
+
: reference.type === "ref/resource" && typeof reference.uri === "string"
|
|
1027
|
+
? resourceTemplates.find((candidate) => candidate[RESOURCE_URI] === reference.uri)?.[RESOURCE_NAME]
|
|
1028
|
+
: undefined;
|
|
1029
|
+
}
|
|
1030
|
+
return undefined;
|
|
921
1031
|
}
|
|
922
1032
|
function principalFrom(authInfo) {
|
|
923
1033
|
if (!authInfo)
|
|
924
|
-
throw new Error("Protected
|
|
925
|
-
|
|
1034
|
+
throw new Error("Protected capability reached execution without verified authorization");
|
|
1035
|
+
if (typeof authInfo.clientId !== "string" || !authInfo.clientId.trim() || authInfo.clientId.length > 256) {
|
|
1036
|
+
throw new TypeError("Authentication verifier returned an invalid clientId");
|
|
1037
|
+
}
|
|
1038
|
+
if (!Array.isArray(authInfo.scopes) || authInfo.scopes.some((scope) => typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) || new Set(authInfo.scopes).size !== authInfo.scopes.length) {
|
|
1039
|
+
throw new TypeError("Authentication verifier returned invalid permissions");
|
|
1040
|
+
}
|
|
1041
|
+
const permissions = Object.freeze([...authInfo.scopes]);
|
|
1042
|
+
return Object.freeze({
|
|
926
1043
|
clientId: authInfo.clientId,
|
|
927
|
-
|
|
1044
|
+
permissions,
|
|
928
1045
|
resource: authInfo.resource?.href,
|
|
929
|
-
};
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function accessAllows(access, principal) {
|
|
1049
|
+
if (access === "public")
|
|
1050
|
+
return true;
|
|
1051
|
+
return principal !== undefined &&
|
|
1052
|
+
access.requiredScopes.every((permission) => principal.permissions.includes(permission));
|
|
930
1053
|
}
|
|
931
1054
|
export async function serveEmseepea(app, options = {}) {
|
|
932
1055
|
const runtime = runtimes.get(app);
|
|
@@ -938,8 +1061,13 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
938
1061
|
}
|
|
939
1062
|
const port = nonNegativePort(options.port ?? 3000);
|
|
940
1063
|
const shutdownTimeoutMs = positiveInteger("shutdownTimeoutMs", options.shutdownTimeoutMs ?? 5_000);
|
|
941
|
-
const
|
|
942
|
-
|
|
1064
|
+
const observabilityFlushTimeoutMs = positiveInteger("observabilityFlushTimeoutMs", options.observabilityFlushTimeoutMs === undefined ? 1_000 : options.observabilityFlushTimeoutMs);
|
|
1065
|
+
if (observabilityFlushTimeoutMs > 60_000) {
|
|
1066
|
+
throw new TypeError("observabilityFlushTimeoutMs must not exceed 60000");
|
|
1067
|
+
}
|
|
1068
|
+
if (runtime.observabilityLimits) {
|
|
1069
|
+
runtime.observabilityLimits.deliveryTimeoutMs = observabilityFlushTimeoutMs;
|
|
1070
|
+
}
|
|
943
1071
|
await app.listen({ host, port });
|
|
944
1072
|
app.server.headersTimeout = 10_000;
|
|
945
1073
|
app.server.keepAliveTimeout = 5_000;
|
|
@@ -957,7 +1085,7 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
957
1085
|
url: new URL(`http://${urlHost}:${address.port}/mcp`),
|
|
958
1086
|
close: () => {
|
|
959
1087
|
runtime.stopping.abort();
|
|
960
|
-
return closing ??= closeApp(app, shutdownTimeoutMs,
|
|
1088
|
+
return closing ??= closeApp(app, shutdownTimeoutMs, observabilityFlushTimeoutMs, runtime.finishObservability);
|
|
961
1089
|
},
|
|
962
1090
|
};
|
|
963
1091
|
}
|
|
@@ -1037,26 +1165,34 @@ class FixedWindowRateLimiter {
|
|
|
1037
1165
|
return "allowed";
|
|
1038
1166
|
}
|
|
1039
1167
|
}
|
|
1040
|
-
function
|
|
1168
|
+
function normalizeCapabilityAccess(kind, access, requiredScopes) {
|
|
1041
1169
|
if (access === "public")
|
|
1042
1170
|
return access;
|
|
1043
1171
|
if (access !== "protected" || !Array.isArray(requiredScopes) || requiredScopes.length === 0) {
|
|
1044
|
-
throw new TypeError(
|
|
1172
|
+
throw new TypeError(`${kind} access must be explicitly declared as "public" or protected with scopes`);
|
|
1045
1173
|
}
|
|
1046
1174
|
const scopes = requiredScopes.map((scope) => {
|
|
1047
1175
|
if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
1048
|
-
throw new TypeError(
|
|
1176
|
+
throw new TypeError(`Protected ${kind.toLowerCase()} scopes must be valid OAuth scope tokens`);
|
|
1049
1177
|
}
|
|
1050
1178
|
return scope;
|
|
1051
1179
|
});
|
|
1052
1180
|
if (new Set(scopes).size !== scopes.length) {
|
|
1053
|
-
throw new TypeError(
|
|
1181
|
+
throw new TypeError(`Protected ${kind.toLowerCase()} scopes must be unique`);
|
|
1054
1182
|
}
|
|
1055
1183
|
return { type: "protected", requiredScopes: scopes };
|
|
1056
1184
|
}
|
|
1057
|
-
function
|
|
1185
|
+
function accessMetadata(metadata, access) {
|
|
1186
|
+
return Object.freeze({
|
|
1187
|
+
...metadata,
|
|
1188
|
+
"io.emseepea/access": access === "public"
|
|
1189
|
+
? Object.freeze({ type: "public" })
|
|
1190
|
+
: Object.freeze({ type: "protected", requiredScopes: [...access.requiredScopes] }),
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function normalizeAuthentication(options) {
|
|
1058
1194
|
if (!options.verifier || typeof options.verifier.verifyAccessToken !== "function") {
|
|
1059
|
-
throw new TypeError("
|
|
1195
|
+
throw new TypeError("authentication.verifier must implement verifyAccessToken");
|
|
1060
1196
|
}
|
|
1061
1197
|
if (options.metadata.dangerouslyAllowInsecureIssuerUrl === true) {
|
|
1062
1198
|
throw new TypeError("Insecure OAuth issuer URLs are not supported");
|
|
@@ -1072,9 +1208,37 @@ function normalizeOAuth(options) {
|
|
|
1072
1208
|
verifier: options.verifier,
|
|
1073
1209
|
metadata: options.metadata,
|
|
1074
1210
|
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(options.metadata.resourceServerUrl),
|
|
1075
|
-
verificationTimeoutMs: positiveInteger("
|
|
1211
|
+
verificationTimeoutMs: positiveInteger("authentication.verificationTimeoutMs", options.verificationTimeoutMs ?? 10_000),
|
|
1212
|
+
discovery: options.discovery === undefined || options.discovery === "public"
|
|
1213
|
+
? "public"
|
|
1214
|
+
: options.discovery === "protected"
|
|
1215
|
+
? "protected"
|
|
1216
|
+
: (() => { throw new TypeError('authentication.discovery must be "public" or "protected"'); })(),
|
|
1076
1217
|
};
|
|
1077
1218
|
}
|
|
1219
|
+
function normalizeObservability(adapters) {
|
|
1220
|
+
if (adapters === undefined)
|
|
1221
|
+
return [];
|
|
1222
|
+
if (!Array.isArray(adapters))
|
|
1223
|
+
throw new TypeError("observability must be an array");
|
|
1224
|
+
const ids = new Set();
|
|
1225
|
+
return Object.freeze(adapters.map((adapter) => {
|
|
1226
|
+
if (!adapter || typeof adapter !== "object" ||
|
|
1227
|
+
typeof adapter.id !== "string" || !/^[A-Za-z0-9_.-]{1,64}$/.test(adapter.id) ||
|
|
1228
|
+
typeof adapter.emit !== "function" ||
|
|
1229
|
+
adapter.flush !== undefined && typeof adapter.flush !== "function") {
|
|
1230
|
+
throw new TypeError("observability adapters require a valid id, emit function, and optional flush function");
|
|
1231
|
+
}
|
|
1232
|
+
if (ids.has(adapter.id))
|
|
1233
|
+
throw new TypeError(`Duplicate observability adapter id: ${adapter.id}`);
|
|
1234
|
+
ids.add(adapter.id);
|
|
1235
|
+
return Object.freeze({
|
|
1236
|
+
id: adapter.id,
|
|
1237
|
+
emit: adapter.emit,
|
|
1238
|
+
...(adapter.flush ? { flush: adapter.flush } : {}),
|
|
1239
|
+
});
|
|
1240
|
+
}));
|
|
1241
|
+
}
|
|
1078
1242
|
function isLoopbackUrl(url) {
|
|
1079
1243
|
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
1080
1244
|
}
|
|
@@ -1448,6 +1612,7 @@ function completionCallback(complete, currentName, allowedNames, timeoutMs, maxA
|
|
|
1448
1612
|
const values = await complete(value, {
|
|
1449
1613
|
signal,
|
|
1450
1614
|
deadlineMs,
|
|
1615
|
+
principal: request?.principal,
|
|
1451
1616
|
arguments: completionArguments(context?.arguments, allowedArguments),
|
|
1452
1617
|
});
|
|
1453
1618
|
signal.throwIfAborted();
|
|
@@ -1679,7 +1844,7 @@ function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal)
|
|
|
1679
1844
|
function isLoopbackHost(host) {
|
|
1680
1845
|
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1681
1846
|
}
|
|
1682
|
-
async function closeApp(app, timeoutMs,
|
|
1847
|
+
async function closeApp(app, timeoutMs, flushTimeoutMs, finishObservability) {
|
|
1683
1848
|
let timer;
|
|
1684
1849
|
let onClosed;
|
|
1685
1850
|
const httpClosed = new Promise((resolve) => { onClosed = resolve; });
|
|
@@ -1698,21 +1863,14 @@ async function closeApp(app, timeoutMs, flushTelemetry, flushTimeoutMs, finishTe
|
|
|
1698
1863
|
finally {
|
|
1699
1864
|
if (timer)
|
|
1700
1865
|
clearTimeout(timer);
|
|
1701
|
-
if (
|
|
1702
|
-
const controller = new AbortController();
|
|
1703
|
-
const flushDeadline = Date.now() + flushTimeoutMs;
|
|
1866
|
+
if (finishObservability) {
|
|
1704
1867
|
const expired = new Promise((resolve) => {
|
|
1705
1868
|
// Keep the process alive long enough to finish flushing after sockets close.
|
|
1706
|
-
timer = setTimeout(
|
|
1869
|
+
timer = setTimeout(resolve, flushTimeoutMs);
|
|
1707
1870
|
});
|
|
1708
1871
|
try {
|
|
1709
1872
|
await Promise.race([
|
|
1710
|
-
httpClosed.then(
|
|
1711
|
-
await finishTelemetry?.();
|
|
1712
|
-
if (!controller.signal.aborted && Date.now() < flushDeadline) {
|
|
1713
|
-
await flushTelemetry({ signal: controller.signal });
|
|
1714
|
-
}
|
|
1715
|
-
}).catch(() => { }),
|
|
1873
|
+
httpClosed.then(() => finishObservability(flushTimeoutMs)).catch(() => { }),
|
|
1716
1874
|
expired,
|
|
1717
1875
|
]);
|
|
1718
1876
|
}
|