@emseepea/server 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,32 +1,56 @@
1
1
  import { createMcpFastifyApp } from "@modelcontextprotocol/fastify";
2
2
  import { GetPromptResultSchema, ReadResourceResultSchema, } from "@modelcontextprotocol/core";
3
3
  import { toNodeHandler } from "@modelcontextprotocol/node";
4
- import { McpServer, OAuthError, OAuthErrorCode, ResourceTemplate, bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, checkResourceAllowed, completable, createMcpHandler, getOAuthProtectedResourceMetadataUrl, oauthMetadataResponse, verifyBearerToken, } from "@modelcontextprotocol/server";
4
+ import { McpServer, OAuthError, OAuthErrorCode, ProtocolError, ProtocolErrorCode, ResourceTemplate, bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, checkResourceAllowed, completable, createMcpHandler, getOAuthProtectedResourceMetadataUrl, inputRequired as sdkInputRequired, acceptedContent as sdkAcceptedContent, inputResponse as sdkInputResponse, isInputRequiredResult, oauthMetadataResponse, specTypeSchemas, verifyBearerToken, } from "@modelcontextprotocol/server";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { createHash } from "node:crypto";
6
7
  import { isIP } from "node:net";
7
8
  import { z } from "zod";
9
+ import { installRequestTelemetry } from "./telemetry.js";
8
10
  export * from "./ui.js";
11
+ export const inputRequired = Object.assign((spec) => sdkInputRequired({ inputRequests: spec.inputRequests }), {
12
+ elicit: sdkInputRequired.elicit,
13
+ elicitUrl: sdkInputRequired.elicitUrl,
14
+ });
15
+ export function acceptedContent(responses, key, schema) {
16
+ return sdkAcceptedContent(checkedInputResponses(responses), key, schema);
17
+ }
18
+ export function inputResponse(responses, key) {
19
+ const response = sdkInputResponse(checkedInputResponses(responses), key);
20
+ return response.kind === "elicit" ? response : { kind: "missing" };
21
+ }
9
22
  const PROTOCOL_VERSION = "2026-07-28";
10
23
  const REGISTER = Symbol("register");
11
24
  const TOOL_NAME = Symbol("toolName");
12
25
  const TOOL_ACCESS = Symbol("toolAccess");
13
26
  const TOOL_STREAMING = Symbol("toolStreaming");
27
+ const TOOL_LISTING = Symbol("toolListing");
14
28
  const RESOURCE_NAME = Symbol("resourceName");
15
29
  const RESOURCE_URI = Symbol("resourceUri");
16
30
  const RESOURCE_KIND = Symbol("resourceKind");
17
31
  const RESOURCE_MATCHES = Symbol("resourceMatches");
18
32
  const RESOURCE_ROUTE = Symbol("resourceRoute");
33
+ const RESOURCE_LISTING = Symbol("resourceListing");
19
34
  const PROMPT_NAME = Symbol("promptName");
20
35
  const HAS_COMPLETION = Symbol("hasCompletion");
36
+ const PROMPT_LISTING = Symbol("promptListing");
37
+ const CACHEABLE_METHODS = [
38
+ "tools/list",
39
+ "prompts/list",
40
+ "resources/list",
41
+ "resources/templates/list",
42
+ "resources/read",
43
+ "server/discover",
44
+ ];
21
45
  const runtimes = new WeakMap();
22
46
  const requestOperations = new AsyncLocalStorage();
23
47
  export function defineTool(definition) {
24
48
  const handler = definition.handler;
25
- return createCheckedTool(definition, handler, false);
49
+ return createCheckedTool(definition, handler, false, true);
26
50
  }
27
51
  export function defineStreamingTool(definition) {
28
52
  const handler = definition.handler;
29
- return createCheckedTool(definition, handler, true);
53
+ return createCheckedTool(definition, handler, true, false);
30
54
  }
31
55
  export function defineMappedTool(definition) {
32
56
  const { backendInputSchema, backendOutputSchema, isAvailable, adapter } = definition;
@@ -54,29 +78,61 @@ export function defineMappedTool(definition) {
54
78
  context.signal.throwIfAborted();
55
79
  return mapOutput(parsedBackendResult.data);
56
80
  };
57
- return createCheckedTool(definition, execute, false);
81
+ return createCheckedTool(definition, execute, false, false);
58
82
  }
