@emseepea/server 0.3.2 → 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 +84 -82
- package/dist/index.d.ts +70 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +247 -87
- 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,
|
|
@@ -490,7 +496,9 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
490
496
|
assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
|
|
491
497
|
return result;
|
|
492
498
|
}
|
|
493
|
-
if (!isRecord(result) ||
|
|
499
|
+
if (!isRecord(result) ||
|
|
500
|
+
(result.text !== undefined && typeof result.text !== "string") ||
|
|
501
|
+
!("data" in result)) {
|
|
494
502
|
throw new Error("Tool returned an invalid result");
|
|
495
503
|
}
|
|
496
504
|
signal.throwIfAborted();
|
|
@@ -499,7 +507,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
499
507
|
throw new Error("Tool returned output that does not match its schema");
|
|
500
508
|
}
|
|
501
509
|
const publicResult = {
|
|
502
|
-
content: [{ type: "text", text: result.text }],
|
|
510
|
+
content: [{ type: "text", text: result.text ?? JSON.stringify(parsedOutput.data) }],
|
|
503
511
|
structuredContent: parsedOutput.data,
|
|
504
512
|
isError: false,
|
|
505
513
|
};
|
|
@@ -564,12 +572,24 @@ function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
|
564
572
|
},
|
|
565
573
|
async finish() {
|
|
566
574
|
closed = true;
|
|
567
|
-
await Promise.allSettled(
|
|
575
|
+
await Promise.allSettled(pending);
|
|
568
576
|
},
|
|
569
577
|
throwIfFailed() { if (failure)
|
|
570
578
|
throw failure; },
|
|
571
579
|
};
|
|
572
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
|
+
}
|
|
573
593
|
function normalizeListPagination(options) {
|
|
574
594
|
const pageSize = positiveInteger("listPagination.pageSize", options.pageSize);
|
|
575
595
|
if (pageSize > 100)
|
|
@@ -661,9 +681,7 @@ function installListPagination(server, pagination) {
|
|
|
661
681
|
server.server.setRequestHandler("prompts/list", (request) => paginationPage(prompts, request.params?.cursor));
|
|
662
682
|
}
|
|
663
683
|
export function createEmseepea(options) {
|
|
664
|
-
|
|
665
|
-
throw new TypeError("telemetry must be a boolean");
|
|
666
|
-
}
|
|
684
|
+
const observability = normalizeObservability(options.observability);
|
|
667
685
|
const readiness = options.readiness;
|
|
668
686
|
const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
|
|
669
687
|
const stopping = new AbortController();
|
|
@@ -712,16 +730,27 @@ export function createEmseepea(options) {
|
|
|
712
730
|
const maxProgressEventBytes = positiveInteger("maxProgressEventBytes", options.maxProgressEventBytes ?? 8 * 1024);
|
|
713
731
|
const operationTimeoutMs = positiveInteger("operationTimeoutMs", options.operationTimeoutMs ?? 30_000);
|
|
714
732
|
const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
|
|
715
|
-
const
|
|
733
|
+
const authentication = options.authentication
|
|
734
|
+
? normalizeAuthentication(options.authentication)
|
|
735
|
+
: undefined;
|
|
716
736
|
const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
|
|
717
737
|
if (deployment.mode !== "loopback" &&
|
|
718
738
|
tools.some((tool) => tool[TOOL_STREAMING] && tool[TOOL_ACCESS] !== "public")) {
|
|
719
739
|
throw new TypeError("Protected streaming tools currently require the loopback deployment profile");
|
|
720
740
|
}
|
|
721
|
-
if (
|
|
722
|
-
|
|
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");
|
|
723
747
|
}
|
|
724
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]));
|
|
725
754
|
const enabledMethods = new Set(["server/discover"]);
|
|
726
755
|
if (tools.length)
|
|
727
756
|
enabledMethods.add("tools/list").add("tools/call");
|
|
@@ -739,37 +768,43 @@ export function createEmseepea(options) {
|
|
|
739
768
|
const cacheHints = options.cacheHints === undefined
|
|
740
769
|
? undefined
|
|
741
770
|
: normalizeCacheHints(options.cacheHints, enabledMethods);
|
|
742
|
-
const
|
|
743
|
-
? compileListPagination(paginationOptions,
|
|
744
|
-
["tools/list", tools.map((tool) => tool[TOOL_LISTING])],
|
|
745
|
-
["resources/list", resources
|
|
746
|
-
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/list")
|
|
747
|
-
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
748
|
-
["resources/templates/list", resources
|
|
749
|
-
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/templates/list")
|
|
750
|
-
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
751
|
-
["prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING])],
|
|
752
|
-
]))
|
|
771
|
+
const basePagination = paginationOptions
|
|
772
|
+
? compileListPagination(paginationOptions, catalogueListings(tools, resources, prompts))
|
|
753
773
|
: undefined;
|
|
754
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]);
|
|
755
787
|
const server = new McpServer(serverInfo, {
|
|
756
788
|
capabilities: {
|
|
757
|
-
...(
|
|
758
|
-
...(
|
|
759
|
-
...(
|
|
760
|
-
...(
|
|
789
|
+
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
790
|
+
...(activeResources.length ? { resources: { subscribe: false, listChanged: false } } : {}),
|
|
791
|
+
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
792
|
+
...(activeHasCompletion ? { completions: {} } : {}),
|
|
761
793
|
},
|
|
762
794
|
instructions: options.instructions,
|
|
763
|
-
cacheHints,
|
|
795
|
+
cacheHints: request?.filterCatalogues ? undefined : cacheHints,
|
|
764
796
|
supportedProtocolVersions: [PROTOCOL_VERSION],
|
|
765
797
|
});
|
|
766
|
-
for (const tool of
|
|
798
|
+
for (const tool of activeTools) {
|
|
767
799
|
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes);
|
|
768
800
|
}
|
|
769
|
-
for (const resource of
|
|
801
|
+
for (const resource of activeResources)
|
|
770
802
|
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
771
|
-
for (const prompt of
|
|
803
|
+
for (const prompt of activePrompts)
|
|
772
804
|
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
805
|
+
const pagination = request?.filterCatalogues && paginationOptions
|
|
806
|
+
? compileListPagination(paginationOptions, catalogueListings(activeTools, activeResources, activePrompts))
|
|
807
|
+
: basePagination;
|
|
773
808
|
if (pagination)
|
|
774
809
|
installListPagination(server, pagination);
|
|
775
810
|
return server;
|
|
@@ -780,7 +815,10 @@ export function createEmseepea(options) {
|
|
|
780
815
|
});
|
|
781
816
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
782
817
|
const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
|
|
783
|
-
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;
|
|
784
822
|
const limiter = deployment.mode === "production-behind-proxy"
|
|
785
823
|
? new FixedWindowRateLimiter(deployment.rateLimit)
|
|
786
824
|
: undefined;
|
|
@@ -791,11 +829,11 @@ export function createEmseepea(options) {
|
|
|
791
829
|
.code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
|
|
792
830
|
});
|
|
793
831
|
app.addHook("preClose", async () => { stopping.abort(); });
|
|
794
|
-
if (
|
|
832
|
+
if (authentication) {
|
|
795
833
|
app.all("/.well-known/*", async (request, reply) => {
|
|
796
|
-
const response = oauthMetadataResponse(new Request(new URL(request.url,
|
|
834
|
+
const response = oauthMetadataResponse(new Request(new URL(request.url, authentication.metadata.resourceServerUrl.origin), {
|
|
797
835
|
method: request.method,
|
|
798
|
-
}),
|
|
836
|
+
}), authentication.metadata);
|
|
799
837
|
if (!response) {
|
|
800
838
|
await reply.code(404).send();
|
|
801
839
|
return;
|
|
@@ -819,20 +857,32 @@ export function createEmseepea(options) {
|
|
|
819
857
|
await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
|
|
820
858
|
return;
|
|
821
859
|
}
|
|
822
|
-
const access =
|
|
823
|
-
|
|
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")) {
|
|
824
864
|
try {
|
|
825
|
-
|
|
865
|
+
authInfo = await verifyAuthenticatedRequest(request, reply, protectsCatalogue ? [] : access.requiredScopes, authentication);
|
|
826
866
|
request.raw.auth = authInfo;
|
|
827
867
|
}
|
|
828
868
|
catch (error) {
|
|
829
869
|
await sendWebResponse(reply, bearerAuthChallengeResponse(safeOAuthError(error), {
|
|
830
|
-
requiredScopes:
|
|
831
|
-
|
|
870
|
+
requiredScopes: protectsCatalogue
|
|
871
|
+
? []
|
|
872
|
+
: [...access.requiredScopes],
|
|
873
|
+
resourceMetadataUrl: authentication.resourceMetadataUrl,
|
|
832
874
|
}));
|
|
833
875
|
return;
|
|
834
876
|
}
|
|
835
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
|
+
}
|
|
836
886
|
const disconnected = new AbortController();
|
|
837
887
|
const abort = () => disconnected.abort();
|
|
838
888
|
request.raw.once("aborted", abort);
|
|
@@ -841,7 +891,12 @@ export function createEmseepea(options) {
|
|
|
841
891
|
abort();
|
|
842
892
|
reply.hijack();
|
|
843
893
|
try {
|
|
844
|
-
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));
|
|
845
900
|
}
|
|
846
901
|
finally {
|
|
847
902
|
request.raw.off("aborted", abort);
|
|
@@ -860,7 +915,13 @@ export function createEmseepea(options) {
|
|
|
860
915
|
}
|
|
861
916
|
});
|
|
862
917
|
app.addHook("onClose", async () => sdkHandler.close());
|
|
863
|
-
runtimes.set(app, {
|
|
918
|
+
runtimes.set(app, {
|
|
919
|
+
deployment,
|
|
920
|
+
requestTimeoutMs: operationTimeoutMs + 5_000,
|
|
921
|
+
stopping,
|
|
922
|
+
observabilityLimits,
|
|
923
|
+
finishObservability,
|
|
924
|
+
});
|
|
864
925
|
return app;
|
|
865
926
|
}
|
|
866
927
|
function safeOAuthError(error) {
|
|
@@ -875,7 +936,7 @@ function safeOAuthError(error) {
|
|
|
875
936
|
: "Authorization request failed";
|
|
876
937
|
return new OAuthError(error.code, message);
|
|
877
938
|
}
|
|
878
|
-
async function
|
|
939
|
+
async function verifyAuthenticatedRequest(request, reply, requiredScopes, authentication) {
|
|
879
940
|
const disconnected = new AbortController();
|
|
880
941
|
const abort = () => disconnected.abort();
|
|
881
942
|
request.raw.once("aborted", abort);
|
|
@@ -883,14 +944,20 @@ async function verifyProtectedCall(request, reply, access, oauth) {
|
|
|
883
944
|
if (requestAlreadyClosed(request, reply))
|
|
884
945
|
abort();
|
|
885
946
|
try {
|
|
886
|
-
const authInfo = await runWithDeadline(disconnected.signal, Date.now() +
|
|
887
|
-
verifier:
|
|
888
|
-
|
|
889
|
-
|
|
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,
|
|
890
957
|
}));
|
|
891
958
|
if (!authInfo.resource || authInfo.resource.hash || !checkResourceAllowed({
|
|
892
959
|
requestedResource: authInfo.resource,
|
|
893
|
-
configuredResource:
|
|
960
|
+
configuredResource: authentication.metadata.resourceServerUrl,
|
|
894
961
|
})) {
|
|
895
962
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "Token is not valid for this resource");
|
|
896
963
|
}
|
|
@@ -910,21 +977,79 @@ async function sendWebResponse(reply, response) {
|
|
|
910
977
|
const body = Buffer.from(await response.arrayBuffer());
|
|
911
978
|
await reply.code(response.status).send(body.length ? body : undefined);
|
|
912
979
|
}
|
|
913
|
-
function
|
|
914
|
-
if (!isRecord(body) ||
|
|
915
|
-
typeof body.params.name !== "string")
|
|
980
|
+
function capabilityAccessForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName) {
|
|
981
|
+
if (!isRecord(body) || !isRecord(body.params))
|
|
916
982
|
return undefined;
|
|
917
|
-
const
|
|
918
|
-
|
|
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;
|
|
919
1031
|
}
|
|
920
1032
|
function principalFrom(authInfo) {
|
|
921
1033
|
if (!authInfo)
|
|
922
|
-
throw new Error("Protected
|
|
923
|
-
|
|
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({
|
|
924
1043
|
clientId: authInfo.clientId,
|
|
925
|
-
|
|
1044
|
+
permissions,
|
|
926
1045
|
resource: authInfo.resource?.href,
|
|
927
|
-
};
|
|
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));
|
|
928
1053
|
}
|
|
929
1054
|
export async function serveEmseepea(app, options = {}) {
|
|
930
1055
|
const runtime = runtimes.get(app);
|
|
@@ -936,8 +1061,13 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
936
1061
|
}
|
|
937
1062
|
const port = nonNegativePort(options.port ?? 3000);
|
|
938
1063
|
const shutdownTimeoutMs = positiveInteger("shutdownTimeoutMs", options.shutdownTimeoutMs ?? 5_000);
|
|
939
|
-
const
|
|
940
|
-
|
|
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
|
+
}
|
|
941
1071
|
await app.listen({ host, port });
|
|
942
1072
|
app.server.headersTimeout = 10_000;
|
|
943
1073
|
app.server.keepAliveTimeout = 5_000;
|
|
@@ -955,7 +1085,7 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
955
1085
|
url: new URL(`http://${urlHost}:${address.port}/mcp`),
|
|
956
1086
|
close: () => {
|
|
957
1087
|
runtime.stopping.abort();
|
|
958
|
-
return closing ??= closeApp(app, shutdownTimeoutMs,
|
|
1088
|
+
return closing ??= closeApp(app, shutdownTimeoutMs, observabilityFlushTimeoutMs, runtime.finishObservability);
|
|
959
1089
|
},
|
|
960
1090
|
};
|
|
961
1091
|
}
|
|
@@ -1035,26 +1165,34 @@ class FixedWindowRateLimiter {
|
|
|
1035
1165
|
return "allowed";
|
|
1036
1166
|
}
|
|
1037
1167
|
}
|
|
1038
|
-
function
|
|
1168
|
+
function normalizeCapabilityAccess(kind, access, requiredScopes) {
|
|
1039
1169
|
if (access === "public")
|
|
1040
1170
|
return access;
|
|
1041
1171
|
if (access !== "protected" || !Array.isArray(requiredScopes) || requiredScopes.length === 0) {
|
|
1042
|
-
throw new TypeError(
|
|
1172
|
+
throw new TypeError(`${kind} access must be explicitly declared as "public" or protected with scopes`);
|
|
1043
1173
|
}
|
|
1044
1174
|
const scopes = requiredScopes.map((scope) => {
|
|
1045
1175
|
if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
1046
|
-
throw new TypeError(
|
|
1176
|
+
throw new TypeError(`Protected ${kind.toLowerCase()} scopes must be valid OAuth scope tokens`);
|
|
1047
1177
|
}
|
|
1048
1178
|
return scope;
|
|
1049
1179
|
});
|
|
1050
1180
|
if (new Set(scopes).size !== scopes.length) {
|
|
1051
|
-
throw new TypeError(
|
|
1181
|
+
throw new TypeError(`Protected ${kind.toLowerCase()} scopes must be unique`);
|
|
1052
1182
|
}
|
|
1053
1183
|
return { type: "protected", requiredScopes: scopes };
|
|
1054
1184
|
}
|
|
1055
|
-
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) {
|
|
1056
1194
|
if (!options.verifier || typeof options.verifier.verifyAccessToken !== "function") {
|
|
1057
|
-
throw new TypeError("
|
|
1195
|
+
throw new TypeError("authentication.verifier must implement verifyAccessToken");
|
|
1058
1196
|
}
|
|
1059
1197
|
if (options.metadata.dangerouslyAllowInsecureIssuerUrl === true) {
|
|
1060
1198
|
throw new TypeError("Insecure OAuth issuer URLs are not supported");
|
|
@@ -1070,9 +1208,37 @@ function normalizeOAuth(options) {
|
|
|
1070
1208
|
verifier: options.verifier,
|
|
1071
1209
|
metadata: options.metadata,
|
|
1072
1210
|
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(options.metadata.resourceServerUrl),
|
|
1073
|
-
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"'); })(),
|
|
1074
1217
|
};
|
|
1075
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
|
+
}
|
|
1076
1242
|
function isLoopbackUrl(url) {
|
|
1077
1243
|
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
1078
1244
|
}
|
|
@@ -1446,6 +1612,7 @@ function completionCallback(complete, currentName, allowedNames, timeoutMs, maxA
|
|
|
1446
1612
|
const values = await complete(value, {
|
|
1447
1613
|
signal,
|
|
1448
1614
|
deadlineMs,
|
|
1615
|
+
principal: request?.principal,
|
|
1449
1616
|
arguments: completionArguments(context?.arguments, allowedArguments),
|
|
1450
1617
|
});
|
|
1451
1618
|
signal.throwIfAborted();
|
|
@@ -1677,7 +1844,7 @@ function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal)
|
|
|
1677
1844
|
function isLoopbackHost(host) {
|
|
1678
1845
|
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1679
1846
|
}
|
|
1680
|
-
async function closeApp(app, timeoutMs,
|
|
1847
|
+
async function closeApp(app, timeoutMs, flushTimeoutMs, finishObservability) {
|
|
1681
1848
|
let timer;
|
|
1682
1849
|
let onClosed;
|
|
1683
1850
|
const httpClosed = new Promise((resolve) => { onClosed = resolve; });
|
|
@@ -1696,21 +1863,14 @@ async function closeApp(app, timeoutMs, flushTelemetry, flushTimeoutMs, finishTe
|
|
|
1696
1863
|
finally {
|
|
1697
1864
|
if (timer)
|
|
1698
1865
|
clearTimeout(timer);
|
|
1699
|
-
if (
|
|
1700
|
-
const controller = new AbortController();
|
|
1701
|
-
const flushDeadline = Date.now() + flushTimeoutMs;
|
|
1866
|
+
if (finishObservability) {
|
|
1702
1867
|
const expired = new Promise((resolve) => {
|
|
1703
1868
|
// Keep the process alive long enough to finish flushing after sockets close.
|
|
1704
|
-
timer = setTimeout(
|
|
1869
|
+
timer = setTimeout(resolve, flushTimeoutMs);
|
|
1705
1870
|
});
|
|
1706
1871
|
try {
|
|
1707
1872
|
await Promise.race([
|
|
1708
|
-
httpClosed.then(
|
|
1709
|
-
await finishTelemetry?.();
|
|
1710
|
-
if (!controller.signal.aborted && Date.now() < flushDeadline) {
|
|
1711
|
-
await flushTelemetry({ signal: controller.signal });
|
|
1712
|
-
}
|
|
1713
|
-
}).catch(() => { }),
|
|
1873
|
+
httpClosed.then(() => finishObservability(flushTimeoutMs)).catch(() => { }),
|
|
1714
1874
|
expired,
|
|
1715
1875
|
]);
|
|
1716
1876
|
}
|