@emseepea/server 0.3.3 → 0.6.1
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 +120 -65
- package/dist/index.d.ts +78 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +306 -102
- 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,
|
|
@@ -24,6 +25,7 @@ export function inputResponse(responses, key) {
|
|
|
24
25
|
}
|
|
25
26
|
const PROTOCOL_VERSION = "2026-07-28";
|
|
26
27
|
const REGISTER = Symbol("register");
|
|
28
|
+
const DISCOVERABLE = Symbol("discoverable");
|
|
27
29
|
const TOOL_NAME = Symbol("toolName");
|
|
28
30
|
const TOOL_ACCESS = Symbol("toolAccess");
|
|
29
31
|
const TOOL_STREAMING = Symbol("toolStreaming");
|
|
@@ -34,7 +36,9 @@ const RESOURCE_KIND = Symbol("resourceKind");
|
|
|
34
36
|
const RESOURCE_MATCHES = Symbol("resourceMatches");
|
|
35
37
|
const RESOURCE_ROUTE = Symbol("resourceRoute");
|
|
36
38
|
const RESOURCE_LISTING = Symbol("resourceListing");
|
|
39
|
+
const RESOURCE_ACCESS = Symbol("resourceAccess");
|
|
37
40
|
const PROMPT_NAME = Symbol("promptName");
|
|
41
|
+
const PROMPT_ACCESS = Symbol("promptAccess");
|
|
38
42
|
const HAS_COMPLETION = Symbol("hasCompletion");
|
|
39
43
|
const PROMPT_LISTING = Symbol("promptListing");
|
|
40
44
|
const CACHEABLE_METHODS = [
|
|
@@ -86,6 +90,7 @@ export function defineMappedTool(definition) {
|
|
|
86
90
|
export function defineResource(definition) {
|
|
87
91
|
const { name, handler } = definition;
|
|
88
92
|
assertRegistrationName("Resource", name);
|
|
93
|
+
const access = normalizeCapabilityAccess("Resource", definition.access, definition.requiredScopes);
|
|
89
94
|
const uri = canonicalResourceUri(definition.uri);
|
|
90
95
|
const listing = checkedProtocolValue("Resource", {
|
|
91
96
|
name,
|
|
@@ -96,7 +101,7 @@ export function defineResource(definition) {
|
|
|
96
101
|
icons: definition.icons,
|
|
97
102
|
annotations: definition.annotations,
|
|
98
103
|
size: definition.size,
|
|
99
|
-
_meta: definition._meta,
|
|
104
|
+
_meta: accessMetadata(definition._meta, access),
|
|
100
105
|
});
|
|
101
106
|
const metadata = Object.freeze({
|
|
102
107
|
title: listing.title,
|
|
@@ -111,9 +116,11 @@ export function defineResource(definition) {
|
|
|
111
116
|
? undefined
|
|
112
117
|
: normalizeCacheHint(definition.cacheHint, `resource ${name}`);
|
|
113
118
|
const registration = {
|
|
119
|
+
[DISCOVERABLE]: normalizeDiscoverable("Resource", definition.discoverable),
|
|
114
120
|
[RESOURCE_NAME]: name,
|
|
115
121
|
[RESOURCE_URI]: uri,
|
|
116
122
|
[RESOURCE_KIND]: "static",
|
|
123
|
+
[RESOURCE_ACCESS]: access,
|
|
117
124
|
[RESOURCE_LISTING]: Object.freeze({
|
|
118
125
|
method: "resources/list",
|
|
119
126
|
value: listing,
|
|
@@ -128,6 +135,7 @@ export function defineResource(definition) {
|
|
|
128
135
|
const result = await handler({
|
|
129
136
|
signal,
|
|
130
137
|
deadlineMs,
|
|
138
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
131
139
|
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
132
140
|
});
|
|
133
141
|
signal.throwIfAborted();
|
|
@@ -156,6 +164,7 @@ export function defineResource(definition) {
|
|
|
156
164
|
export function defineResourceTemplate(definition) {
|
|
157
165
|
const { name, handler } = definition;
|
|
158
166
|
assertRegistrationName("Resource template", name);
|
|
167
|
+
const access = normalizeCapabilityAccess("Resource template", definition.access, definition.requiredScopes);
|
|
159
168
|
const { template, route } = checkedResourceTemplate(definition.uriTemplate);
|
|
160
169
|
const uriTemplate = template.uriTemplate.toString();
|
|
161
170
|
const listing = checkedProtocolValue("ResourceTemplate", {
|
|
@@ -166,7 +175,7 @@ export function defineResourceTemplate(definition) {
|
|
|
166
175
|
mimeType: definition.mimeType,
|
|
167
176
|
icons: definition.icons,
|
|
168
177
|
annotations: definition.annotations,
|
|
169
|
-
_meta: definition._meta,
|
|
178
|
+
_meta: accessMetadata(definition._meta, access),
|
|
170
179
|
});
|
|
171
180
|
const metadata = Object.freeze({
|
|
172
181
|
title: listing.title,
|
|
@@ -182,9 +191,11 @@ export function defineResourceTemplate(definition) {
|
|
|
182
191
|
const variableNames = [...template.uriTemplate.variableNames];
|
|
183
192
|
const completions = checkedCompletionHandlers("Resource template", definition.complete, variableNames);
|
|
184
193
|
const registration = {
|
|
194
|
+
[DISCOVERABLE]: normalizeDiscoverable("Resource template", definition.discoverable),
|
|
185
195
|
[RESOURCE_NAME]: name,
|
|
186
196
|
[RESOURCE_URI]: uriTemplate,
|
|
187
197
|
[RESOURCE_KIND]: "template",
|
|
198
|
+
[RESOURCE_ACCESS]: access,
|
|
188
199
|
[RESOURCE_MATCHES]: (uri) => template.uriTemplate.match(uri) !== null,
|
|
189
200
|
[RESOURCE_ROUTE]: route,
|
|
190
201
|
[RESOURCE_LISTING]: Object.freeze({
|
|
@@ -210,6 +221,7 @@ export function defineResourceTemplate(definition) {
|
|
|
210
221
|
const result = await handler({ uri: requestedUri.href, variables }, {
|
|
211
222
|
signal,
|
|
212
223
|
deadlineMs,
|
|
224
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
213
225
|
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
214
226
|
});
|
|
215
227
|
signal.throwIfAborted();
|
|
@@ -239,6 +251,7 @@ export function defineResourceTemplate(definition) {
|
|
|
239
251
|
export function definePrompt(definition) {
|
|
240
252
|
const { name, argsSchema, handler } = definition;
|
|
241
253
|
assertRegistrationName("Prompt", name);
|
|
254
|
+
const access = normalizeCapabilityAccess("Prompt", definition.access, definition.requiredScopes);
|
|
242
255
|
const argumentNames = Object.keys(argsSchema.shape);
|
|
243
256
|
const completions = checkedCompletionHandlers("Prompt", definition.complete, argumentNames);
|
|
244
257
|
const listing = checkedProtocolValue("Prompt", {
|
|
@@ -246,7 +259,7 @@ export function definePrompt(definition) {
|
|
|
246
259
|
title: definition.title,
|
|
247
260
|
description: definition.description,
|
|
248
261
|
icons: definition.icons,
|
|
249
|
-
_meta: definition._meta,
|
|
262
|
+
_meta: accessMetadata(definition._meta, access),
|
|
250
263
|
arguments: promptArguments(argsSchema),
|
|
251
264
|
});
|
|
252
265
|
const metadata = Object.freeze({
|
|
@@ -256,7 +269,9 @@ export function definePrompt(definition) {
|
|
|
256
269
|
_meta: listing._meta,
|
|
257
270
|
});
|
|
258
271
|
const registration = {
|
|
272
|
+
[DISCOVERABLE]: normalizeDiscoverable("Prompt", definition.discoverable),
|
|
259
273
|
[PROMPT_NAME]: name,
|
|
274
|
+
[PROMPT_ACCESS]: access,
|
|
260
275
|
[HAS_COMPLETION]: completions.size > 0,
|
|
261
276
|
[PROMPT_LISTING]: listing,
|
|
262
277
|
[REGISTER](server, timeoutMs, maxApplicationResultBytes) {
|
|
@@ -274,6 +289,7 @@ export function definePrompt(definition) {
|
|
|
274
289
|
const result = await handler(parsedArgs.data, {
|
|
275
290
|
signal,
|
|
276
291
|
deadlineMs,
|
|
292
|
+
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
277
293
|
inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
|
|
278
294
|
});
|
|
279
295
|
signal.throwIfAborted();
|
|
@@ -420,10 +436,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
420
436
|
const { name, inputSchema, outputSchema } = definition;
|
|
421
437
|
assertRegistrationName("Tool", name);
|
|
422
438
|
assertValidMcpHeaderAnnotations(inputSchema);
|
|
423
|
-
const access =
|
|
424
|
-
const publicAccess = access === "public"
|
|
425
|
-
? Object.freeze({ type: "public" })
|
|
426
|
-
: Object.freeze({ type: "protected", requiredScopes: [...access.requiredScopes] });
|
|
439
|
+
const access = normalizeCapabilityAccess("Tool", definition.access, definition.requiredScopes);
|
|
427
440
|
const sdkInputSchema = sdkMetadataSchema(inputSchema);
|
|
428
441
|
const sdkOutputSchema = sdkMetadataSchema(outputSchema);
|
|
429
442
|
const listing = checkedProtocolValue("Tool", {
|
|
@@ -434,10 +447,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
434
447
|
annotations: definition.annotations,
|
|
435
448
|
inputSchema: jsonMetadataSchema(inputSchema, "input"),
|
|
436
449
|
outputSchema: jsonMetadataSchema(outputSchema, "output"),
|
|
437
|
-
_meta:
|
|
438
|
-
...definition._meta,
|
|
439
|
-
"io.emseepea/access": publicAccess,
|
|
440
|
-
},
|
|
450
|
+
_meta: accessMetadata(definition._meta, access),
|
|
441
451
|
});
|
|
442
452
|
const metadata = Object.freeze({
|
|
443
453
|
title: listing.title,
|
|
@@ -449,6 +459,7 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
449
459
|
_meta: listing._meta,
|
|
450
460
|
});
|
|
451
461
|
const registration = {
|
|
462
|
+
[DISCOVERABLE]: normalizeDiscoverable("Tool", definition.discoverable),
|
|
452
463
|
[TOOL_NAME]: name,
|
|
453
464
|
[TOOL_ACCESS]: access,
|
|
454
465
|
[TOOL_STREAMING]: streaming,
|
|
@@ -566,12 +577,30 @@ function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
|
566
577
|
},
|
|
567
578
|
async finish() {
|
|
568
579
|
closed = true;
|
|
569
|
-
await Promise.allSettled(
|
|
580
|
+
await Promise.allSettled(pending);
|
|
570
581
|
},
|
|
571
582
|
throwIfFailed() { if (failure)
|
|
572
583
|
throw failure; },
|
|
573
584
|
};
|
|
574
585
|
}
|
|
586
|
+
function catalogueListings(tools, resources, prompts, supportedTools = tools, supportedResources = resources, supportedPrompts = prompts) {
|
|
587
|
+
const catalogues = new Map();
|
|
588
|
+
if (supportedTools.length)
|
|
589
|
+
catalogues.set("tools/list", tools.map((tool) => tool[TOOL_LISTING]));
|
|
590
|
+
if (supportedResources.some((resource) => resource[RESOURCE_KIND] === "static")) {
|
|
591
|
+
catalogues.set("resources/list", resources
|
|
592
|
+
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/list")
|
|
593
|
+
.map((resource) => resource[RESOURCE_LISTING].value));
|
|
594
|
+
}
|
|
595
|
+
if (supportedResources.some((resource) => resource[RESOURCE_KIND] === "template")) {
|
|
596
|
+
catalogues.set("resources/templates/list", resources
|
|
597
|
+
.filter((resource) => resource[RESOURCE_LISTING].method === "resources/templates/list")
|
|
598
|
+
.map((resource) => resource[RESOURCE_LISTING].value));
|
|
599
|
+
}
|
|
600
|
+
if (supportedPrompts.length)
|
|
601
|
+
catalogues.set("prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING]));
|
|
602
|
+
return catalogues;
|
|
603
|
+
}
|
|
575
604
|
function normalizeListPagination(options) {
|
|
576
605
|
const pageSize = positiveInteger("listPagination.pageSize", options.pageSize);
|
|
577
606
|
if (pageSize > 100)
|
|
@@ -584,21 +613,12 @@ function normalizeListPagination(options) {
|
|
|
584
613
|
function compileListPagination(options, catalogues) {
|
|
585
614
|
const compiled = new Map();
|
|
586
615
|
for (const [method, entries] of catalogues) {
|
|
587
|
-
|
|
588
|
-
continue;
|
|
589
|
-
const resultKey = method === "tools/list"
|
|
590
|
-
? "tools"
|
|
591
|
-
: method === "prompts/list"
|
|
592
|
-
? "prompts"
|
|
593
|
-
: method === "resources/templates/list"
|
|
594
|
-
? "resourceTemplates"
|
|
595
|
-
: "resources";
|
|
596
|
-
compiled.set(method, compileCataloguePages(method, resultKey, entries, options));
|
|
616
|
+
compiled.set(method, compileCataloguePages(method, listResultKey(method), entries, options));
|
|
597
617
|
}
|
|
598
618
|
return compiled;
|
|
599
619
|
}
|
|
600
620
|
function compileCataloguePages(method, resultKey, entries, options) {
|
|
601
|
-
const groups = [];
|
|
621
|
+
const groups = entries.length ? [] : [[]];
|
|
602
622
|
for (let index = 0; index < entries.length;) {
|
|
603
623
|
const group = [];
|
|
604
624
|
while (group.length < options.pageSize && index < entries.length) {
|
|
@@ -634,6 +654,21 @@ function compileCataloguePages(method, resultKey, entries, options) {
|
|
|
634
654
|
}
|
|
635
655
|
return Object.freeze({ first: pages[0], byCursor });
|
|
636
656
|
}
|
|
657
|
+
function compileUnpaginatedCatalogue(catalogues) {
|
|
658
|
+
return new Map([...catalogues].map(([method, entries]) => [method, Object.freeze({
|
|
659
|
+
first: Object.freeze({
|
|
660
|
+
[listResultKey(method)]: Object.freeze([...entries]),
|
|
661
|
+
}),
|
|
662
|
+
byCursor: new Map(),
|
|
663
|
+
})]));
|
|
664
|
+
}
|
|
665
|
+
function listResultKey(method) {
|
|
666
|
+
if (method === "tools/list")
|
|
667
|
+
return "tools";
|
|
668
|
+
if (method === "prompts/list")
|
|
669
|
+
return "prompts";
|
|
670
|
+
return method === "resources/templates/list" ? "resourceTemplates" : "resources";
|
|
671
|
+
}
|
|
637
672
|
function cataloguePageBytes(resultKey, entries, hasNext) {
|
|
638
673
|
return Buffer.byteLength(JSON.stringify({
|
|
639
674
|
[resultKey]: entries,
|
|
@@ -663,9 +698,7 @@ function installListPagination(server, pagination) {
|
|
|
663
698
|
server.server.setRequestHandler("prompts/list", (request) => paginationPage(prompts, request.params?.cursor));
|
|
664
699
|
}
|
|
665
700
|
export function createEmseepea(options) {
|
|
666
|
-
|
|
667
|
-
throw new TypeError("telemetry must be a boolean");
|
|
668
|
-
}
|
|
701
|
+
const observability = normalizeObservability(options.observability);
|
|
669
702
|
const readiness = options.readiness;
|
|
670
703
|
const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
|
|
671
704
|
const stopping = new AbortController();
|
|
@@ -699,9 +732,12 @@ export function createEmseepea(options) {
|
|
|
699
732
|
icons: options.icons,
|
|
700
733
|
websiteUrl: options.websiteUrl,
|
|
701
734
|
});
|
|
702
|
-
const tools = Object.freeze([...(options.tools ?? [])]);
|
|
735
|
+
const tools = Object.freeze([...(options.tools ?? []), ...(options.additionalTools ?? [])]);
|
|
703
736
|
const resources = Object.freeze([...(options.resources ?? [])]);
|
|
704
737
|
const prompts = Object.freeze([...(options.prompts ?? [])]);
|
|
738
|
+
const discoverableTools = Object.freeze(tools.filter((tool) => tool[DISCOVERABLE]));
|
|
739
|
+
const discoverableResources = Object.freeze(resources.filter((resource) => resource[DISCOVERABLE]));
|
|
740
|
+
const discoverablePrompts = Object.freeze(prompts.filter((prompt) => prompt[DISCOVERABLE]));
|
|
705
741
|
assertUniqueToolNames(tools);
|
|
706
742
|
assertUniqueResources(resources);
|
|
707
743
|
assertUniquePromptNames(prompts);
|
|
@@ -714,16 +750,23 @@ export function createEmseepea(options) {
|
|
|
714
750
|
const maxProgressEventBytes = positiveInteger("maxProgressEventBytes", options.maxProgressEventBytes ?? 8 * 1024);
|
|
715
751
|
const operationTimeoutMs = positiveInteger("operationTimeoutMs", options.operationTimeoutMs ?? 30_000);
|
|
716
752
|
const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
|
|
717
|
-
const
|
|
753
|
+
const authentication = options.authentication
|
|
754
|
+
? normalizeAuthentication(options.authentication)
|
|
755
|
+
: undefined;
|
|
718
756
|
const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
|
|
719
|
-
if (
|
|
720
|
-
tools.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
throw new TypeError("Protected
|
|
757
|
+
if ([
|
|
758
|
+
...tools.map((tool) => tool[TOOL_ACCESS]),
|
|
759
|
+
...resources.map((resource) => resource[RESOURCE_ACCESS]),
|
|
760
|
+
...prompts.map((prompt) => prompt[PROMPT_ACCESS]),
|
|
761
|
+
].some((access) => access !== "public") && !authentication) {
|
|
762
|
+
throw new TypeError("Protected capabilities require authentication configuration");
|
|
725
763
|
}
|
|
726
764
|
const toolsByName = new Map(tools.map((tool) => [tool[TOOL_NAME], tool]));
|
|
765
|
+
const resourcesByUri = new Map(resources
|
|
766
|
+
.filter((resource) => resource[RESOURCE_KIND] === "static")
|
|
767
|
+
.map((resource) => [resource[RESOURCE_URI], resource]));
|
|
768
|
+
const resourceTemplates = resources.filter((resource) => resource[RESOURCE_KIND] === "template");
|
|
769
|
+
const promptsByName = new Map(prompts.map((prompt) => [prompt[PROMPT_NAME], prompt]));
|
|
727
770
|
const enabledMethods = new Set(["server/discover"]);
|
|
728
771
|
if (tools.length)
|
|
729
772
|
enabledMethods.add("tools/list").add("tools/call");
|
|
@@ -741,37 +784,66 @@ export function createEmseepea(options) {
|
|
|
741
784
|
const cacheHints = options.cacheHints === undefined
|
|
742
785
|
? undefined
|
|
743
786
|
: normalizeCacheHints(options.cacheHints, enabledMethods);
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
.map((resource) => resource[RESOURCE_LISTING].value)],
|
|
753
|
-
["prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING])],
|
|
754
|
-
]))
|
|
755
|
-
: undefined;
|
|
787
|
+
const baseCatalogues = catalogueListings(discoverableTools, discoverableResources, discoverablePrompts, tools, resources, prompts);
|
|
788
|
+
const hasSuppressedCapabilities = discoverableTools.length !== tools.length ||
|
|
789
|
+
discoverableResources.length !== resources.length || discoverablePrompts.length !== prompts.length;
|
|
790
|
+
const basePagination = paginationOptions
|
|
791
|
+
? compileListPagination(paginationOptions, baseCatalogues)
|
|
792
|
+
: hasSuppressedCapabilities
|
|
793
|
+
? compileUnpaginatedCatalogue(baseCatalogues)
|
|
794
|
+
: undefined;
|
|
756
795
|
const sdkHandler = createMcpHandler(() => {
|
|
796
|
+
const request = requestOperations.getStore();
|
|
797
|
+
const activeTools = request?.filterCatalogues
|
|
798
|
+
? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], request.principal))
|
|
799
|
+
: tools;
|
|
800
|
+
const activeResources = request?.filterCatalogues
|
|
801
|
+
? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], request.principal))
|
|
802
|
+
: resources;
|
|
803
|
+
const activePrompts = request?.filterCatalogues
|
|
804
|
+
? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], request.principal))
|
|
805
|
+
: prompts;
|
|
806
|
+
const activeDiscoverableTools = request?.filterCatalogues
|
|
807
|
+
? discoverableTools.filter((tool) => accessAllows(tool[TOOL_ACCESS], request.principal))
|
|
808
|
+
: discoverableTools;
|
|
809
|
+
const activeDiscoverableResources = request?.filterCatalogues
|
|
810
|
+
? discoverableResources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], request.principal))
|
|
811
|
+
: discoverableResources;
|
|
812
|
+
const activeDiscoverablePrompts = request?.filterCatalogues
|
|
813
|
+
? discoverablePrompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], request.principal))
|
|
814
|
+
: discoverablePrompts;
|
|
815
|
+
const activeHasCompletion = activeResources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
816
|
+
activePrompts.some((prompt) => prompt[HAS_COMPLETION]);
|
|
757
817
|
const server = new McpServer(serverInfo, {
|
|
758
818
|
capabilities: {
|
|
759
|
-
...(
|
|
760
|
-
...(
|
|
761
|
-
...(
|
|
762
|
-
...(
|
|
819
|
+
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
820
|
+
...(activeResources.length ? { resources: { subscribe: false, listChanged: false } } : {}),
|
|
821
|
+
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
822
|
+
...(activeHasCompletion ? { completions: {} } : {}),
|
|
763
823
|
},
|
|
764
824
|
instructions: options.instructions,
|
|
765
|
-
cacheHints,
|
|
825
|
+
cacheHints: request?.filterCatalogues ? undefined : cacheHints,
|
|
766
826
|
supportedProtocolVersions: [PROTOCOL_VERSION],
|
|
767
827
|
});
|
|
768
|
-
for (const tool of
|
|
828
|
+
for (const tool of activeTools) {
|
|
769
829
|
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes);
|
|
770
830
|
}
|
|
771
|
-
for (const resource of
|
|
831
|
+
for (const resource of activeResources)
|
|
772
832
|
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
773
|
-
for (const prompt of
|
|
833
|
+
for (const prompt of activePrompts)
|
|
774
834
|
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
|
|
835
|
+
const filteredCatalogues = request?.filterCatalogues
|
|
836
|
+
? catalogueListings(activeDiscoverableTools, activeDiscoverableResources, activeDiscoverablePrompts, activeTools, activeResources, activePrompts)
|
|
837
|
+
: undefined;
|
|
838
|
+
const pagination = filteredCatalogues
|
|
839
|
+
? paginationOptions
|
|
840
|
+
? compileListPagination(paginationOptions, filteredCatalogues)
|
|
841
|
+
: activeDiscoverableTools.length !== activeTools.length ||
|
|
842
|
+
activeDiscoverableResources.length !== activeResources.length ||
|
|
843
|
+
activeDiscoverablePrompts.length !== activePrompts.length
|
|
844
|
+
? compileUnpaginatedCatalogue(filteredCatalogues)
|
|
845
|
+
: undefined
|
|
846
|
+
: basePagination;
|
|
775
847
|
if (pagination)
|
|
776
848
|
installListPagination(server, pagination);
|
|
777
849
|
return server;
|
|
@@ -782,7 +854,10 @@ export function createEmseepea(options) {
|
|
|
782
854
|
});
|
|
783
855
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
784
856
|
const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
|
|
785
|
-
const
|
|
857
|
+
const observabilityLimits = observability.length ? { deliveryTimeoutMs: 1_000 } : undefined;
|
|
858
|
+
const finishObservability = observabilityLimits
|
|
859
|
+
? installObservability(app, observability, (body) => capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName), observabilityLimits)
|
|
860
|
+
: undefined;
|
|
786
861
|
const limiter = deployment.mode === "production-behind-proxy"
|
|
787
862
|
? new FixedWindowRateLimiter(deployment.rateLimit)
|
|
788
863
|
: undefined;
|
|
@@ -793,11 +868,11 @@ export function createEmseepea(options) {
|
|
|
793
868
|
.code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
|
|
794
869
|
});
|
|
795
870
|
app.addHook("preClose", async () => { stopping.abort(); });
|
|
796
|
-
if (
|
|
871
|
+
if (authentication) {
|
|
797
872
|
app.all("/.well-known/*", async (request, reply) => {
|
|
798
|
-
const response = oauthMetadataResponse(new Request(new URL(request.url,
|
|
873
|
+
const response = oauthMetadataResponse(new Request(new URL(request.url, authentication.metadata.resourceServerUrl.origin), {
|
|
799
874
|
method: request.method,
|
|
800
|
-
}),
|
|
875
|
+
}), authentication.metadata);
|
|
801
876
|
if (!response) {
|
|
802
877
|
await reply.code(404).send();
|
|
803
878
|
return;
|
|
@@ -821,20 +896,32 @@ export function createEmseepea(options) {
|
|
|
821
896
|
await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
|
|
822
897
|
return;
|
|
823
898
|
}
|
|
824
|
-
const access =
|
|
825
|
-
|
|
899
|
+
const access = capabilityAccessForRequest(request.body, toolsByName, resourcesByUri, resourceTemplates, promptsByName);
|
|
900
|
+
const protectsCatalogue = authentication?.discovery === "protected";
|
|
901
|
+
let authInfo;
|
|
902
|
+
if (authentication && (protectsCatalogue || access && access !== "public")) {
|
|
826
903
|
try {
|
|
827
|
-
|
|
904
|
+
authInfo = await verifyAuthenticatedRequest(request, reply, protectsCatalogue ? [] : access.requiredScopes, authentication);
|
|
828
905
|
request.raw.auth = authInfo;
|
|
829
906
|
}
|
|
830
907
|
catch (error) {
|
|
831
908
|
await sendWebResponse(reply, bearerAuthChallengeResponse(safeOAuthError(error), {
|
|
832
|
-
requiredScopes:
|
|
833
|
-
|
|
909
|
+
requiredScopes: protectsCatalogue
|
|
910
|
+
? []
|
|
911
|
+
: [...access.requiredScopes],
|
|
912
|
+
resourceMetadataUrl: authentication.resourceMetadataUrl,
|
|
834
913
|
}));
|
|
835
914
|
return;
|
|
836
915
|
}
|
|
837
916
|
}
|
|
917
|
+
const principal = authInfo ? principalFrom(authInfo) : undefined;
|
|
918
|
+
if (protectsCatalogue)
|
|
919
|
+
reply.raw.setHeader("cache-control", "private, no-store");
|
|
920
|
+
if (protectsCatalogue && isCapabilityInvocation(request.body) &&
|
|
921
|
+
(access === undefined || !accessAllows(access, principal))) {
|
|
922
|
+
await sendRpcError(reply, 200, -32602, "Capability not found", requestId(isRecord(request.body) ? request.body.id : undefined));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
838
925
|
const disconnected = new AbortController();
|
|
839
926
|
const abort = () => disconnected.abort();
|
|
840
927
|
request.raw.once("aborted", abort);
|
|
@@ -843,7 +930,12 @@ export function createEmseepea(options) {
|
|
|
843
930
|
abort();
|
|
844
931
|
reply.hijack();
|
|
845
932
|
try {
|
|
846
|
-
await requestOperations.run({
|
|
933
|
+
await requestOperations.run({
|
|
934
|
+
deadlineMs: Date.now() + operationTimeoutMs,
|
|
935
|
+
signal: disconnected.signal,
|
|
936
|
+
principal,
|
|
937
|
+
filterCatalogues: protectsCatalogue,
|
|
938
|
+
}, () => nodeHandler(request.raw, reply.raw, request.body));
|
|
847
939
|
}
|
|
848
940
|
finally {
|
|
849
941
|
request.raw.off("aborted", abort);
|
|
@@ -862,7 +954,13 @@ export function createEmseepea(options) {
|
|
|
862
954
|
}
|
|
863
955
|
});
|
|
864
956
|
app.addHook("onClose", async () => sdkHandler.close());
|
|
865
|
-
runtimes.set(app, {
|
|
957
|
+
runtimes.set(app, {
|
|
958
|
+
deployment,
|
|
959
|
+
requestTimeoutMs: operationTimeoutMs + 5_000,
|
|
960
|
+
stopping,
|
|
961
|
+
observabilityLimits,
|
|
962
|
+
finishObservability,
|
|
963
|
+
});
|
|
866
964
|
return app;
|
|
867
965
|
}
|
|
868
966
|
function safeOAuthError(error) {
|
|
@@ -877,7 +975,7 @@ function safeOAuthError(error) {
|
|
|
877
975
|
: "Authorization request failed";
|
|
878
976
|
return new OAuthError(error.code, message);
|
|
879
977
|
}
|
|
880
|
-
async function
|
|
978
|
+
async function verifyAuthenticatedRequest(request, reply, requiredScopes, authentication) {
|
|
881
979
|
const disconnected = new AbortController();
|
|
882
980
|
const abort = () => disconnected.abort();
|
|
883
981
|
request.raw.once("aborted", abort);
|
|
@@ -885,14 +983,20 @@ async function verifyProtectedCall(request, reply, access, oauth) {
|
|
|
885
983
|
if (requestAlreadyClosed(request, reply))
|
|
886
984
|
abort();
|
|
887
985
|
try {
|
|
888
|
-
const authInfo = await runWithDeadline(disconnected.signal, Date.now() +
|
|
889
|
-
verifier:
|
|
890
|
-
|
|
891
|
-
|
|
986
|
+
const authInfo = await runWithDeadline(disconnected.signal, Date.now() + authentication.verificationTimeoutMs, () => verifyBearerToken(singleHeader(request.raw.rawHeaders, "authorization"), {
|
|
987
|
+
verifier: {
|
|
988
|
+
async verifyAccessToken(token) {
|
|
989
|
+
const authInfo = await authentication.verifier.verifyAccessToken(token);
|
|
990
|
+
principalFrom(authInfo);
|
|
991
|
+
return authInfo;
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
requiredScopes: [...requiredScopes],
|
|
995
|
+
resourceMetadataUrl: authentication.resourceMetadataUrl,
|
|
892
996
|
}));
|
|
893
997
|
if (!authInfo.resource || authInfo.resource.hash || !checkResourceAllowed({
|
|
894
998
|
requestedResource: authInfo.resource,
|
|
895
|
-
configuredResource:
|
|
999
|
+
configuredResource: authentication.metadata.resourceServerUrl,
|
|
896
1000
|
})) {
|
|
897
1001
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "Token is not valid for this resource");
|
|
898
1002
|
}
|
|
@@ -912,21 +1016,79 @@ async function sendWebResponse(reply, response) {
|
|
|
912
1016
|
const body = Buffer.from(await response.arrayBuffer());
|
|
913
1017
|
await reply.code(response.status).send(body.length ? body : undefined);
|
|
914
1018
|
}
|
|
915
|
-
function
|
|
916
|
-
if (!isRecord(body) ||
|
|
917
|
-
|
|
1019
|
+
function capabilityAccessForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName) {
|
|
1020
|
+
if (!isRecord(body) || !isRecord(body.params))
|
|
1021
|
+
return undefined;
|
|
1022
|
+
const params = body.params;
|
|
1023
|
+
if (body.method === "tools/call" && typeof params.name === "string") {
|
|
1024
|
+
return toolsByName.get(params.name)?.[TOOL_ACCESS];
|
|
1025
|
+
}
|
|
1026
|
+
if (body.method === "resources/read" && typeof params.uri === "string") {
|
|
1027
|
+
return resourcesByUri.get(params.uri)?.[RESOURCE_ACCESS] ?? resourceTemplates.find((resource) => resource[RESOURCE_MATCHES]?.(params.uri))?.[RESOURCE_ACCESS];
|
|
1028
|
+
}
|
|
1029
|
+
if (body.method === "prompts/get" && typeof params.name === "string") {
|
|
1030
|
+
return promptsByName.get(params.name)?.[PROMPT_ACCESS];
|
|
1031
|
+
}
|
|
1032
|
+
if (body.method === "completion/complete" && isRecord(params.ref)) {
|
|
1033
|
+
const reference = params.ref;
|
|
1034
|
+
if (reference.type === "ref/prompt" && typeof reference.name === "string") {
|
|
1035
|
+
return promptsByName.get(reference.name)?.[PROMPT_ACCESS];
|
|
1036
|
+
}
|
|
1037
|
+
if (reference.type === "ref/resource" && typeof reference.uri === "string") {
|
|
1038
|
+
return resourceTemplates.find((resource) => resource[RESOURCE_URI] === reference.uri)?.[RESOURCE_ACCESS];
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
function isCapabilityInvocation(body) {
|
|
1044
|
+
return isRecord(body) && (body.method === "tools/call" || body.method === "resources/read" ||
|
|
1045
|
+
body.method === "prompts/get" || body.method === "completion/complete");
|
|
1046
|
+
}
|
|
1047
|
+
function capabilityNameForRequest(body, toolsByName, resourcesByUri, resourceTemplates, promptsByName) {
|
|
1048
|
+
if (!isRecord(body) || !isRecord(body.params))
|
|
918
1049
|
return undefined;
|
|
919
|
-
const
|
|
920
|
-
|
|
1050
|
+
const params = body.params;
|
|
1051
|
+
if (body.method === "tools/call" && typeof params.name === "string") {
|
|
1052
|
+
return toolsByName.has(params.name) ? params.name : undefined;
|
|
1053
|
+
}
|
|
1054
|
+
if (body.method === "resources/read" && typeof params.uri === "string") {
|
|
1055
|
+
const resource = resourcesByUri.get(params.uri) ?? resourceTemplates.find((candidate) => candidate[RESOURCE_MATCHES]?.(params.uri));
|
|
1056
|
+
return resource?.[RESOURCE_NAME];
|
|
1057
|
+
}
|
|
1058
|
+
if (body.method === "prompts/get" && typeof params.name === "string") {
|
|
1059
|
+
return promptsByName.has(params.name) ? params.name : undefined;
|
|
1060
|
+
}
|
|
1061
|
+
if (body.method === "completion/complete" && isRecord(params.ref)) {
|
|
1062
|
+
const reference = params.ref;
|
|
1063
|
+
return reference.type === "ref/prompt" && typeof reference.name === "string"
|
|
1064
|
+
? (promptsByName.has(reference.name) ? reference.name : undefined)
|
|
1065
|
+
: reference.type === "ref/resource" && typeof reference.uri === "string"
|
|
1066
|
+
? resourceTemplates.find((candidate) => candidate[RESOURCE_URI] === reference.uri)?.[RESOURCE_NAME]
|
|
1067
|
+
: undefined;
|
|
1068
|
+
}
|
|
1069
|
+
return undefined;
|
|
921
1070
|
}
|
|
922
1071
|
function principalFrom(authInfo) {
|
|
923
1072
|
if (!authInfo)
|
|
924
|
-
throw new Error("Protected
|
|
925
|
-
|
|
1073
|
+
throw new Error("Protected capability reached execution without verified authorization");
|
|
1074
|
+
if (typeof authInfo.clientId !== "string" || !authInfo.clientId.trim() || authInfo.clientId.length > 256) {
|
|
1075
|
+
throw new TypeError("Authentication verifier returned an invalid clientId");
|
|
1076
|
+
}
|
|
1077
|
+
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) {
|
|
1078
|
+
throw new TypeError("Authentication verifier returned invalid permissions");
|
|
1079
|
+
}
|
|
1080
|
+
const permissions = Object.freeze([...authInfo.scopes]);
|
|
1081
|
+
return Object.freeze({
|
|
926
1082
|
clientId: authInfo.clientId,
|
|
927
|
-
|
|
1083
|
+
permissions,
|
|
928
1084
|
resource: authInfo.resource?.href,
|
|
929
|
-
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function accessAllows(access, principal) {
|
|
1088
|
+
if (access === "public")
|
|
1089
|
+
return true;
|
|
1090
|
+
return principal !== undefined &&
|
|
1091
|
+
access.requiredScopes.every((permission) => principal.permissions.includes(permission));
|
|
930
1092
|
}
|
|
931
1093
|
export async function serveEmseepea(app, options = {}) {
|
|
932
1094
|
const runtime = runtimes.get(app);
|
|
@@ -938,8 +1100,13 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
938
1100
|
}
|
|
939
1101
|
const port = nonNegativePort(options.port ?? 3000);
|
|
940
1102
|
const shutdownTimeoutMs = positiveInteger("shutdownTimeoutMs", options.shutdownTimeoutMs ?? 5_000);
|
|
941
|
-
const
|
|
942
|
-
|
|
1103
|
+
const observabilityFlushTimeoutMs = positiveInteger("observabilityFlushTimeoutMs", options.observabilityFlushTimeoutMs === undefined ? 1_000 : options.observabilityFlushTimeoutMs);
|
|
1104
|
+
if (observabilityFlushTimeoutMs > 60_000) {
|
|
1105
|
+
throw new TypeError("observabilityFlushTimeoutMs must not exceed 60000");
|
|
1106
|
+
}
|
|
1107
|
+
if (runtime.observabilityLimits) {
|
|
1108
|
+
runtime.observabilityLimits.deliveryTimeoutMs = observabilityFlushTimeoutMs;
|
|
1109
|
+
}
|
|
943
1110
|
await app.listen({ host, port });
|
|
944
1111
|
app.server.headersTimeout = 10_000;
|
|
945
1112
|
app.server.keepAliveTimeout = 5_000;
|
|
@@ -957,7 +1124,7 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
957
1124
|
url: new URL(`http://${urlHost}:${address.port}/mcp`),
|
|
958
1125
|
close: () => {
|
|
959
1126
|
runtime.stopping.abort();
|
|
960
|
-
return closing ??= closeApp(app, shutdownTimeoutMs,
|
|
1127
|
+
return closing ??= closeApp(app, shutdownTimeoutMs, observabilityFlushTimeoutMs, runtime.finishObservability);
|
|
961
1128
|
},
|
|
962
1129
|
};
|
|
963
1130
|
}
|
|
@@ -1037,26 +1204,41 @@ class FixedWindowRateLimiter {
|
|
|
1037
1204
|
return "allowed";
|
|
1038
1205
|
}
|
|
1039
1206
|
}
|
|
1040
|
-
function
|
|
1207
|
+
function normalizeCapabilityAccess(kind, access, requiredScopes) {
|
|
1041
1208
|
if (access === "public")
|
|
1042
1209
|
return access;
|
|
1043
1210
|
if (access !== "protected" || !Array.isArray(requiredScopes) || requiredScopes.length === 0) {
|
|
1044
|
-
throw new TypeError(
|
|
1211
|
+
throw new TypeError(`${kind} access must be explicitly declared as "public" or protected with scopes`);
|
|
1045
1212
|
}
|
|
1046
1213
|
const scopes = requiredScopes.map((scope) => {
|
|
1047
1214
|
if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
1048
|
-
throw new TypeError(
|
|
1215
|
+
throw new TypeError(`Protected ${kind.toLowerCase()} scopes must be valid OAuth scope tokens`);
|
|
1049
1216
|
}
|
|
1050
1217
|
return scope;
|
|
1051
1218
|
});
|
|
1052
1219
|
if (new Set(scopes).size !== scopes.length) {
|
|
1053
|
-
throw new TypeError(
|
|
1220
|
+
throw new TypeError(`Protected ${kind.toLowerCase()} scopes must be unique`);
|
|
1054
1221
|
}
|
|
1055
1222
|
return { type: "protected", requiredScopes: scopes };
|
|
1056
1223
|
}
|
|
1057
|
-
function
|
|
1224
|
+
function normalizeDiscoverable(kind, value) {
|
|
1225
|
+
if (value === undefined || value === true)
|
|
1226
|
+
return true;
|
|
1227
|
+
if (value === false)
|
|
1228
|
+
return false;
|
|
1229
|
+
throw new TypeError(`${kind} discoverable must be a boolean`);
|
|
1230
|
+
}
|
|
1231
|
+
function accessMetadata(metadata, access) {
|
|
1232
|
+
return Object.freeze({
|
|
1233
|
+
...metadata,
|
|
1234
|
+
"io.emseepea/access": access === "public"
|
|
1235
|
+
? Object.freeze({ type: "public" })
|
|
1236
|
+
: Object.freeze({ type: "protected", requiredScopes: [...access.requiredScopes] }),
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
function normalizeAuthentication(options) {
|
|
1058
1240
|
if (!options.verifier || typeof options.verifier.verifyAccessToken !== "function") {
|
|
1059
|
-
throw new TypeError("
|
|
1241
|
+
throw new TypeError("authentication.verifier must implement verifyAccessToken");
|
|
1060
1242
|
}
|
|
1061
1243
|
if (options.metadata.dangerouslyAllowInsecureIssuerUrl === true) {
|
|
1062
1244
|
throw new TypeError("Insecure OAuth issuer URLs are not supported");
|
|
@@ -1072,9 +1254,37 @@ function normalizeOAuth(options) {
|
|
|
1072
1254
|
verifier: options.verifier,
|
|
1073
1255
|
metadata: options.metadata,
|
|
1074
1256
|
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(options.metadata.resourceServerUrl),
|
|
1075
|
-
verificationTimeoutMs: positiveInteger("
|
|
1257
|
+
verificationTimeoutMs: positiveInteger("authentication.verificationTimeoutMs", options.verificationTimeoutMs ?? 10_000),
|
|
1258
|
+
discovery: options.discovery === undefined || options.discovery === "public"
|
|
1259
|
+
? "public"
|
|
1260
|
+
: options.discovery === "protected"
|
|
1261
|
+
? "protected"
|
|
1262
|
+
: (() => { throw new TypeError('authentication.discovery must be "public" or "protected"'); })(),
|
|
1076
1263
|
};
|
|
1077
1264
|
}
|
|
1265
|
+
function normalizeObservability(adapters) {
|
|
1266
|
+
if (adapters === undefined)
|
|
1267
|
+
return [];
|
|
1268
|
+
if (!Array.isArray(adapters))
|
|
1269
|
+
throw new TypeError("observability must be an array");
|
|
1270
|
+
const ids = new Set();
|
|
1271
|
+
return Object.freeze(adapters.map((adapter) => {
|
|
1272
|
+
if (!adapter || typeof adapter !== "object" ||
|
|
1273
|
+
typeof adapter.id !== "string" || !/^[A-Za-z0-9_.-]{1,64}$/.test(adapter.id) ||
|
|
1274
|
+
typeof adapter.emit !== "function" ||
|
|
1275
|
+
adapter.flush !== undefined && typeof adapter.flush !== "function") {
|
|
1276
|
+
throw new TypeError("observability adapters require a valid id, emit function, and optional flush function");
|
|
1277
|
+
}
|
|
1278
|
+
if (ids.has(adapter.id))
|
|
1279
|
+
throw new TypeError(`Duplicate observability adapter id: ${adapter.id}`);
|
|
1280
|
+
ids.add(adapter.id);
|
|
1281
|
+
return Object.freeze({
|
|
1282
|
+
id: adapter.id,
|
|
1283
|
+
emit: adapter.emit,
|
|
1284
|
+
...(adapter.flush ? { flush: adapter.flush } : {}),
|
|
1285
|
+
});
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1078
1288
|
function isLoopbackUrl(url) {
|
|
1079
1289
|
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
1080
1290
|
}
|
|
@@ -1448,6 +1658,7 @@ function completionCallback(complete, currentName, allowedNames, timeoutMs, maxA
|
|
|
1448
1658
|
const values = await complete(value, {
|
|
1449
1659
|
signal,
|
|
1450
1660
|
deadlineMs,
|
|
1661
|
+
principal: request?.principal,
|
|
1451
1662
|
arguments: completionArguments(context?.arguments, allowedArguments),
|
|
1452
1663
|
});
|
|
1453
1664
|
signal.throwIfAborted();
|
|
@@ -1679,7 +1890,7 @@ function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal)
|
|
|
1679
1890
|
function isLoopbackHost(host) {
|
|
1680
1891
|
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1681
1892
|
}
|
|
1682
|
-
async function closeApp(app, timeoutMs,
|
|
1893
|
+
async function closeApp(app, timeoutMs, flushTimeoutMs, finishObservability) {
|
|
1683
1894
|
let timer;
|
|
1684
1895
|
let onClosed;
|
|
1685
1896
|
const httpClosed = new Promise((resolve) => { onClosed = resolve; });
|
|
@@ -1698,21 +1909,14 @@ async function closeApp(app, timeoutMs, flushTelemetry, flushTimeoutMs, finishTe
|
|
|
1698
1909
|
finally {
|
|
1699
1910
|
if (timer)
|
|
1700
1911
|
clearTimeout(timer);
|
|
1701
|
-
if (
|
|
1702
|
-
const controller = new AbortController();
|
|
1703
|
-
const flushDeadline = Date.now() + flushTimeoutMs;
|
|
1912
|
+
if (finishObservability) {
|
|
1704
1913
|
const expired = new Promise((resolve) => {
|
|
1705
1914
|
// Keep the process alive long enough to finish flushing after sockets close.
|
|
1706
|
-
timer = setTimeout(
|
|
1915
|
+
timer = setTimeout(resolve, flushTimeoutMs);
|
|
1707
1916
|
});
|
|
1708
1917
|
try {
|
|
1709
1918
|
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(() => { }),
|
|
1919
|
+
httpClosed.then(() => finishObservability(flushTimeoutMs)).catch(() => { }),
|
|
1716
1920
|
expired,
|
|
1717
1921
|
]);
|
|
1718
1922
|
}
|