@emseepea/server 0.8.1 → 0.10.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 +39 -0
- package/dist/index.d.ts +46 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +265 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,15 +4,17 @@ import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
|
4
4
|
import { McpServer, InMemoryServerEventBus, OAuthError, OAuthErrorCode, ProtocolError, ProtocolErrorCode, ResourceTemplate, bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, checkResourceAllowed, classifyInboundRequest, completable, createMcpHandler, createRequestStateCodec, 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
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
7
8
|
import { readdir, realpath } from "node:fs/promises";
|
|
8
9
|
import { isIP } from "node:net";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
+
import { isAbsolute, join } from "node:path";
|
|
10
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
import { installObservability, } from "./telemetry.js";
|
|
13
14
|
export { openTelemetry, structuredLogging, } from "./telemetry.js";
|
|
14
15
|
export * from "./ui.js";
|
|
15
16
|
export const inputRequired = Object.assign((spec) => sdkInputRequired(spec), {
|
|
17
|
+
roots: sdkInputRequired.listRoots,
|
|
16
18
|
elicit: sdkInputRequired.elicit,
|
|
17
19
|
elicitUrl: sdkInputRequired.elicitUrl,
|
|
18
20
|
});
|
|
@@ -23,6 +25,20 @@ export function inputResponse(responses, key) {
|
|
|
23
25
|
const response = sdkInputResponse(checkedInputResponses(responses), key);
|
|
24
26
|
return response.kind === "elicit" ? response : { kind: "missing" };
|
|
25
27
|
}
|
|
28
|
+
const checkedRootsResponses = new WeakSet();
|
|
29
|
+
/** Reads roots validated by the framework; missing keys return undefined. */
|
|
30
|
+
export function rootsResponse(responses, key) {
|
|
31
|
+
if (responses === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!checkedRootsResponses.has(responses))
|
|
34
|
+
throw new TypeError("Roots responses have not been checked");
|
|
35
|
+
if (!Object.hasOwn(responses, key))
|
|
36
|
+
return undefined;
|
|
37
|
+
const value = responses[key];
|
|
38
|
+
if (!isRecord(value) || !Array.isArray(value.roots))
|
|
39
|
+
throw new TypeError("Response is not roots");
|
|
40
|
+
return value.roots;
|
|
41
|
+
}
|
|
26
42
|
const PROTOCOL_VERSION = "2026-07-28";
|
|
27
43
|
const LEGACY_PROTOCOL_VERSIONS = Object.freeze([
|
|
28
44
|
"2025-11-25",
|
|
@@ -98,6 +114,51 @@ function createRequestStateRuntime(options) {
|
|
|
98
114
|
},
|
|
99
115
|
});
|
|
100
116
|
}
|
|
117
|
+
export function loadDeploymentProfile(environment = process.env) {
|
|
118
|
+
const mode = environment.EMSEEPEA_DEPLOYMENT_MODE;
|
|
119
|
+
const configPath = environment.EMSEEPEA_DEPLOYMENT_CONFIG_FILE;
|
|
120
|
+
if (mode === undefined) {
|
|
121
|
+
if (configPath !== undefined)
|
|
122
|
+
throw new TypeError("Deployment config requires an explicit deployment mode");
|
|
123
|
+
return { mode: "loopback" };
|
|
124
|
+
}
|
|
125
|
+
if (mode !== "production-behind-proxy")
|
|
126
|
+
throw new TypeError(`Unsupported deployment mode: ${mode}`);
|
|
127
|
+
if (!configPath || !isAbsolute(configPath)) {
|
|
128
|
+
throw new TypeError("Production deployment config must be an absolute file path");
|
|
129
|
+
}
|
|
130
|
+
const bytes = readFileSync(configPath);
|
|
131
|
+
if (bytes.byteLength > 16 * 1024)
|
|
132
|
+
throw new TypeError("Production deployment config exceeds 16 KiB");
|
|
133
|
+
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
134
|
+
const parsed = z.strictObject({
|
|
135
|
+
allowedAuthorities: z.array(z.string()).min(1),
|
|
136
|
+
allowedOrigins: z.array(z.string()).min(1),
|
|
137
|
+
trustedProxyAddresses: z.array(z.string()).min(1),
|
|
138
|
+
rateLimit: z.strictObject({
|
|
139
|
+
maxRequests: z.number().int().positive(),
|
|
140
|
+
windowMs: z.number().int().positive(),
|
|
141
|
+
maxClients: z.number().int().positive(),
|
|
142
|
+
}),
|
|
143
|
+
}).parse(JSON.parse(source));
|
|
144
|
+
const profile = { mode, ...parsed };
|
|
145
|
+
const normalized = normalizeDeployment(profile);
|
|
146
|
+
if (normalized.mode !== "production-behind-proxy")
|
|
147
|
+
throw new TypeError("Production deployment config is invalid");
|
|
148
|
+
for (const [label, values, normalizedValues] of [
|
|
149
|
+
["allowed authority", parsed.allowedAuthorities, normalized.allowedAuthorities],
|
|
150
|
+
["allowed origin", parsed.allowedOrigins, normalized.allowedOrigins],
|
|
151
|
+
["trusted proxy address", parsed.trustedProxyAddresses, normalized.trustedProxyAddresses],
|
|
152
|
+
]) {
|
|
153
|
+
if (normalizedValues.size !== values.length)
|
|
154
|
+
throw new TypeError(`Duplicate ${label}`);
|
|
155
|
+
}
|
|
156
|
+
for (const address of parsed.trustedProxyAddresses) {
|
|
157
|
+
if (normalizeIp(address) !== address)
|
|
158
|
+
throw new TypeError(`Trusted proxy address is not normalized: ${address}`);
|
|
159
|
+
}
|
|
160
|
+
return profile;
|
|
161
|
+
}
|
|
101
162
|
export function defineTool(definition) {
|
|
102
163
|
const handler = definition.handler;
|
|
103
164
|
return createCheckedTool(definition, handler, false, true);
|
|
@@ -173,13 +234,13 @@ export function defineResource(definition) {
|
|
|
173
234
|
value: listing,
|
|
174
235
|
}),
|
|
175
236
|
[HAS_COMPLETION]: false,
|
|
176
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState) {
|
|
237
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState, clientLogging) {
|
|
177
238
|
server.registerResource(name, uri, cacheHint ? { ...metadata, cacheHint } : metadata, async (_requestedUri, context) => {
|
|
178
239
|
try {
|
|
179
240
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
180
241
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
181
242
|
signal.throwIfAborted();
|
|
182
|
-
const result = await handler(directHandlerContext(access, context, signal, deadlineMs, requestState));
|
|
243
|
+
const result = await withClientLogReporter(context, signal, clientLogging, (reportLog) => handler(directHandlerContext(access, context, signal, deadlineMs, requestState, reportLog)));
|
|
183
244
|
signal.throwIfAborted();
|
|
184
245
|
if (isInputRequiredResult(result)) {
|
|
185
246
|
await assertInputRequired(result, requestState, context);
|
|
@@ -245,7 +306,7 @@ export function defineResourceTemplate(definition) {
|
|
|
245
306
|
value: listing,
|
|
246
307
|
}),
|
|
247
308
|
[HAS_COMPLETION]: completions.size > 0,
|
|
248
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState) {
|
|
309
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState, clientLogging) {
|
|
249
310
|
const registeredTemplate = completions.size === 0
|
|
250
311
|
? template
|
|
251
312
|
: new ResourceTemplate(uriTemplate, {
|
|
@@ -260,7 +321,7 @@ export function defineResourceTemplate(definition) {
|
|
|
260
321
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
261
322
|
return await runWithDeadline(context.mcpReq.signal, deadlineMs, async (signal) => {
|
|
262
323
|
signal.throwIfAborted();
|
|
263
|
-
const result = await handler({ uri: requestedUri.href, variables }, directHandlerContext(access, context, signal, deadlineMs, requestState));
|
|
324
|
+
const result = await withClientLogReporter(context, signal, clientLogging, (reportLog) => handler({ uri: requestedUri.href, variables }, directHandlerContext(access, context, signal, deadlineMs, requestState, reportLog)));
|
|
264
325
|
signal.throwIfAborted();
|
|
265
326
|
if (isInputRequiredResult(result)) {
|
|
266
327
|
await assertInputRequired(result, requestState, context);
|
|
@@ -311,7 +372,7 @@ export function definePrompt(definition) {
|
|
|
311
372
|
[PROMPT_ACCESS]: access,
|
|
312
373
|
[HAS_COMPLETION]: completions.size > 0,
|
|
313
374
|
[PROMPT_LISTING]: listing,
|
|
314
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState) {
|
|
375
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, requestState, clientLogging) {
|
|
315
376
|
server.registerPrompt(name, {
|
|
316
377
|
...metadata,
|
|
317
378
|
argsSchema: sdkPromptMetadataSchema(argsSchema, completions, argumentNames, timeoutMs, maxApplicationResultBytes),
|
|
@@ -323,7 +384,7 @@ export function definePrompt(definition) {
|
|
|
323
384
|
if (!parsedArgs.success)
|
|
324
385
|
throw new Error("Prompt received invalid arguments");
|
|
325
386
|
signal.throwIfAborted();
|
|
326
|
-
const result = await handler(parsedArgs.data, directHandlerContext(access, context, signal, deadlineMs, requestState));
|
|
387
|
+
const result = await withClientLogReporter(context, signal, clientLogging, (reportLog) => handler(parsedArgs.data, directHandlerContext(access, context, signal, deadlineMs, requestState, reportLog)));
|
|
327
388
|
signal.throwIfAborted();
|
|
328
389
|
if (isInputRequiredResult(result)) {
|
|
329
390
|
await assertInputRequired(result, requestState, context);
|
|
@@ -496,7 +557,10 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
496
557
|
[TOOL_ACCESS]: access,
|
|
497
558
|
[TOOL_STREAMING]: streaming,
|
|
498
559
|
[TOOL_LISTING]: listing,
|
|
499
|
-
[REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes, requestState) {
|
|
560
|
+
[REGISTER](server, timeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes, requestState, clientLogging) {
|
|
561
|
+
const runLoggedTool = clientLogging
|
|
562
|
+
? createLoggedToolRunner(clientLogging, execute, allowsInputRequired, access, requestState)
|
|
563
|
+
: undefined;
|
|
500
564
|
server.registerTool(name, metadata, async (input, context) => {
|
|
501
565
|
try {
|
|
502
566
|
const deadlineMs = requestOperations.getStore()?.deadlineMs ?? Date.now() + timeoutMs;
|
|
@@ -510,9 +574,12 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
510
574
|
? progressReporter(context, signal, maxProgressEvents, maxProgressEventBytes)
|
|
511
575
|
: undefined;
|
|
512
576
|
let result;
|
|
513
|
-
|
|
514
|
-
result = await
|
|
515
|
-
|
|
577
|
+
if (runLoggedTool) {
|
|
578
|
+
result = await runLoggedTool(parsedInput.data, context, signal, deadlineMs, reporter);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
try {
|
|
582
|
+
const handlerContext = allowsInputRequired
|
|
516
583
|
? directHandlerContext(access, context, signal, deadlineMs, requestState)
|
|
517
584
|
: {
|
|
518
585
|
signal,
|
|
@@ -520,14 +587,16 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
520
587
|
principal: access === "public"
|
|
521
588
|
? undefined
|
|
522
589
|
: principalFrom(context.http?.authInfo),
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
590
|
+
};
|
|
591
|
+
if (reporter)
|
|
592
|
+
Object.assign(handlerContext, { reportProgress: reporter.report });
|
|
593
|
+
result = await execute(parsedInput.data, handlerContext);
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
await reporter?.finish();
|
|
597
|
+
}
|
|
598
|
+
reporter?.throwIfFailed();
|
|
529
599
|
}
|
|
530
|
-
reporter?.throwIfFailed();
|
|
531
600
|
if (allowsInputRequired && isInputRequiredResult(result)) {
|
|
532
601
|
signal.throwIfAborted();
|
|
533
602
|
await assertInputRequired(result, requestState, context);
|
|
@@ -561,6 +630,33 @@ function createCheckedTool(definition, execute, streaming, allowsInputRequired)
|
|
|
561
630
|
};
|
|
562
631
|
return Object.freeze(registration);
|
|
563
632
|
}
|
|
633
|
+
function createLoggedToolRunner(clientLogging, execute, allowsInputRequired, access, requestState) {
|
|
634
|
+
return async (input, context, signal, deadlineMs, reporter) => {
|
|
635
|
+
const logReporter = clientLogReporter(context, signal, clientLogging.maxEvents, clientLogging.maxEventBytes);
|
|
636
|
+
let result;
|
|
637
|
+
try {
|
|
638
|
+
result = await execute(input, {
|
|
639
|
+
...(allowsInputRequired
|
|
640
|
+
? directHandlerContext(access, context, signal, deadlineMs, requestState, logReporter.report)
|
|
641
|
+
: {
|
|
642
|
+
signal,
|
|
643
|
+
deadlineMs,
|
|
644
|
+
principal: access === "public"
|
|
645
|
+
? undefined
|
|
646
|
+
: principalFrom(context.http?.authInfo),
|
|
647
|
+
reportLog: logReporter.report,
|
|
648
|
+
}),
|
|
649
|
+
...(reporter ? { reportProgress: reporter.report } : {}),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
await Promise.all([reporter?.finish(), logReporter.finish()]);
|
|
654
|
+
}
|
|
655
|
+
reporter?.throwIfFailed();
|
|
656
|
+
logReporter.throwIfFailed();
|
|
657
|
+
return result;
|
|
658
|
+
};
|
|
659
|
+
}
|
|
564
660
|
function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
565
661
|
const token = context.mcpReq._meta?.progressToken;
|
|
566
662
|
let closed = false;
|
|
@@ -616,6 +712,74 @@ function progressReporter(context, signal, maxEvents, maxEventBytes) {
|
|
|
616
712
|
throw failure; },
|
|
617
713
|
};
|
|
618
714
|
}
|
|
715
|
+
function clientLogReporter(context, signal, maxEvents, maxEventBytes) {
|
|
716
|
+
let closed = false;
|
|
717
|
+
let failure;
|
|
718
|
+
let attempts = 0;
|
|
719
|
+
const pending = new Set();
|
|
720
|
+
return {
|
|
721
|
+
report(message) {
|
|
722
|
+
if (closed)
|
|
723
|
+
return Promise.reject(new Error("Client logging is no longer available"));
|
|
724
|
+
if (failure)
|
|
725
|
+
return Promise.reject(failure);
|
|
726
|
+
const operation = (async () => {
|
|
727
|
+
try {
|
|
728
|
+
signal.throwIfAborted();
|
|
729
|
+
attempts += 1;
|
|
730
|
+
if (attempts > maxEvents)
|
|
731
|
+
throw new Error("Client log event limit exceeded");
|
|
732
|
+
const notification = {
|
|
733
|
+
method: "notifications/message",
|
|
734
|
+
params: {
|
|
735
|
+
level: message.level,
|
|
736
|
+
data: message.data,
|
|
737
|
+
...(message.logger === undefined ? {} : { logger: message.logger }),
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
assertJsonValue(notification, new WeakSet());
|
|
741
|
+
const encoded = JSON.stringify(notification);
|
|
742
|
+
if (Buffer.byteLength(encoded, "utf8") > maxEventBytes) {
|
|
743
|
+
throw new Error("Client log event exceeds configured size limit");
|
|
744
|
+
}
|
|
745
|
+
const parsed = specTypeSchemas.LoggingMessageNotification["~standard"].validate(JSON.parse(encoded));
|
|
746
|
+
if (parsed instanceof Promise || "issues" in parsed) {
|
|
747
|
+
throw new Error("Client log event is invalid");
|
|
748
|
+
}
|
|
749
|
+
const params = parsed.value.params;
|
|
750
|
+
await context.mcpReq.log(params.level, params.data, params.logger);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
failure = error instanceof Error ? error : new Error("Client log emission failed");
|
|
754
|
+
throw failure;
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
pending.add(operation);
|
|
758
|
+
void operation.then(() => pending.delete(operation), () => pending.delete(operation));
|
|
759
|
+
return operation;
|
|
760
|
+
},
|
|
761
|
+
async finish() {
|
|
762
|
+
closed = true;
|
|
763
|
+
await Promise.allSettled(pending);
|
|
764
|
+
},
|
|
765
|
+
throwIfFailed() { if (failure)
|
|
766
|
+
throw failure; },
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async function withClientLogReporter(context, signal, options, run) {
|
|
770
|
+
const reporter = options
|
|
771
|
+
? clientLogReporter(context, signal, options.maxEvents, options.maxEventBytes)
|
|
772
|
+
: undefined;
|
|
773
|
+
let result;
|
|
774
|
+
try {
|
|
775
|
+
result = await run(reporter?.report);
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
await reporter?.finish();
|
|
779
|
+
}
|
|
780
|
+
reporter?.throwIfFailed();
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
619
783
|
function catalogueListings(tools, resources, prompts, supportedTools = tools, supportedResources = resources, supportedPrompts = prompts) {
|
|
620
784
|
const catalogues = new Map();
|
|
621
785
|
if (supportedTools.length)
|
|
@@ -778,10 +942,25 @@ export function createEmseepea(options) {
|
|
|
778
942
|
? normalizeListPagination(options.listPagination)
|
|
779
943
|
: undefined;
|
|
780
944
|
const maxRequestBytes = positiveInteger("maxRequestBytes", options.maxRequestBytes ?? 1024 * 1024);
|
|
945
|
+
if (options.clientRoots !== undefined && !isRecord(options.clientRoots)) {
|
|
946
|
+
throw new TypeError("clientRoots must be an object");
|
|
947
|
+
}
|
|
948
|
+
const maxClientRoots = options.clientRoots === undefined ? undefined
|
|
949
|
+
: positiveInteger("clientRoots.maxRoots", options.clientRoots.maxRoots === undefined ? 100 : options.clientRoots.maxRoots);
|
|
781
950
|
const requestState = options.requestState
|
|
782
951
|
? createRequestStateRuntime(options.requestState)
|
|
783
952
|
: undefined;
|
|
784
953
|
const maxApplicationResultBytes = positiveInteger("maxApplicationResultBytes", options.maxApplicationResultBytes ?? 1024 * 1024);
|
|
954
|
+
const clientLoggingOptions = options.clientLogging;
|
|
955
|
+
if (clientLoggingOptions !== undefined && !isRecord(clientLoggingOptions)) {
|
|
956
|
+
throw new TypeError("clientLogging must be an object");
|
|
957
|
+
}
|
|
958
|
+
const clientLogging = clientLoggingOptions === undefined
|
|
959
|
+
? undefined
|
|
960
|
+
: Object.freeze({
|
|
961
|
+
maxEvents: positiveInteger("clientLogging.maxEvents", clientLoggingOptions.maxEvents ?? 32),
|
|
962
|
+
maxEventBytes: positiveInteger("clientLogging.maxEventBytes", clientLoggingOptions.maxEventBytes ?? 8 * 1024),
|
|
963
|
+
});
|
|
785
964
|
const maxProgressEvents = positiveInteger("maxProgressEvents", options.maxProgressEvents ?? 32);
|
|
786
965
|
const maxProgressEventBytes = positiveInteger("maxProgressEventBytes", options.maxProgressEventBytes ?? 8 * 1024);
|
|
787
966
|
const operationTimeoutMs = positiveInteger("operationTimeoutMs", options.operationTimeoutMs ?? 30_000);
|
|
@@ -861,6 +1040,7 @@ export function createEmseepea(options) {
|
|
|
861
1040
|
: discoverablePrompts;
|
|
862
1041
|
const activeHasCompletion = activeResources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
863
1042
|
activePrompts.some((prompt) => prompt[HAS_COMPLETION]);
|
|
1043
|
+
const activeClientLogging = request?.legacy ? undefined : clientLogging;
|
|
864
1044
|
const server = new McpServer(serverInfo, {
|
|
865
1045
|
capabilities: {
|
|
866
1046
|
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
@@ -872,6 +1052,7 @@ export function createEmseepea(options) {
|
|
|
872
1052
|
} : {}),
|
|
873
1053
|
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
874
1054
|
...(activeHasCompletion ? { completions: {} } : {}),
|
|
1055
|
+
...(activeClientLogging ? { logging: {} } : {}),
|
|
875
1056
|
},
|
|
876
1057
|
instructions: options.instructions,
|
|
877
1058
|
cacheHints: request?.filterCatalogues || request?.legacy ? undefined : cacheHints,
|
|
@@ -879,13 +1060,13 @@ export function createEmseepea(options) {
|
|
|
879
1060
|
...(requestState && !request?.legacy ? { requestState: { verify: requestState.verify } } : {}),
|
|
880
1061
|
});
|
|
881
1062
|
for (const tool of activeTools) {
|
|
882
|
-
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes, requestState);
|
|
1063
|
+
tool[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, maxProgressEvents, maxProgressEventBytes, requestState, activeClientLogging);
|
|
883
1064
|
}
|
|
884
1065
|
for (const resource of activeResources) {
|
|
885
|
-
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, requestState);
|
|
1066
|
+
resource[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, requestState, activeClientLogging);
|
|
886
1067
|
}
|
|
887
1068
|
for (const prompt of activePrompts) {
|
|
888
|
-
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, requestState);
|
|
1069
|
+
prompt[REGISTER](server, operationTimeoutMs, maxApplicationResultBytes, requestState, activeClientLogging);
|
|
889
1070
|
}
|
|
890
1071
|
const filteredCatalogues = request?.filterCatalogues
|
|
891
1072
|
? catalogueListings(activeDiscoverableTools, activeDiscoverableResources, activeDiscoverablePrompts, activeTools, activeResources, activePrompts)
|
|
@@ -903,7 +1084,7 @@ export function createEmseepea(options) {
|
|
|
903
1084
|
installListPagination(server, pagination);
|
|
904
1085
|
return server;
|
|
905
1086
|
}, {
|
|
906
|
-
responseMode: hasStreaming || resourceSubscriptions ? "auto" : "json",
|
|
1087
|
+
responseMode: hasStreaming || resourceSubscriptions || clientLogging ? "auto" : "json",
|
|
907
1088
|
keepAliveMs: 0,
|
|
908
1089
|
});
|
|
909
1090
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
@@ -1111,6 +1292,7 @@ export function createEmseepea(options) {
|
|
|
1111
1292
|
filterCatalogues: protectsCatalogue,
|
|
1112
1293
|
capability,
|
|
1113
1294
|
legacy,
|
|
1295
|
+
maxClientRoots,
|
|
1114
1296
|
}, () => nodeHandler(request.raw, reply.raw, request.body));
|
|
1115
1297
|
}
|
|
1116
1298
|
finally {
|
|
@@ -1313,7 +1495,7 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
1313
1495
|
const runtime = runtimes.get(app);
|
|
1314
1496
|
if (!runtime)
|
|
1315
1497
|
throw new TypeError("serveEmseepea requires an app created by createEmseepea");
|
|
1316
|
-
const host = options.host ?? "127.0.0.1";
|
|
1498
|
+
const host = options.host ?? (runtime.deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0");
|
|
1317
1499
|
if (runtime.deployment.mode === "loopback" && !isLoopbackHost(host)) {
|
|
1318
1500
|
throw new TypeError("The loopback deployment profile cannot bind publicly");
|
|
1319
1501
|
}
|
|
@@ -2083,31 +2265,88 @@ function nonNegativePort(value) {
|
|
|
2083
2265
|
}
|
|
2084
2266
|
return value;
|
|
2085
2267
|
}
|
|
2086
|
-
function directHandlerContext(access, context, signal, deadlineMs, requestState) {
|
|
2268
|
+
function directHandlerContext(access, context, signal, deadlineMs, requestState, reportLog) {
|
|
2087
2269
|
return {
|
|
2088
2270
|
signal,
|
|
2089
2271
|
deadlineMs,
|
|
2090
2272
|
principal: access === "public" ? undefined : principalFrom(context.http?.authInfo),
|
|
2091
|
-
inputResponses:
|
|
2273
|
+
inputResponses: checkedDirectInputResponses(context, signal, deadlineMs),
|
|
2092
2274
|
...(requestState ? {
|
|
2093
2275
|
requestState: context.mcpReq.requestState(),
|
|
2094
2276
|
mintRequestState: (payload) => requestState.mint(payload, context),
|
|
2095
2277
|
} : {}),
|
|
2278
|
+
...(reportLog ? { reportLog } : {}),
|
|
2096
2279
|
};
|
|
2097
2280
|
}
|
|
2098
2281
|
async function assertInputRequired(result, requestState, context) {
|
|
2099
2282
|
if (!result.inputRequests && result.requestState === undefined) {
|
|
2100
2283
|
throw new Error("Client-input result must include requests or state");
|
|
2101
2284
|
}
|
|
2102
|
-
if (result.inputRequests && Object.values(result.inputRequests).some((request) => request.method !== "elicitation/create")) {
|
|
2285
|
+
if (result.inputRequests && Object.values(result.inputRequests).some((request) => request.method !== "elicitation/create" && request.method !== "roots/list")) {
|
|
2103
2286
|
throw new Error("Client-input request kind is not supported");
|
|
2104
2287
|
}
|
|
2288
|
+
if (result.inputRequests && Object.values(result.inputRequests).some((request) => request.method === "roots/list")) {
|
|
2289
|
+
const operation = requestOperations.getStore();
|
|
2290
|
+
if (operation?.legacy || operation?.maxClientRoots === undefined) {
|
|
2291
|
+
throw new Error("Client roots are unavailable for this request");
|
|
2292
|
+
}
|
|
2293
|
+
// The SDK checks outgoing requests against this round's client capabilities.
|
|
2294
|
+
}
|
|
2105
2295
|
if (result.requestState !== undefined) {
|
|
2106
2296
|
if (!requestState)
|
|
2107
2297
|
throw new Error("Client-input request state is not configured");
|
|
2108
2298
|
await requestState.verify(result.requestState, context);
|
|
2109
2299
|
}
|
|
2110
2300
|
}
|
|
2301
|
+
function assertClientRootsEnabled(context) {
|
|
2302
|
+
const operation = requestOperations.getStore();
|
|
2303
|
+
const envelope = context.mcpReq.envelope;
|
|
2304
|
+
const capabilities = isRecord(envelope) ? envelope["io.modelcontextprotocol/clientCapabilities"] : undefined;
|
|
2305
|
+
if (operation?.legacy || operation?.maxClientRoots === undefined ||
|
|
2306
|
+
!isRecord(capabilities) || !isRecord(capabilities.roots)) {
|
|
2307
|
+
throw new Error("Client roots are unavailable for this request");
|
|
2308
|
+
}
|
|
2309
|
+
return operation.maxClientRoots;
|
|
2310
|
+
}
|
|
2311
|
+
function checkedDirectInputResponses(context, signal, deadlineMs) {
|
|
2312
|
+
const operation = requestOperations.getStore();
|
|
2313
|
+
if (operation?.maxClientRoots === undefined || operation.legacy) {
|
|
2314
|
+
return checkedInputResponses(context.mcpReq.inputResponses);
|
|
2315
|
+
}
|
|
2316
|
+
if (context.mcpReq.droppedInputResponseKeys?.length)
|
|
2317
|
+
throw new Error("Malformed client input response");
|
|
2318
|
+
const responses = context.mcpReq.inputResponses;
|
|
2319
|
+
if (responses === undefined)
|
|
2320
|
+
return undefined;
|
|
2321
|
+
const checked = Object.create(null);
|
|
2322
|
+
for (const [key, value] of Object.entries(responses)) {
|
|
2323
|
+
if (isRecord(value) && Object.hasOwn(value, "roots")) {
|
|
2324
|
+
const maxRoots = assertClientRootsEnabled(context);
|
|
2325
|
+
if (!Array.isArray(value.roots) || value.roots.length > maxRoots || Object.hasOwn(value, "action")) {
|
|
2326
|
+
throw new Error("Invalid roots response");
|
|
2327
|
+
}
|
|
2328
|
+
const result = specTypeSchemas.ListRootsResult["~standard"].validate(value);
|
|
2329
|
+
if (result instanceof Promise || "issues" in result)
|
|
2330
|
+
throw new Error("Invalid roots response");
|
|
2331
|
+
for (const root of result.value.roots) {
|
|
2332
|
+
if (new URL(root.uri).protocol !== "file:")
|
|
2333
|
+
throw new Error("Invalid root URI");
|
|
2334
|
+
}
|
|
2335
|
+
checked[key] = deepFreeze(structuredClone(result.value));
|
|
2336
|
+
}
|
|
2337
|
+
else {
|
|
2338
|
+
const result = specTypeSchemas.ElicitResult["~standard"].validate(value);
|
|
2339
|
+
if (result instanceof Promise || "issues" in result)
|
|
2340
|
+
throw new Error("Invalid client input response");
|
|
2341
|
+
checked[key] = deepFreeze(structuredClone(result.value));
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
signal.throwIfAborted();
|
|
2345
|
+
if (Date.now() >= deadlineMs)
|
|
2346
|
+
throw new Error("Client input validation exceeded its deadline");
|
|
2347
|
+
checkedRootsResponses.add(checked);
|
|
2348
|
+
return Object.freeze(checked);
|
|
2349
|
+
}
|
|
2111
2350
|
function assertResultSize(result, maxApplicationResultBytes, deadlineMs, signal) {
|
|
2112
2351
|
let serialized;
|
|
2113
2352
|
try {
|