59
83
  export function defineResource(definition) {
60
- const { name, title, description, mimeType, handler } = definition;
84
+ const { name, handler } = definition;
61
85
  assertRegistrationName("Resource", name);
62
86
  const uri = canonicalResourceUri(definition.uri);
87
+ const listing = checkedProtocolValue("Resource", {
88
+ name,
89
+ uri,
90
+ title: definition.title,
91
+ description: definition.description,
92
+ mimeType: definition.mimeType,
93
+ icons: definition.icons,
94
+ annotations: definition.annotations,
95
+ size: definition.size,
96
+ _meta: definition._meta,
97
+ });
98
+ const metadata = Object.freeze({
99
+ title: listing.title,
100
+ description: listing.description,
101
+ mimeType: listing.mimeType,
102
+ icons: listing.icons,
103
+ annotations: listing.annotations,
104
+ size: listing.size,
105
+ _meta: listing._meta,
106
+ });
107
+ const cacheHint = definition.cacheHint === undefined
108
+ ? undefined
109
+ : normalizeCacheHint(definition.cacheHint, `resource ${name}`);
63
110
  const registration = {
64
111
  [RESOURCE_NAME]: name,
65
112
  [RESOURCE_URI]: uri,
66
113
  [RESOURCE_KIND]: "static",
114
+ [RESOURCE_LISTING]: Object.freeze({
115
+ method: "resources/list",
116
+ value: listing,
117
+ }),
67
118
  [HAS_COMPLETION]: false,
68
119
  [REGISTER](server, timeoutMs, maxApplicationResultBytes) {
69
- server.registerResource(name, uri, {
70
- title,
71
- description,
72
- mimeType,
73
- }, async (_requestedUri, context) => {
120
+ server.registerResource(name, uri, cacheHint ? { ...metadata, cacheHint } : metadata, async (_requestedUri, context) => {
74
121
  try {
75
122
  const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
76
123
  return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
77
124
  signal.throwIfAborted();
78
- const result = await handler({ signal, deadlineMs });
125
+ const result = await handler({
126
+ signal,
127
+ deadlineMs,
128
+ inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
129
+ });
79
130
  signal.throwIfAborted();
131
+ if (isInputRequiredResult(result)) {
132
+ assertStatelessInputRequired(result);
133
+ assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
134
+ return result;
135
+ }
80
136
  const parsed = await ReadResourceResultSchema.safeParseAsync(result);
81
137
  if (!parsed.success || parsed.data.contents.some((content) => content.uri !== uri)) {
82
138
  throw new Error("Resource returned an invalid result");
@@ -95,10 +151,31 @@ export function defineResource(definition) {
95
151
  return Object.freeze(registration);
96
152
  }
97
153
  export function defineResourceTemplate(definition) {
98
- const { name, title, description, mimeType, handler } = definition;
154
+ const { name, handler } = definition;
99
155
  assertRegistrationName("Resource template", name);
100
156
  const { template, route } = checkedResourceTemplate(definition.uriTemplate);
101
157
  const uriTemplate = template.uriTemplate.toString();
158
+ const listing = checkedProtocolValue("ResourceTemplate", {
159
+ name,
160
+ uriTemplate,
161
+ title: definition.title,
162
+ description: definition.description,
163
+ mimeType: definition.mimeType,
164
+ icons: definition.icons,
165
+ annotations: definition.annotations,
166
+ _meta: definition._meta,
167
+ });
168
+ const metadata = Object.freeze({
169
+ title: listing.title,
170
+ description: listing.description,
171
+ mimeType: listing.mimeType,
172
+ icons: listing.icons,
173
+ annotations: listing.annotations,
174
+ _meta: listing._meta,
175
+ });
176
+ const cacheHint = definition.cacheHint === undefined
177
+ ? undefined
178
+ : normalizeCacheHint(definition.cacheHint, `resource template ${name}`);
102
179
  const variableNames = [...template.uriTemplate.variableNames];
103
180
  const completions = checkedCompletionHandlers("Resource template", definition.complete, variableNames);
104
181
  const registration = {
@@ -107,6 +184,10 @@ export function defineResourceTemplate(definition) {
107
184
  [RESOURCE_KIND]: "template",
108
185
  [RESOURCE_MATCHES]: (uri) => template.uriTemplate.match(uri) !== null,
109
186
  [RESOURCE_ROUTE]: route,
187
+ [RESOURCE_LISTING]: Object.freeze({
188
+ method: "resources/templates/list",
189
+ value: listing,
190
+ }),
110
191
  [HAS_COMPLETION]: completions.size > 0,
111
192
  [REGISTER](server, timeoutMs, maxApplicationResultBytes) {
112
193
  const registeredTemplate = completions.size === 0
@@ -118,13 +199,22 @@ export function defineResourceTemplate(definition) {
118
199
  completionCallback(complete, variable, variableNames, timeoutMs, maxApplicationResultBytes),
119
200
  ])),
120
201
  });
121
- server.registerResource(name, registeredTemplate, { title, description, mimeType }, async (requestedUri, variables, context) => {
202
+ server.registerResource(name, registeredTemplate, cacheHint ? { ...metadata, cacheHint } : metadata, async (requestedUri, variables, context) => {
122
203
  try {
123
204
  const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
124
205
  return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
125
206
  signal.throwIfAborted();
126
- const result = await handler({ uri: requestedUri.href, variables }, { signal, deadlineMs });
207
+ const result = await handler({ uri: requestedUri.href, variables }, {
208
+ signal,
209
+ deadlineMs,
210
+ inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
211
+ });
127
212
  signal.throwIfAborted();
213
+ if (isInputRequiredResult(result)) {
214
+ assertStatelessInputRequired(result);
215
+ assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
216
+ return result;
217
+ }
128
218
  const parsed = await ReadResourceResultSchema.safeParseAsync(result);
129
219
  if (!parsed.success ||
130
220
  parsed.data.contents.some((content) => content.uri !== requestedUri.href)) {
@@ -144,17 +234,31 @@ export function defineResourceTemplate(definition) {
144
234
  return Object.freeze(registration);
145
235
  }
146
236
  export function definePrompt(definition) {
147
- const { name, title, description, argsSchema, handler } = definition;
237
+ const { name, argsSchema, handler } = definition;
148
238
  assertRegistrationName("Prompt", name);
149
239
  const argumentNames = Object.keys(argsSchema.shape);
150
240
  const completions = checkedCompletionHandlers("Prompt", definition.complete, argumentNames);
241
+ const listing = checkedProtocolValue("Prompt", {
242
+ name,
243
+ title: definition.title,
244
+ description: definition.description,
245
+ icons: definition.icons,
246
+ _meta: definition._meta,
247
+ arguments: promptArguments(argsSchema),
248
+ });
249
+ const metadata = Object.freeze({
250
+ title: listing.title,
251
+ description: listing.description,
252
+ icons: listing.icons,
253
+ _meta: listing._meta,
254
+ });
151
255
  const registration = {
152
256
  [PROMPT_NAME]: name,
153
257
  [HAS_COMPLETION]: completions.size > 0,
258
+ [PROMPT_LISTING]: listing,
154
259
  [REGISTER](server, timeoutMs, maxApplicationResultBytes) {
155
260
  server.registerPrompt(name, {
156
- title,
157
- description,
261
+ ...metadata,
158
262
  argsSchema: sdkPromptMetadataSchema(argsSchema, completions, argumentNames, timeoutMs, maxApplicationResultBytes),
159
263
  }, async (args, context) => {
160
264
  try {
@@ -164,8 +268,17 @@ export function definePrompt(definition) {
164
268
  if (!parsedArgs.success)
165
269
  throw new Error("Prompt received invalid arguments");
166
270
  signal.throwIfAborted();
167
- const result = await handler(parsedArgs.data, { signal, deadlineMs });
271
+ const result = await handler(parsedArgs.data, {
272
+ signal,
273
+ deadlineMs,
274
+ inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
275
+ });
168
276
  signal.throwIfAborted();
277
+ if (isInputRequiredResult(result)) {
278
+ assertStatelessInputRequired(result);
279
+ assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
280
+ return result;
281
+ }
169
282
  const parsedResult = await GetPromptResultSchema.safeParseAsync(result);
170
283
  if (!parsedResult.success)
171
284
  throw new Error("Prompt returned an invalid result");
@@ -187,26 +300,45 @@ const progressUpdateSchema = z.strictObject({
187
300
  total: z.number().positive().optional(),
188
301
  message: z.string().optional(),
189
302
  });
190
- function createCheckedTool(definition, execute, streaming) {
191
- const { name, title, description, inputSchema, outputSchema } = definition;
303
+ function createCheckedTool(definition, execute, streaming, allowsInputRequired) {
304
+ const { name, inputSchema, outputSchema } = definition;
192
305
  assertRegistrationName("Tool", name);
306
+ assertValidMcpHeaderAnnotations(inputSchema);
193
307
  const access = normalizeToolAccess(definition.access, definition.requiredScopes);
308
+ const publicAccess = access === "public"
309
+ ? Object.freeze({ type: "public" })
310
+ : Object.freeze({ type: "protected", requiredScopes: [...access.requiredScopes] });
311
+ const sdkInputSchema = sdkMetadataSchema(inputSchema);
312
+ const sdkOutputSchema = sdkMetadataSchema(outputSchema);
313
+ const listing = checkedProtocolValue("Tool", {
314
+ name,
315
+ title: definition.title,
316
+ description: definition.description,
317
+ icons: definition.icons,
318
+ annotations: definition.annotations,
319
+ inputSchema: jsonMetadataSchema(inputSchema, "input"),
320
+ outputSchema: jsonMetadataSchema(outputSchema, "output"),
321
+ _meta: {
322
+ ...definition._meta,
323
+ "io.emseepea/access": publicAccess,
324
+ },
325
+ });
326
+ const metadata = Object.freeze({
327
+ title: listing.title,
328
+ description: listing.description,
329
+ icons: listing.icons,
330
+ annotations: listing.annotations,
331
+ inputSchema: sdkInputSchema,
332
+ outputSchema: sdkOutputSchema,
333
+ _meta: listing._meta,
334
+ });
194
335
  const registration = {
195
336
  [TOOL_NAME]: name,
196
337
  [TOOL_ACCESS]: access,
197
338
  [TOOL_STREAMING]: streaming,
339
+ [TOOL_LISTING]: listing,
198
340
  [REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes) {
199
- server.registerTool(name, {
200
- title,
201
- description,
202
- inputSchema: sdkMetadataSchema(inputSchema),
203
- outputSchema: sdkMetadataSchema(outputSchema),
204
- _meta: {
205
- "io.emseepea/access": access === "public"
206
- ? { type: "public" }
207
- : { type: "protected", requiredScopes: [...access.requiredScopes] },
208
- },
209
- }, async (input, context) => {
341
+ server.registerTool(name, metadata, async (input, context) => {
210
342
  try {
211
343
  const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
212
344
  return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
@@ -224,6 +356,11 @@ function createCheckedTool(definition, execute, streaming) {
224
356
  signal,
225
357
  deadlineMs,
226
358
  principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
359
+ ...(allowsInputRequired
360
+ ? {
361
+ inputResponses: checkedInputResponses(context.mcpReq.inputResponses),
362
+ }
363
+ : {}),
227
364
  ...(reporter ? { reportProgress: reporter.report } : {}),
228
365
  });
229
366
  }
@@ -231,6 +368,12 @@ function createCheckedTool(definition, execute, streaming) {
231
368
  await reporter?.finish();
232
369
  }
233
370
  reporter?.throwIfFailed();
371
+ if (allowsInputRequired && isInputRequiredResult(result)) {
372
+ signal.throwIfAborted();
373
+ assertStatelessInputRequired(result);
374
+ assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal);
375
+ return result;
376
+ }
234
377
  if (!isRecord(result) || typeof result.text !== "string" || !("data" in result)) {
235
378
  throw new Error("Tool returned an invalid result");
236
379
  }
@@ -311,15 +454,142 @@ function progressReporter(context, signal, maxEvents, maxEventBytes) {
311
454
  throw failure; },
312
455
  };
313
456
  }
457
+ function normalizeListPagination(options) {
458
+ const pageSize = positiveInteger("listPagination.pageSize", options.pageSize);
459
+ if (pageSize > 100)
460
+ throw new TypeError("listPagination.pageSize must not exceed 100");
461
+ return Object.freeze({
462
+ pageSize,
463
+ maxPageBytes: positiveInteger("listPagination.maxPageBytes", options.maxPageBytes ?? 1024 * 1024),
464
+ });
465
+ }
466
+ function compileListPagination(options, catalogues) {
467
+ const compiled = new Map();
468
+ for (const [method, entries] of catalogues) {
469
+ if (entries.length === 0)
470
+ continue;
471
+ const resultKey = method === "tools/list"
472
+ ? "tools"
473
+ : method === "prompts/list"
474
+ ? "prompts"
475
+ : method === "resources/templates/list"
476
+ ? "resourceTemplates"
477
+ : "resources";
478
+ compiled.set(method, compileCataloguePages(method, resultKey, entries, options));
479
+ }
480
+ return compiled;
481
+ }
482
+ function compileCataloguePages(method, resultKey, entries, options) {
483
+ const groups = [];
484
+ for (let index = 0; index < entries.length;) {
485
+ const group = [];
486
+ while (group.length < options.pageSize && index < entries.length) {
487
+ const candidate = [...group, entries[index]];
488
+ const hasNext = index + 1 < entries.length;
489
+ if (cataloguePageBytes(resultKey, candidate, hasNext) > options.maxPageBytes) {
490
+ if (group.length === 0) {
491
+ throw new TypeError(`${method} has an entry larger than listPagination.maxPageBytes`);
492
+ }
493
+ break;
494
+ }
495
+ group.push(entries[index]);
496
+ index += 1;
497
+ }
498
+ groups.push(group);
499
+ }
500
+ const catalogueDigest = createHash("sha256").update(JSON.stringify(entries)).digest("base64url");
501
+ const cursorFor = (page) => createHash("sha256").update([
502
+ "emseepea-list-cursor-v1",
503
+ method,
504
+ String(page),
505
+ String(options.pageSize),
506
+ String(options.maxPageBytes),
507
+ catalogueDigest,
508
+ ].join("\n")).digest("base64url");
509
+ const pages = groups.map((group, index) => Object.freeze({
510
+ [resultKey]: Object.freeze([...group]),
511
+ ...(index + 1 < groups.length ? { nextCursor: cursorFor(index + 1) } : {}),
512
+ }));
513
+ const byCursor = new Map();
514
+ for (let index = 1; index < pages.length; index += 1) {
515
+ byCursor.set(cursorFor(index), pages[index]);
516
+ }
517
+ return Object.freeze({ first: pages[0], byCursor });
518
+ }
519
+ function cataloguePageBytes(resultKey, entries, hasNext) {
520
+ return Buffer.byteLength(JSON.stringify({
521
+ [resultKey]: entries,
522
+ ...(hasNext ? { nextCursor: "x".repeat(43) } : {}),
523
+ }), "utf8");
524
+ }
525
+ function paginationPage(pages, cursor) {
526
+ if (cursor === undefined)
527
+ return pages.first;
528
+ const page = /^[A-Za-z0-9_-]{43}$/.test(cursor) ? pages.byCursor.get(cursor) : undefined;
529
+ if (!page)
530
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Invalid pagination cursor");
531
+ return page;
532
+ }
533
+ function installListPagination(server, pagination) {
534
+ const tools = pagination.get("tools/list");
535
+ if (tools)
536
+ server.server.setRequestHandler("tools/list", (request) => paginationPage(tools, request.params?.cursor));
537
+ const resources = pagination.get("resources/list");
538
+ if (resources)
539
+ server.server.setRequestHandler("resources/list", (request) => paginationPage(resources, request.params?.cursor));
540
+ const templates = pagination.get("resources/templates/list");
541
+ if (templates)
542
+ server.server.setRequestHandler("resources/templates/list", (request) => paginationPage(templates, request.params?.cursor));
543
+ const prompts = pagination.get("prompts/list");
544
+ if (prompts)
545
+ server.server.setRequestHandler("prompts/list", (request) => paginationPage(prompts, request.params?.cursor));
546
+ }
314
547
  export function createEmseepea(options) {
548
+ if (options.telemetry !== undefined && typeof options.telemetry !== "boolean") {
549
+ throw new TypeError("telemetry must be a boolean");
550
+ }
551
+ const readiness = options.readiness;
552
+ const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
553
+ const stopping = new AbortController();
554
+ let pendingReadiness;
555
+ async function ready() {
556
+ if (stopping.signal.aborted)
557
+ return false;
558
+ if (!readiness)
559
+ return true;
560
+ if (!pendingReadiness) {
561
+ const deadline = Date.now() + readinessTimeoutMs;
562
+ pendingReadiness = runWithDeadline(stopping.signal, deadline, async (signal) => {
563
+ try {
564
+ return await readiness({ signal }) === true && Date.now() < deadline;
565
+ }
566
+ finally {
567
+ pendingReadiness = undefined;
568
+ }
569
+ }).catch(() => false);
570
+ }
571
+ const result = await pendingReadiness;
572
+ return result && !stopping.signal.aborted;
573
+ }
315
574
  assertNonEmpty("name", options.name);
316
575
  assertNonEmpty("version", options.version);
576
+ const serverInfo = checkedProtocolValue("Implementation", {
577
+ name: options.name,
578
+ version: options.version,
579
+ title: options.title,
580
+ description: options.description,
581
+ icons: options.icons,
582
+ websiteUrl: options.websiteUrl,
583
+ });
317
584
  const tools = Object.freeze([...(options.tools ?? [])]);
318
585
  const resources = Object.freeze([...(options.resources ?? [])]);
319
586
  const prompts = Object.freeze([...(options.prompts ?? [])]);
320
587
  assertUniqueToolNames(tools);
321
588
  assertUniqueResources(resources);
322
589
  assertUniquePromptNames(prompts);
590
+ const paginationOptions = options.listPagination
591
+ ? normalizeListPagination(options.listPagination)
592
+ : undefined;
323
593
  const maxRequestBytes = positiveInteger("maxRequestBytes", options.maxRequestBytes ?? 1024 * 1024);
324
594
  const maxApplicationResultBytes = positiveInteger("maxApplicationResultBytes", options.maxApplicationResultBytes ?? 1024 * 1024);
325
595
  const maxProgressEvents = positiveInteger("maxProgressEvents", options.maxProgressEvents ?? 32);
@@ -328,8 +598,9 @@ export function createEmseepea(options) {
328
598
  const deployment = normalizeDeployment(options.deployment ?? { mode: "loopback" });
329
599
  const oauth = options.oauth ? normalizeOAuth(options.oauth) : undefined;
330
600
  const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
331
- if (hasStreaming && deployment.mode !== "loopback") {
332
- throw new TypeError("Streaming tools currently require the loopback deployment profile");
601
+ if (deployment.mode !== "loopback" &&
602
+ tools.some((tool) => tool[TOOL_STREAMING] && tool[TOOL_ACCESS] !== "public")) {
603
+ throw new TypeError("Protected streaming tools currently require the loopback deployment profile");
333
604
  }
334
605
  if (tools.some((tool) => tool[TOOL_ACCESS] !== "public") && !oauth) {
335
606
  throw new TypeError("Protected tools require OAuth resource-server configuration");
@@ -349,8 +620,23 @@ export function createEmseepea(options) {
349
620
  prompts.some((prompt) => prompt[HAS_COMPLETION]);
350
621
  if (hasCompletion)
351
622
  enabledMethods.add("completion/complete");
623
+ const cacheHints = options.cacheHints === undefined
624
+ ? undefined
625
+ : normalizeCacheHints(options.cacheHints, enabledMethods);
626
+ const pagination = paginationOptions
627
+ ? compileListPagination(paginationOptions, new Map([
628
+ ["tools/list", tools.map((tool) => tool[TOOL_LISTING])],
629
+ ["resources/list", resources
630
+ .filter((resource) => resource[RESOURCE_LISTING].method === "resources/list")
631
+ .map((resource) => resource[RESOURCE_LISTING].value)],
632
+ ["resources/templates/list", resources
633
+ .filter((resource) => resource[RESOURCE_LISTING].method === "resources/templates/list")
634
+ .map((resource) => resource[RESOURCE_LISTING].value)],
635
+ ["prompts/list", prompts.map((prompt) => prompt[PROMPT_LISTING])],
636
+ ]))
637
+ : undefined;
352
638
  const sdkHandler = createMcpHandler(() => {
353
- const server = new McpServer({ name: options.name, version: options.version }, {
639
+ const server = new McpServer(serverInfo, {
354
640
  capabilities: {
355
641
  ...(tools.length ? { tools: { listChanged: false } } : {}),
356
642
  ...(resources.length ? { resources: { subscribe: false, listChanged: false } } : {}),
@@ -358,6 +644,7 @@ export function createEmseepea(options) {
358
644
  ...(hasCompletion ? { completions: {} } : {}),
359
645
  },
360
646
  instructions: options.instructions,
647
+ cacheHints,
361
648
  supportedProtocolVersions: [PROTOCOL_VERSION],
362
649
  });
363
650
  for (const tool of tools) {
@@ -367,6 +654,8 @@ export function createEmseepea(options) {
367
654
  resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
368
655
  for (const prompt of prompts)
369
656
  prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes);
657
+ if (pagination)
658
+ installListPagination(server, pagination);
370
659
  return server;
371
660
  }, {
372
661
  legacy: "reject",
@@ -375,11 +664,17 @@ export function createEmseepea(options) {
375
664
  });
376
665
  const nodeHandler = toNodeHandler(sdkHandler);
377
666
  const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
667
+ const finishTelemetry = options.telemetry === true ? installRequestTelemetry(app) : undefined;
378
668
  const limiter = deployment.mode === "production-behind-proxy"
379
669
  ? new FixedWindowRateLimiter(deployment.rateLimit)
380
670
  : undefined;
381
671
  app.get("/healthz", health("ok\n"));
382
- app.get("/readyz", health("ready\n"));
672
+ app.get("/readyz", async (_request, reply) => {
673
+ const available = await ready();
674
+ await reply.header("cache-control", "no-store").type("text/plain; charset=utf-8")
675
+ .code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
676
+ });
677
+ app.addHook("preClose", async () => { stopping.abort(); });
383
678
  if (oauth) {
384
679
  app.all("/.well-known/*", async (request, reply) => {
385
680
  const response = oauthMetadataResponse(new Request(new URL(request.url, oauth.metadata.resourceServerUrl.origin), {
@@ -401,6 +696,8 @@ export function createEmseepea(options) {
401
696
  if (deployment.mode === "production-behind-proxy" &&
402
697
  !validateProductionRequest(request, reply, deployment, limiter))
403
698
  return;
699
+ if (!await validateMcpRequestHeaders(request, reply))
700
+ return;
404
701
  if (isRecord(request.body) && typeof request.body.method === "string" &&
405
702
  !enabledMethods.has(request.body.method)) {
406
703
  await sendRpcError(reply, 404, -32601, "Method not found", requestId(request.body.id));
@@ -424,7 +721,7 @@ export function createEmseepea(options) {
424
721
  const abort = () => disconnected.abort();
425
722
  request.raw.once("aborted", abort);
426
723
  reply.raw.once("close", abort);
427
- if (request.raw.aborted || request.raw.destroyed || reply.raw.destroyed)
724
+ if (requestAlreadyClosed(request, reply))
428
725
  abort();
429
726
  reply.hijack();
430
727
  try {
@@ -447,7 +744,7 @@ export function createEmseepea(options) {
447
744
  }
448
745
  });
449
746
  app.addHook("onClose", async () => sdkHandler.close());
450
- runtimes.set(app, { deployment, requestTimeoutMs: operationTimeoutMs + 5_000 });
747
+ runtimes.set(app, { deployment, requestTimeoutMs: operationTimeoutMs + 5_000, stopping, finishTelemetry });
451
748
  return app;
452
749
  }
453
750
  function safeOAuthError(error) {
@@ -467,6 +764,8 @@ async function verifyProtectedCall(request, reply, access, oauth) {
467
764
  const abort = () => disconnected.abort();
468
765
  request.raw.once("aborted", abort);
469
766
  reply.raw.once("close", abort);
767
+ if (requestAlreadyClosed(request, reply))
768
+ abort();
470
769
  try {
471
770
  const authInfo = await runWithDeadline(disconnected.signal, Date.now() + oauth.verificationTimeoutMs, () => verifyBearerToken(singleHeader(request.raw.rawHeaders, "authorization"), {
472
771
  verifier: oauth.verifier,
@@ -486,6 +785,9 @@ async function verifyProtectedCall(request, reply, access, oauth) {
486
785
  reply.raw.off("close", abort);
487
786
  }
488
787
  }
788
+ function requestAlreadyClosed(request, reply) {
789
+ return request.raw.aborted || (request.raw.destroyed && !request.raw.complete) || reply.raw.destroyed;
790
+ }
489
791
  async function sendWebResponse(reply, response) {
490
792
  for (const [name, value] of response.headers)
491
793
  reply.header(name, value);
@@ -518,6 +820,8 @@ export async function serveEmseepea(app, options = {}) {
518
820
  }
519
821
  const port = nonNegativePort(options.port ?? 3000);
520
822
  const shutdownTimeoutMs = positiveInteger("shutdownTimeoutMs", options.shutdownTimeoutMs ?? 5_000);
823
+ const flushTelemetry = options.flushTelemetry;
824
+ const telemetryFlushTimeoutMs = callbackTimeout("flushTelemetry", flushTelemetry, "telemetryFlushTimeoutMs", options.telemetryFlushTimeoutMs);
521
825
  await app.listen({ host, port });
522
826
  app.server.headersTimeout = 10_000;
523
827
  app.server.keepAliveTimeout = 5_000;
@@ -533,7 +837,10 @@ export async function serveEmseepea(app, options = {}) {
533
837
  let closing;
534
838
  return {
535
839
  url: new URL(`http://${urlHost}:${address.port}/mcp`),
536
- close: () => closing ??= closeApp(app, shutdownTimeoutMs),
840
+ close: () => {
841
+ runtime.stopping.abort();
842
+ return closing ??= closeApp(app, shutdownTimeoutMs, flushTelemetry, telemetryFlushTimeoutMs, runtime.finishTelemetry);
843
+ },
537
844
  };
538
845
  }
539
846
  function health(body) {
@@ -541,8 +848,12 @@ function health(body) {
541
848
  await reply.header("cache-control", "no-store").type("text/plain; charset=utf-8").send(body);
542
849
  };
543
850
  }
544
- async function sendRpcError(reply, status, code, message, id = null) {
545
- await reply.code(status).send({ jsonrpc: "2.0", id, error: { code, message } });
851
+ async function sendRpcError(reply, status, code, message, id = null, data) {
852
+ await reply.code(status).send({
853
+ jsonrpc: "2.0",
854
+ id,
855
+ error: { code, message, ...(data ? { data } : {}) },
856
+ });
546
857
  }
547
858
  function isFastifyError(error) {
548
859
  return error instanceof Error && "code" in error;
@@ -713,9 +1024,203 @@ function singleHeader(rawHeaders, name) {
713
1024
  }
714
1025
  return values.length === 0 ? undefined : values.length === 1 ? values[0] : null;
715
1026
  }
1027
+ async function validateMcpRequestHeaders(request, reply) {
1028
+ const body = request.body;
1029
+ const id = isRecord(body) ? requestId(body.id) : null;
1030
+ if (!acceptsMcpResponses(request.headers.accept)) {
1031
+ await sendRpcError(reply, 406, -32000, "Not Acceptable: Client must accept both application/json and text/event-stream", id);
1032
+ return false;
1033
+ }
1034
+ if (!isRecord(body) || typeof body.method !== "string")
1035
+ return true;
1036
+ const header = (name) => singleHeader(request.raw.rawHeaders, name);
1037
+ const rejectMismatch = async (name) => {
1038
+ await sendRpcError(reply, 400, -32020, `Missing or mismatched ${name} header`, id);
1039
+ return false;
1040
+ };
1041
+ const protocolVersion = header("mcp-protocol-version");
1042
+ if (!protocolVersion)
1043
+ return rejectMismatch("MCP-Protocol-Version");
1044
+ const params = isRecord(body.params) ? body.params : undefined;
1045
+ const meta = params && isRecord(params._meta) ? params._meta : undefined;
1046
+ const bodyVersion = meta?.["io.modelcontextprotocol/protocolVersion"];
1047
+ if (typeof bodyVersion !== "string" || protocolVersion !== bodyVersion) {
1048
+ return rejectMismatch("MCP-Protocol-Version");
1049
+ }
1050
+ if (protocolVersion !== PROTOCOL_VERSION) {
1051
+ await sendRpcError(reply, 400, ProtocolErrorCode.UnsupportedProtocolVersion, `Unsupported protocol version: ${protocolVersion}`, id, { requested: protocolVersion, supported: [PROTOCOL_VERSION] });
1052
+ return false;
1053
+ }
1054
+ if (header("mcp-method") !== body.method)
1055
+ return rejectMismatch("Mcp-Method");
1056
+ const nameField = body.method === "tools/call" || body.method === "prompts/get"
1057
+ ? "name"
1058
+ : body.method === "resources/read"
1059
+ ? "uri"
1060
+ : undefined;
1061
+ if (!nameField)
1062
+ return true;
1063
+ if (!params || typeof params[nameField] !== "string")
1064
+ return rejectMismatch("Mcp-Name");
1065
+ const name = header("mcp-name");
1066
+ if (!name || decodeMcpHeaderValue(name) !== params[nameField])
1067
+ return rejectMismatch("Mcp-Name");
1068
+ return true;
1069
+ }
1070
+ function acceptsMcpResponses(value) {
1071
+ const types = (Array.isArray(value) ? value : value ? [value] : [])
1072
+ .flatMap((header) => header.split(","))
1073
+ .filter((part) => !part.split(";").slice(1).some((parameter) => /^\s*q\s*=\s*0(?:\.0*)?\s*$/i.test(parameter)))
1074
+ .map((part) => part.split(";", 1)[0]?.trim().toLowerCase());
1075
+ return types.includes("application/json") && types.includes("text/event-stream");
1076
+ }
1077
+ function decodeMcpHeaderValue(value) {
1078
+ const normalized = value.replace(/^[\t ]+|[\t ]+$/g, "");
1079
+ const prefix = "=?base64?";
1080
+ if (!normalized.startsWith(prefix) || !normalized.endsWith("?=")) {
1081
+ return /^[\x20-\x7E\t]*$/.test(normalized) ? normalized : undefined;
1082
+ }
1083
+ const encoded = normalized.slice(prefix.length, -2);
1084
+ if (!encoded || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
1085
+ return undefined;
1086
+ }
1087
+ try {
1088
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(encoded, "base64"));
1089
+ }
1090
+ catch {
1091
+ return undefined;
1092
+ }
1093
+ }
716
1094
  function isRecord(value) {
717
1095
  return typeof value === "object" && value !== null && !Array.isArray(value);
718
1096
  }
1097
+ function normalizeCacheHint(value, field) {
1098
+ if (!isRecord(value))
1099
+ throw new TypeError(`${field} cacheHint must be an object`);
1100
+ const unknownField = Object.keys(value).find((key) => key !== "ttlMs" && key !== "cacheScope");
1101
+ if (unknownField)
1102
+ throw new TypeError(`${field} cacheHint has an unknown field: ${unknownField}`);
1103
+ const { ttlMs, cacheScope } = value;
1104
+ if (ttlMs !== undefined && (!Number.isSafeInteger(ttlMs) || ttlMs < 0)) {
1105
+ throw new TypeError(`${field} cacheHint.ttlMs must be a non-negative safe integer`);
1106
+ }
1107
+ if (cacheScope !== undefined && cacheScope !== "public" && cacheScope !== "private") {
1108
+ throw new TypeError(`${field} cacheHint.cacheScope must be public or private`);
1109
+ }
1110
+ return Object.freeze({
1111
+ ...(ttlMs === undefined ? {} : { ttlMs: ttlMs }),
1112
+ ...(cacheScope === undefined ? {} : { cacheScope }),
1113
+ });
1114
+ }
1115
+ function normalizeCacheHints(value, enabledMethods) {
1116
+ if (!isRecord(value))
1117
+ throw new TypeError("cacheHints must be an object");
1118
+ const allowed = new Set(CACHEABLE_METHODS);
1119
+ const normalized = {};
1120
+ for (const [method, hint] of Object.entries(value)) {
1121
+ if (!allowed.has(method))
1122
+ throw new TypeError(`cacheHints has an unknown method: ${method}`);
1123
+ if (hint === undefined)
1124
+ continue;
1125
+ if (!enabledMethods.has(method))
1126
+ throw new TypeError(`cacheHints cannot configure disabled method: ${method}`);
1127
+ normalized[method] = normalizeCacheHint(hint, `cacheHints.${method}`);
1128
+ }
1129
+ return Object.freeze(normalized);
1130
+ }
1131
+ function checkedInputResponses(responses) {
1132
+ if (!responses)
1133
+ return undefined;
1134
+ const checked = {};
1135
+ for (const [key, value] of Object.entries(responses)) {
1136
+ const elicitation = specTypeSchemas.ElicitResult["~standard"].validate(value);
1137
+ if ("value" in elicitation) {
1138
+ checked[key] = elicitation.value;
1139
+ continue;
1140
+ }
1141
+ }
1142
+ return checked;
1143
+ }
1144
+ function checkedProtocolValue(type, value) {
1145
+ let copy;
1146
+ try {
1147
+ assertJsonValue(value, new WeakSet(), true);
1148
+ const encoded = JSON.stringify(structuredClone(value));
1149
+ if (encoded === undefined)
1150
+ throw new TypeError("value is not JSON data");
1151
+ copy = JSON.parse(encoded);
1152
+ }
1153
+ catch {
1154
+ throw new TypeError(`${type} metadata must be JSON data`);
1155
+ }
1156
+ const schema = specTypeSchemas[type];
1157
+ const result = schema["~standard"].validate(copy);
1158
+ if (result instanceof Promise || "issues" in result) {
1159
+ throw new TypeError(`${type} metadata does not match the MCP schema`);
1160
+ }
1161
+ return deepFreeze(result.value);
1162
+ }
1163
+ function assertJsonValue(value, seen, allowUndefinedProperties = false) {
1164
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1165
+ return;
1166
+ if (typeof value === "number") {
1167
+ if (!Number.isFinite(value))
1168
+ throw new TypeError("number must be finite");
1169
+ return;
1170
+ }
1171
+ if (typeof value !== "object")
1172
+ throw new TypeError("value is not JSON data");
1173
+ if (seen.has(value))
1174
+ throw new TypeError("value is cyclic");
1175
+ seen.add(value);
1176
+ if (Array.isArray(value)) {
1177
+ if (Reflect.ownKeys(value).length !== value.length + 1) {
1178
+ throw new TypeError("JSON arrays must contain only indexed values");
1179
+ }
1180
+ for (let index = 0; index < value.length; index += 1) {
1181
+ if (!(index in value))
1182
+ throw new TypeError("array must not be sparse");
1183
+ assertJsonValue(value[index], seen);
1184
+ }
1185
+ seen.delete(value);
1186
+ return;
1187
+ }
1188
+ const prototype = Object.getPrototypeOf(value);
1189
+ if (prototype !== Object.prototype && prototype !== null) {
1190
+ throw new TypeError("object must be a plain JSON object");
1191
+ }
1192
+ if (Reflect.ownKeys(value).length !== Object.keys(value).length) {
1193
+ throw new TypeError("JSON object keys must be enumerable strings");
1194
+ }
1195
+ for (const child of Object.values(value)) {
1196
+ if (child === undefined && allowUndefinedProperties)
1197
+ continue;
1198
+ assertJsonValue(child, seen);
1199
+ }
1200
+ seen.delete(value);
1201
+ }
1202
+ function deepFreeze(value) {
1203
+ if (value === null || typeof value !== "object" || Object.isFrozen(value))
1204
+ return value;
1205
+ for (const child of Object.values(value))
1206
+ deepFreeze(child);
1207
+ return Object.freeze(value);
1208
+ }
1209
+ function jsonMetadataSchema(schema, io) {
1210
+ return { ...z.toJSONSchema(schema, { target: "draft-2020-12", io }) };
1211
+ }
1212
+ function promptArguments(schema) {
1213
+ const jsonSchema = jsonMetadataSchema(schema, "input");
1214
+ const properties = isRecord(jsonSchema.properties) ? jsonSchema.properties : {};
1215
+ const required = new Set(Array.isArray(jsonSchema.required) ? jsonSchema.required : []);
1216
+ return Object.freeze(Object.entries(properties).map(([name, property]) => Object.freeze({
1217
+ name,
1218
+ ...(isRecord(property) && typeof property.description === "string"
1219
+ ? { description: property.description }
1220
+ : {}),
1221
+ required: required.has(name),
1222
+ })));
1223
+ }
719
1224
  function sdkMetadataSchema(schema) {
720
1225
  return {
721
1226
  "~standard": {
@@ -723,12 +1228,63 @@ function sdkMetadataSchema(schema) {
723
1228
  vendor: "emseepea",
724
1229
  validate: (value) => ({ value }),
725
1230
  jsonSchema: {
726
- input: () => z.toJSONSchema(schema, { target: "draft-2020-12", io: "input" }),
727
- output: () => z.toJSONSchema(schema, { target: "draft-2020-12", io: "output" }),
1231
+ input: () => jsonMetadataSchema(schema, "input"),
1232
+ output: () => jsonMetadataSchema(schema, "output"),
728
1233
  },
729
1234
  },
730
1235
  };
731
1236
  }
1237
+ const MCP_HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
1238
+ function assertValidMcpHeaderAnnotations(schema) {
1239
+ const jsonSchema = z.toJSONSchema(schema, { target: "draft-2020-12", io: "input" });
1240
+ const names = new Set();
1241
+ function visit(value, path, reachable) {
1242
+ if (!isRecord(value))
1243
+ return;
1244
+ if ("x-mcp-header" in value) {
1245
+ const name = value["x-mcp-header"];
1246
+ const location = path.join(".") || "the schema root";
1247
+ if (!reachable || path.length === 0) {
1248
+ throw new TypeError(`Tool x-mcp-header at ${location} is not reachable through properties`);
1249
+ }
1250
+ if (typeof name !== "string" || !MCP_HEADER_NAME.test(name)) {
1251
+ throw new TypeError(`Tool x-mcp-header at ${location} must be a non-empty HTTP header token`);
1252
+ }
1253
+ if (value.type !== "string" && value.type !== "integer" && value.type !== "boolean") {
1254
+ throw new TypeError(`Tool x-mcp-header at ${location} must describe a string, integer, or boolean`);
1255
+ }
1256
+ const normalized = name.toLowerCase();
1257
+ if (names.has(normalized)) {
1258
+ throw new TypeError(`Tool x-mcp-header name is duplicated: ${name}`);
1259
+ }
1260
+ names.add(normalized);
1261
+ }
1262
+ if (isRecord(value.properties)) {
1263
+ for (const [property, propertySchema] of Object.entries(value.properties)) {
1264
+ visit(propertySchema, [...path, property], reachable);
1265
+ }
1266
+ }
1267
+ for (const key of ["prefixItems", "oneOf", "anyOf", "allOf"]) {
1268
+ const schemas = value[key];
1269
+ if (Array.isArray(schemas))
1270
+ for (const child of schemas)
1271
+ visit(child, path, false);
1272
+ }
1273
+ for (const key of [
1274
+ "items", "contains", "additionalProperties", "unevaluatedProperties",
1275
+ "unevaluatedItems", "propertyNames", "not", "if", "then", "else",
1276
+ ]) {
1277
+ visit(value[key], path, false);
1278
+ }
1279
+ for (const key of ["patternProperties", "dependentSchemas", "$defs", "definitions"]) {
1280
+ const schemas = value[key];
1281
+ if (isRecord(schemas))
1282
+ for (const child of Object.values(schemas))
1283
+ visit(child, path, false);
1284
+ }
1285
+ }
1286
+ visit(jsonSchema, [], true);
1287
+ }
732
1288
  function checkedCompletionHandlers(kind, complete, allowedNames) {
733
1289
  if (complete === undefined)
734
1290
  return new Map();
@@ -963,12 +1519,30 @@ function positiveInteger(field, value) {
963
1519
  throw new TypeError(`${field} must be a positive safe integer`);
964
1520
  return value;
965
1521
  }
1522
+ function callbackTimeout(callbackName, callback, timeoutName, value) {
1523
+ if (callback !== undefined && typeof callback !== "function")
1524
+ throw new TypeError(`${callbackName} must be a function`);
1525
+ if (value !== undefined && callback === undefined)
1526
+ throw new TypeError(`${timeoutName} requires ${callbackName}`);
1527
+ const timeout = positiveInteger(timeoutName, value === undefined ? 1_000 : value);
1528
+ if (timeout > 60_000)
1529
+ throw new TypeError(`${timeoutName} must not exceed 60000`);
1530
+ return timeout;
1531
+ }
966
1532
  function nonNegativePort(value) {
967
1533
  if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) {
968
1534
  throw new TypeError("port must be an integer from 0 to 65535");
969
1535
  }
970
1536
  return value;
971
1537
  }
1538
+ function assertStatelessInputRequired(result) {
1539
+ if (!result.inputRequests || result.requestState !== undefined) {
1540
+ throw new Error("Client-input requests must be stateless");
1541
+ }
1542
+ if (Object.values(result.inputRequests).some((request) => request.method !== "elicitation/create")) {
1543
+ throw new Error("Client-input request kind is not supported");
1544
+ }
1545
+ }
972
1546
  function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal) {
973
1547
  let serialized;
974
1548
  try {
@@ -987,14 +1561,52 @@ function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal)
987
1561
  function isLoopbackHost(host) {
988
1562
  return host === "127.0.0.1" || host === "::1" || host === "localhost";
989
1563
  }
990
- async function closeApp(app, timeoutMs) {
1564
+ async function closeApp(app, timeoutMs, flushTelemetry, flushTimeoutMs, finishTelemetry) {
991
1565
  let timer;
1566
+ let onClosed;
1567
+ const httpClosed = new Promise((resolve) => { onClosed = resolve; });
1568
+ app.server.once("close", onClosed);
992
1569
  const timeout = new Promise((resolve) => {
993
- timer = setTimeout(() => { app.server.closeAllConnections(); resolve(); }, timeoutMs);
1570
+ timer = setTimeout(() => {
1571
+ app.server.close();
1572
+ app.server.closeAllConnections();
1573
+ resolve();
1574
+ }, timeoutMs);
994
1575
  timer.unref();
995
1576
  });
996
- await Promise.race([app.close(), timeout]);
997
- if (timer)
998
- clearTimeout(timer);
1577
+ try {
1578
+ await Promise.race([app.close(), timeout]);
1579
+ }
1580
+ finally {
1581
+ if (timer)
1582
+ clearTimeout(timer);
1583
+ if (flushTelemetry) {
1584
+ const controller = new AbortController();
1585
+ const flushDeadline = Date.now() + flushTimeoutMs;
1586
+ const expired = new Promise((resolve) => {
1587
+ // Keep the process alive long enough to finish flushing after sockets close.
1588
+ timer = setTimeout(() => { controller.abort(); resolve(); }, flushTimeoutMs);
1589
+ });
1590
+ try {
1591
+ await Promise.race([
1592
+ httpClosed.then(async () => {
1593
+ await finishTelemetry?.();
1594
+ if (!controller.signal.aborted && Date.now() < flushDeadline) {
1595
+ await flushTelemetry({ signal: controller.signal });
1596
+ }
1597
+ }).catch(() => { }),
1598
+ expired,
1599
+ ]);
1600
+ }
1601
+ finally {
1602
+ if (timer)
1603
+ clearTimeout(timer);
1604
+ app.server.off("close", onClosed);
1605
+ }
1606
+ }
1607
+ else {
1608
+ app.server.off("close", onClosed);
1609
+ }
1610
+ }
999
1611
  }
1000
1612
  //# sourceMappingURL=index.js.map