stitchkit 0.43.1 → 0.44.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 +12 -4
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +18 -0
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/{index-310bfer5.js → index-36f1xkdb.js} +1 -1
- package/dist/{index-esqmem78.js → index-4fyn9f09.js} +2 -1
- package/dist/{index-h2wdcsby.js → index-mvjyq9h9.js} +176 -12
- package/dist/{index-gex6gxhe.js → index-yh0p5ddn.js} +34 -2
- package/dist/index.js +4 -1
- package/dist/internal/secure-fetch.d.ts +21 -1
- package/dist/internal/secure-fetch.d.ts.map +1 -1
- package/dist/node.js +2 -2
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +16 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +30 -1
- package/dist/observability/trace.d.ts +10 -0
- package/dist/observability/trace.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/types.d.ts +14 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +1 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/mcp-app.d.ts +3 -0
- package/dist/tools/mcp-app.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +34 -23
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp-round.d.ts +43 -0
- package/dist/tools/mcp-round.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.d.ts +11 -18
- package/dist/tools/mcp-stdio.d.ts.map +1 -1
- package/dist/tools/mcp-trace.d.ts +4 -0
- package/dist/tools/mcp-trace.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts +24 -3
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +2 -2
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts +1 -1
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts +3 -3
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +1 -1
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +3 -2
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/oauth-provider.d.ts +77 -7
- package/dist/tools/oauth-provider.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +27 -20
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/toolkit.d.ts +5 -5
- package/dist/tools/toolkit.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +1 -1
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +5 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +816 -197
- package/llms-full.txt +412 -78
- package/package.json +5 -4
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
inputIsQuery,
|
|
3
3
|
signJwt,
|
|
4
4
|
verifyPkce
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-36f1xkdb.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CORS_ALLOW_HEADERS
|
|
8
8
|
} from "./index-6jypn22c.js";
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createToolRunner,
|
|
17
17
|
executeToolMethod,
|
|
18
18
|
fetchGuarded,
|
|
19
|
+
fetchPinnedDocument,
|
|
19
20
|
flattenToolJsonSchema,
|
|
20
21
|
formatToolError,
|
|
21
22
|
isObjectPresentationSchema,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
toolErrorFromResult,
|
|
26
27
|
toolResultFromError,
|
|
27
28
|
writeDownload
|
|
28
|
-
} from "./index-
|
|
29
|
+
} from "./index-mvjyq9h9.js";
|
|
29
30
|
import {
|
|
30
31
|
toJsonSchema
|
|
31
32
|
} from "./index-frfyw9fa.js";
|
|
@@ -34,12 +35,15 @@ import {
|
|
|
34
35
|
} from "./index-x3fcszf8.js";
|
|
35
36
|
import {
|
|
36
37
|
AppError,
|
|
38
|
+
getRequestContext,
|
|
37
39
|
getTraceId,
|
|
38
40
|
isRecord,
|
|
39
41
|
mergeMeta,
|
|
40
42
|
parseTrailingWildcard,
|
|
43
|
+
resolvePropagationContext,
|
|
44
|
+
runWithRequestContext,
|
|
41
45
|
typedEntries
|
|
42
|
-
} from "./index-
|
|
46
|
+
} from "./index-yh0p5ddn.js";
|
|
43
47
|
|
|
44
48
|
// src/tools/agent.ts
|
|
45
49
|
import { jsonSchema, tool } from "ai";
|
|
@@ -54,7 +58,12 @@ function defineRuntimeTool(definition) {
|
|
|
54
58
|
function createRuntimeToolFactory(config) {
|
|
55
59
|
function parseContext(context) {
|
|
56
60
|
const parsed = config.context.parse(context);
|
|
57
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
...context,
|
|
63
|
+
...parsed,
|
|
64
|
+
params: undefined,
|
|
65
|
+
input: context.input
|
|
66
|
+
};
|
|
58
67
|
}
|
|
59
68
|
function define(definition) {
|
|
60
69
|
if (definition.output !== undefined) {
|
|
@@ -104,7 +113,8 @@ function runtimeToolIdentity(definition) {
|
|
|
104
113
|
scope: definition.identity.scope,
|
|
105
114
|
meta: definition.identity.meta,
|
|
106
115
|
annotations: definition.annotations,
|
|
107
|
-
ui: definition.ui
|
|
116
|
+
ui: definition.ui,
|
|
117
|
+
mcp: definition.mcp
|
|
108
118
|
};
|
|
109
119
|
}
|
|
110
120
|
function runtimeToolMountable(definition, assertName = true) {
|
|
@@ -334,7 +344,10 @@ function buildToolManifest(config) {
|
|
|
334
344
|
}));
|
|
335
345
|
}
|
|
336
346
|
// src/tools/mcp.ts
|
|
337
|
-
import {
|
|
347
|
+
import {
|
|
348
|
+
createRequestStateCodec,
|
|
349
|
+
McpServer
|
|
350
|
+
} from "@modelcontextprotocol/server";
|
|
338
351
|
import { z as z2 } from "zod";
|
|
339
352
|
|
|
340
353
|
// src/tools/mcp-app.ts
|
|
@@ -358,12 +371,271 @@ function inlineMcpAppBundle(html) {
|
|
|
358
371
|
return html.replace(EXT_APPS_BUNDLE_PLACEHOLDER, () => bundle);
|
|
359
372
|
}
|
|
360
373
|
|
|
374
|
+
// src/tools/mcp-round.ts
|
|
375
|
+
import {
|
|
376
|
+
acceptedContent,
|
|
377
|
+
CLIENT_INFO_META_KEY,
|
|
378
|
+
inputRequired,
|
|
379
|
+
inputResponse,
|
|
380
|
+
PROTOCOL_VERSION_META_KEY
|
|
381
|
+
} from "@modelcontextprotocol/server";
|
|
382
|
+
function transportContext(context, toolName, outcome, round) {
|
|
383
|
+
const protocolVersionValue = isRecord(context.mcpReq.envelope) ? Reflect.get(context.mcpReq.envelope, PROTOCOL_VERSION_META_KEY) : undefined;
|
|
384
|
+
const protocolVersion = typeof protocolVersionValue === "string" ? protocolVersionValue : undefined;
|
|
385
|
+
const clientInfoValue = isRecord(context.mcpReq.envelope) ? Reflect.get(context.mcpReq.envelope, CLIENT_INFO_META_KEY) : undefined;
|
|
386
|
+
const clientInfo = isRecord(clientInfoValue) && typeof clientInfoValue.name === "string" && typeof clientInfoValue.version === "string" ? { name: clientInfoValue.name, version: clientInfoValue.version } : undefined;
|
|
387
|
+
return {
|
|
388
|
+
signal: context.mcpReq.signal,
|
|
389
|
+
mcp: {
|
|
390
|
+
era: context.mcpReq.envelope ? "modern" : "legacy",
|
|
391
|
+
method: context.mcpReq.method,
|
|
392
|
+
toolName,
|
|
393
|
+
...protocolVersion !== undefined && { protocolVersion },
|
|
394
|
+
...clientInfo !== undefined && { clientInfo },
|
|
395
|
+
...outcome !== undefined && { outcome },
|
|
396
|
+
...round !== undefined && { round }
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function stableValue(value) {
|
|
401
|
+
if (Array.isArray(value))
|
|
402
|
+
return value.map(stableValue);
|
|
403
|
+
if (value === null || typeof value !== "object")
|
|
404
|
+
return value;
|
|
405
|
+
const result = {};
|
|
406
|
+
for (const key of Object.keys(value).sort()) {
|
|
407
|
+
result[key] = stableValue(Reflect.get(value, key));
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
async function argumentsDigest(args) {
|
|
412
|
+
const bytes = new TextEncoder().encode(JSON.stringify(stableValue(args)));
|
|
413
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
414
|
+
let binary = "";
|
|
415
|
+
for (const byte of digest)
|
|
416
|
+
binary += String.fromCodePoint(byte);
|
|
417
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
418
|
+
}
|
|
419
|
+
function operationIdentity(tool2) {
|
|
420
|
+
return {
|
|
421
|
+
toolName: tool2.name,
|
|
422
|
+
serviceName: tool2.method.serviceName,
|
|
423
|
+
action: tool2.method.key,
|
|
424
|
+
method: tool2.method.method,
|
|
425
|
+
...tool2.method.scope !== undefined && { scope: tool2.method.scope }
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function sameIdentity(left, right) {
|
|
429
|
+
return left.toolName === right.toolName && left.serviceName === right.serviceName && left.action === right.action && left.method === right.method && left.scope === right.scope;
|
|
430
|
+
}
|
|
431
|
+
async function runRoundSuccess(tool2, rawArgs, runTool, context) {
|
|
432
|
+
return runTool({
|
|
433
|
+
...tool2,
|
|
434
|
+
method: {
|
|
435
|
+
...tool2.method,
|
|
436
|
+
outputSchema: undefined,
|
|
437
|
+
handler: () => {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}, rawArgs, context);
|
|
442
|
+
}
|
|
443
|
+
async function runRoundFailure(tool2, rawArgs, runTool, context, code, message) {
|
|
444
|
+
return runTool({
|
|
445
|
+
...tool2,
|
|
446
|
+
method: {
|
|
447
|
+
...tool2.method,
|
|
448
|
+
outputSchema: undefined,
|
|
449
|
+
handler: () => {
|
|
450
|
+
throw new AppError(code, message, 400);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}, rawArgs, context);
|
|
454
|
+
}
|
|
455
|
+
function validatePolicy(tool2, policy, maxRounds) {
|
|
456
|
+
if (!Number.isInteger(maxRounds) || maxRounds < 1) {
|
|
457
|
+
throw new Error("[stitchkit] multiRound.serving.maxRounds must be a positive integer");
|
|
458
|
+
}
|
|
459
|
+
if (policy.inputRequired.length === 0) {
|
|
460
|
+
throw new Error(`[stitchkit] MCP tool "${tool2.name}" must declare at least one input round`);
|
|
461
|
+
}
|
|
462
|
+
if (policy.inputRequired.length > maxRounds) {
|
|
463
|
+
throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares ${policy.inputRequired.length} input rounds, exceeding maxRounds ${maxRounds}`);
|
|
464
|
+
}
|
|
465
|
+
const keys = new Set;
|
|
466
|
+
for (const request of policy.inputRequired) {
|
|
467
|
+
if (keys.has(request.key)) {
|
|
468
|
+
throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares duplicate input key "${request.key}"`);
|
|
469
|
+
}
|
|
470
|
+
keys.add(request.key);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function failedResolution(options) {
|
|
474
|
+
const result = await runRoundFailure(options.tool, options.rawArgs, options.runTool, options.context, options.code, options.message);
|
|
475
|
+
return { kind: "response", response: options.formatFailure(result) };
|
|
476
|
+
}
|
|
477
|
+
async function resolveMcpRound(options) {
|
|
478
|
+
const { policy } = options;
|
|
479
|
+
if (!policy) {
|
|
480
|
+
return {
|
|
481
|
+
kind: "continue",
|
|
482
|
+
context: transportContext(options.context, options.tool.name)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (!options.runtime) {
|
|
486
|
+
throw new Error(`[stitchkit] MCP tool "${options.tool.name}" declares inputRequired but no multiRound.state key is configured`);
|
|
487
|
+
}
|
|
488
|
+
validatePolicy(options.tool, policy, options.runtime.maxRounds);
|
|
489
|
+
const requests = policy.inputRequired;
|
|
490
|
+
const state = options.context.mcpReq.requestState();
|
|
491
|
+
const digest = await argumentsDigest(options.rawArgs);
|
|
492
|
+
if (!state) {
|
|
493
|
+
if (isRecord(options.context.mcpReq.inputResponses)) {
|
|
494
|
+
return failedResolution({
|
|
495
|
+
...options,
|
|
496
|
+
context: transportContext(options.context, options.tool.name, "invalid", 0),
|
|
497
|
+
code: "INVALID_REQUEST_STATE",
|
|
498
|
+
message: "Input responses require a valid continuation state"
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const guarded = await runRoundSuccess(options.tool, options.rawArgs, options.runTool, transportContext(options.context, options.tool.name, "input_required", 0));
|
|
502
|
+
if (!guarded.ok) {
|
|
503
|
+
return { kind: "response", response: options.formatFailure(guarded) };
|
|
504
|
+
}
|
|
505
|
+
const request2 = requests[0];
|
|
506
|
+
if (!request2)
|
|
507
|
+
throw new Error("[stitchkit] validated MRTR policy has no first round");
|
|
508
|
+
const requestState = await options.runtime.codec.mint({
|
|
509
|
+
identity: operationIdentity(options.tool),
|
|
510
|
+
argumentsDigest: digest,
|
|
511
|
+
round: 0,
|
|
512
|
+
accepted: {}
|
|
513
|
+
}, options.context);
|
|
514
|
+
return {
|
|
515
|
+
kind: "response",
|
|
516
|
+
response: inputRequired({
|
|
517
|
+
inputRequests: {
|
|
518
|
+
[request2.key]: inputRequired.elicit({
|
|
519
|
+
message: request2.message,
|
|
520
|
+
requestedSchema: request2.schema
|
|
521
|
+
})
|
|
522
|
+
},
|
|
523
|
+
requestState
|
|
524
|
+
})
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const invalidState = !sameIdentity(state.identity, operationIdentity(options.tool)) || state.argumentsDigest !== digest || !Number.isInteger(state.round) || state.round < 0 || state.round >= requests.length || state.round >= options.runtime.maxRounds || !isRecord(state.accepted);
|
|
528
|
+
if (invalidState) {
|
|
529
|
+
return failedResolution({
|
|
530
|
+
...options,
|
|
531
|
+
context: transportContext(options.context, options.tool.name, "invalid", state.round),
|
|
532
|
+
code: "INVALID_REQUEST_STATE",
|
|
533
|
+
message: "Continuation state does not match this operation, arguments or round"
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const request = requests[state.round];
|
|
537
|
+
if (!request)
|
|
538
|
+
throw new Error("[stitchkit] validated MRTR state points outside its policy");
|
|
539
|
+
const view = inputResponse(options.context.mcpReq.inputResponses, request.key);
|
|
540
|
+
if (view.kind !== "elicit") {
|
|
541
|
+
return failedResolution({
|
|
542
|
+
...options,
|
|
543
|
+
context: transportContext(options.context, options.tool.name, "invalid", state.round),
|
|
544
|
+
code: "INVALID_INPUT_RESPONSE",
|
|
545
|
+
message: "Expected an elicitation response for the current round"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (view.action !== "accept") {
|
|
549
|
+
const outcome = view.action === "decline" ? "declined" : "cancelled";
|
|
550
|
+
return failedResolution({
|
|
551
|
+
...options,
|
|
552
|
+
context: transportContext(options.context, options.tool.name, outcome, state.round),
|
|
553
|
+
code: view.action === "decline" ? "INPUT_DECLINED" : "INPUT_CANCELLED",
|
|
554
|
+
message: view.action === "decline" ? "Required input was declined" : "Required input was cancelled"
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const content = acceptedContent(options.context.mcpReq.inputResponses, request.key, request.schema);
|
|
558
|
+
if (!content) {
|
|
559
|
+
return failedResolution({
|
|
560
|
+
...options,
|
|
561
|
+
context: transportContext(options.context, options.tool.name, "invalid", state.round),
|
|
562
|
+
code: "INVALID_INPUT_RESPONSE",
|
|
563
|
+
message: "Accepted input failed its declared schema"
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const accepted = { ...state.accepted, [request.key]: content };
|
|
567
|
+
const nextRound = state.round + 1;
|
|
568
|
+
const nextRequest = requests[nextRound];
|
|
569
|
+
if (nextRequest) {
|
|
570
|
+
const guarded = await runRoundSuccess(options.tool, options.rawArgs, options.runTool, transportContext(options.context, options.tool.name, "input_required", nextRound));
|
|
571
|
+
if (!guarded.ok) {
|
|
572
|
+
return { kind: "response", response: options.formatFailure(guarded) };
|
|
573
|
+
}
|
|
574
|
+
const requestState = await options.runtime.codec.mint({
|
|
575
|
+
identity: state.identity,
|
|
576
|
+
argumentsDigest: state.argumentsDigest,
|
|
577
|
+
round: nextRound,
|
|
578
|
+
accepted
|
|
579
|
+
}, options.context);
|
|
580
|
+
return {
|
|
581
|
+
kind: "response",
|
|
582
|
+
response: inputRequired({
|
|
583
|
+
inputRequests: {
|
|
584
|
+
[nextRequest.key]: inputRequired.elicit({
|
|
585
|
+
message: nextRequest.message,
|
|
586
|
+
requestedSchema: nextRequest.schema
|
|
587
|
+
})
|
|
588
|
+
},
|
|
589
|
+
requestState
|
|
590
|
+
})
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
kind: "continue",
|
|
595
|
+
context: {
|
|
596
|
+
...transportContext(options.context, options.tool.name, "complete", state.round),
|
|
597
|
+
mcpInput: accepted
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/tools/mcp-trace.ts
|
|
603
|
+
import {
|
|
604
|
+
BAGGAGE_META_KEY,
|
|
605
|
+
TRACEPARENT_META_KEY,
|
|
606
|
+
TRACESTATE_META_KEY
|
|
607
|
+
} from "@modelcontextprotocol/server";
|
|
608
|
+
function runInMcpRequestContext(context, toolName, body) {
|
|
609
|
+
const ambient = getRequestContext();
|
|
610
|
+
const metadata = context.mcpReq._meta;
|
|
611
|
+
const propagation = metadata ? {
|
|
612
|
+
traceparent: metadata[TRACEPARENT_META_KEY],
|
|
613
|
+
tracestate: metadata[TRACESTATE_META_KEY],
|
|
614
|
+
baggage: metadata[BAGGAGE_META_KEY]
|
|
615
|
+
} : undefined;
|
|
616
|
+
const request = {
|
|
617
|
+
...ambient ?? {
|
|
618
|
+
source: "mcp",
|
|
619
|
+
method: "MCP",
|
|
620
|
+
path: `/mcp/${toolName}`,
|
|
621
|
+
startedAt: process.hrtime.bigint()
|
|
622
|
+
},
|
|
623
|
+
trace: resolvePropagationContext(propagation, ambient?.trace),
|
|
624
|
+
source: "mcp",
|
|
625
|
+
method: "MCP",
|
|
626
|
+
path: `/mcp/${toolName}`,
|
|
627
|
+
startedAt: process.hrtime.bigint(),
|
|
628
|
+
dimensions: ambient?.dimensions ? { ...ambient.dimensions } : undefined,
|
|
629
|
+
error: undefined
|
|
630
|
+
};
|
|
631
|
+
return runWithRequestContext(request, body);
|
|
632
|
+
}
|
|
633
|
+
|
|
361
634
|
// src/tools/native-mcp.ts
|
|
362
|
-
import {
|
|
635
|
+
import {
|
|
636
|
+
isCallToolResult
|
|
637
|
+
} from "@modelcontextprotocol/server";
|
|
363
638
|
import { z } from "zod";
|
|
364
|
-
function isCallToolResult(value) {
|
|
365
|
-
return CallToolResultSchema.safeParse(value).success;
|
|
366
|
-
}
|
|
367
639
|
function mountPreparedRuntimeMcp(server, tools, config) {
|
|
368
640
|
for (const { definition, descriptor } of tools) {
|
|
369
641
|
const { mountable } = descriptor;
|
|
@@ -381,35 +653,47 @@ function mountPreparedRuntimeMcp(server, tools, config) {
|
|
|
381
653
|
"ui/resourceUri": definition.ui.resourceUri
|
|
382
654
|
};
|
|
383
655
|
}
|
|
384
|
-
server.registerTool(definition.name, toolConfig, async (rawArgs) => {
|
|
656
|
+
server.registerTool(definition.name, toolConfig, async (rawArgs, mcpContext) => runInMcpRequestContext(mcpContext, definition.name, async () => {
|
|
385
657
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
386
|
-
const
|
|
387
|
-
|
|
658
|
+
const runTool = (tool2, toolArgs, roundContext) => executeToolMethod(tool2.method, definition.name, toolArgs, { ...config.context, ...roundContext, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined);
|
|
659
|
+
const round = await resolveMcpRound({
|
|
660
|
+
tool: mountable,
|
|
661
|
+
rawArgs: args,
|
|
662
|
+
context: mcpContext,
|
|
663
|
+
policy: mountable.method.mcp,
|
|
664
|
+
runtime: config.multiRoundRuntime,
|
|
665
|
+
runTool,
|
|
666
|
+
formatFailure: (result) => config.formatResult(result, "none", definition.name)
|
|
667
|
+
});
|
|
668
|
+
if (round.kind === "response")
|
|
669
|
+
return round.response;
|
|
670
|
+
if (!definition.present?.mcp) {
|
|
671
|
+
const result = await runTool(mountable, args, round.context);
|
|
388
672
|
return config.formatResult(result, descriptor.outputMode, definition.name);
|
|
389
673
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
...structured && { structuredContent: structured }
|
|
404
|
-
};
|
|
405
|
-
if (isCallToolResult(response))
|
|
674
|
+
const presentedResult = await executeToolMethod(mountable.method, definition.name, args, { ...config.context, ...round.context, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined, undefined, async (data) => {
|
|
675
|
+
const presented = await definition.present?.mcp?.(data);
|
|
676
|
+
if (!presented) {
|
|
677
|
+
throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter did not return a result", 500);
|
|
678
|
+
}
|
|
679
|
+
if ("structuredContent" in presented || "isError" in presented) {
|
|
680
|
+
throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter cannot set framework-owned structuredContent or isError", 500);
|
|
681
|
+
}
|
|
682
|
+
const structured = config.formatResult({ ok: true, data }, descriptor.outputMode, definition.name).structuredContent;
|
|
683
|
+
const response = structured === undefined ? presented : { ...presented, structuredContent: structured };
|
|
684
|
+
if (!isCallToolResult(response)) {
|
|
685
|
+
throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter returned an invalid CallToolResult", 500);
|
|
686
|
+
}
|
|
406
687
|
return response;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
688
|
+
});
|
|
689
|
+
if (!presentedResult.ok) {
|
|
690
|
+
return config.formatResult(presentedResult, "none", definition.name);
|
|
691
|
+
}
|
|
692
|
+
if (!isCallToolResult(presentedResult.data)) {
|
|
693
|
+
throw new Error("[stitchkit] Runtime MCP presenter invariant failed");
|
|
694
|
+
}
|
|
695
|
+
return presentedResult.data;
|
|
696
|
+
}));
|
|
413
697
|
}
|
|
414
698
|
}
|
|
415
699
|
|
|
@@ -576,14 +860,13 @@ function textBlock(text) {
|
|
|
576
860
|
}
|
|
577
861
|
function formatMcpResult(result, mode, toolName, errorHint) {
|
|
578
862
|
if (result.ok) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
863
|
+
if (mode === "none")
|
|
864
|
+
return { content: [] };
|
|
865
|
+
const serialized = JSON.stringify(result.data, null, 2);
|
|
866
|
+
if (serialized === undefined) {
|
|
867
|
+
throw new Error(`[stitchkit] MCP tool "${toolName ?? "unknown"}" produced a non-JSON output`);
|
|
582
868
|
}
|
|
583
|
-
|
|
584
|
-
return { content, structuredContent: result.data };
|
|
585
|
-
}
|
|
586
|
-
return { content };
|
|
869
|
+
return { content: textBlock(serialized), structuredContent: result.data };
|
|
587
870
|
}
|
|
588
871
|
return {
|
|
589
872
|
content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
|
|
@@ -601,10 +884,7 @@ function probeSchema(schema, io) {
|
|
|
601
884
|
function resolveOutputSchema(outputSchema) {
|
|
602
885
|
if (!outputSchema)
|
|
603
886
|
return null;
|
|
604
|
-
|
|
605
|
-
return { schema: outputSchema, mode: "direct" };
|
|
606
|
-
}
|
|
607
|
-
return { schema: z2.object({ result: outputSchema }), mode: "wrapped" };
|
|
887
|
+
return { schema: outputSchema, mode: "direct" };
|
|
608
888
|
}
|
|
609
889
|
function reportIncompatible(message, policy, logger, failures) {
|
|
610
890
|
if (policy === "throw") {
|
|
@@ -774,15 +1054,26 @@ function mountPreparedMcp(mcpServer, prepared, config = {}) {
|
|
|
774
1054
|
"ui/resourceUri": mountable.method.ui.resourceUri
|
|
775
1055
|
};
|
|
776
1056
|
}
|
|
777
|
-
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
1057
|
+
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs, mcpContext) => runInMcpRequestContext(mcpContext, mountable.name, async () => {
|
|
778
1058
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
779
1059
|
try {
|
|
780
|
-
const
|
|
1060
|
+
const round = await resolveMcpRound({
|
|
1061
|
+
tool: mountable,
|
|
1062
|
+
rawArgs: args,
|
|
1063
|
+
context: mcpContext,
|
|
1064
|
+
policy: mountable.method.mcp,
|
|
1065
|
+
runtime: config.multiRoundRuntime,
|
|
1066
|
+
runTool,
|
|
1067
|
+
formatFailure: (result2) => formatMcpResult(result2, "none", mountable.name, config.errorHint)
|
|
1068
|
+
});
|
|
1069
|
+
if (round.kind === "response")
|
|
1070
|
+
return round.response;
|
|
1071
|
+
const result = await runTool(mountable, args, round.context);
|
|
781
1072
|
return formatMcpResult(result, descriptor.outputMode, mountable.name, config.errorHint);
|
|
782
1073
|
} catch (err) {
|
|
783
1074
|
return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
|
|
784
1075
|
}
|
|
785
|
-
});
|
|
1076
|
+
}));
|
|
786
1077
|
}
|
|
787
1078
|
}
|
|
788
1079
|
function buildMcpServer(config, auth) {
|
|
@@ -810,7 +1101,29 @@ function buildMcpServer(config, auth) {
|
|
|
810
1101
|
return buildMcpServerFromPrepared(config, auth, prepared);
|
|
811
1102
|
}
|
|
812
1103
|
function buildMcpServerFromPrepared(config, auth, prepared) {
|
|
813
|
-
|
|
1104
|
+
let roundCodec;
|
|
1105
|
+
if (config.multiRound) {
|
|
1106
|
+
const principal = config.multiRound.state.principal(auth);
|
|
1107
|
+
roundCodec = createRequestStateCodec({
|
|
1108
|
+
key: config.multiRound.state.key,
|
|
1109
|
+
ttlSeconds: config.multiRound.state.ttlSeconds,
|
|
1110
|
+
bind: (mcpContext) => `${mcpContext.mcpReq.method}\x00${principal}`
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const dynamicSurface = !config.surfaces && (typeof config.services === "function" || typeof config.runtimeTools === "function");
|
|
1114
|
+
const serverOptions = {
|
|
1115
|
+
...config.instructions !== undefined && { instructions: config.instructions },
|
|
1116
|
+
...!dynamicSurface && config.cache?.operations !== undefined && {
|
|
1117
|
+
cacheHints: config.cache.operations
|
|
1118
|
+
},
|
|
1119
|
+
...config.multiRound?.serving !== undefined && {
|
|
1120
|
+
inputRequired: config.multiRound.serving
|
|
1121
|
+
},
|
|
1122
|
+
...roundCodec !== undefined && {
|
|
1123
|
+
requestState: { verify: roundCodec.verify }
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
const server = new McpServer(config.serverInfo, serverOptions);
|
|
814
1127
|
const context = config.context?.(auth);
|
|
815
1128
|
mountPreparedMcp(server, prepared.contractTools, {
|
|
816
1129
|
context,
|
|
@@ -822,7 +1135,13 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
|
|
|
822
1135
|
coerceJsonArgs: config.coerceJsonArgs,
|
|
823
1136
|
flattenUnionInput: config.flattenUnionInput,
|
|
824
1137
|
errorHint: config.errorHint,
|
|
825
|
-
onOutputStrip: config.onOutputStrip
|
|
1138
|
+
onOutputStrip: config.onOutputStrip,
|
|
1139
|
+
...roundCodec !== undefined && {
|
|
1140
|
+
multiRoundRuntime: {
|
|
1141
|
+
codec: roundCodec,
|
|
1142
|
+
maxRounds: config.multiRound?.serving?.maxRounds ?? 10
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
826
1145
|
});
|
|
827
1146
|
mountPreparedRuntimeMcp(server, prepared.runtimeTools, {
|
|
828
1147
|
context,
|
|
@@ -830,7 +1149,13 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
|
|
|
830
1149
|
lifecycle: config.lifecycle,
|
|
831
1150
|
coerceJsonArgs: config.coerceJsonArgs,
|
|
832
1151
|
onOutputStrip: config.onOutputStrip,
|
|
833
|
-
formatResult: (result, mode, toolName) => formatMcpResult(result, mode, toolName, config.errorHint)
|
|
1152
|
+
formatResult: (result, mode, toolName) => formatMcpResult(result, mode, toolName, config.errorHint),
|
|
1153
|
+
...roundCodec !== undefined && {
|
|
1154
|
+
multiRoundRuntime: {
|
|
1155
|
+
codec: roundCodec,
|
|
1156
|
+
maxRounds: config.multiRound?.serving?.maxRounds ?? 10
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
834
1159
|
});
|
|
835
1160
|
config.rawTools?.(server, auth);
|
|
836
1161
|
for (const resource of config.resources ?? []) {
|
|
@@ -839,7 +1164,10 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
|
|
|
839
1164
|
return server;
|
|
840
1165
|
}
|
|
841
1166
|
function mountMcpResource(server, resource) {
|
|
842
|
-
server.registerResource(resource.name, resource.uri, {
|
|
1167
|
+
server.registerResource(resource.name, resource.uri, {
|
|
1168
|
+
mimeType: resource.mimeType ?? RESOURCE_MIME_TYPE,
|
|
1169
|
+
...resource.cacheHint !== undefined && { cacheHint: resource.cacheHint }
|
|
1170
|
+
}, async () => ({
|
|
843
1171
|
contents: [
|
|
844
1172
|
{
|
|
845
1173
|
uri: resource.uri,
|
|
@@ -851,10 +1179,11 @@ function mountMcpResource(server, resource) {
|
|
|
851
1179
|
}));
|
|
852
1180
|
}
|
|
853
1181
|
// src/tools/mcp-handler.ts
|
|
854
|
-
import { randomUUID } from "node:crypto";
|
|
855
1182
|
import {
|
|
856
|
-
|
|
857
|
-
|
|
1183
|
+
createMcpHandler as createSdkMcpHandler,
|
|
1184
|
+
hostHeaderValidationResponse,
|
|
1185
|
+
originValidationResponse
|
|
1186
|
+
} from "@modelcontextprotocol/server";
|
|
858
1187
|
|
|
859
1188
|
// src/tools/oauth-metadata.ts
|
|
860
1189
|
var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
@@ -897,53 +1226,26 @@ function oauthProtectedResourceRoute(config) {
|
|
|
897
1226
|
}
|
|
898
1227
|
|
|
899
1228
|
// src/tools/mcp-handler.ts
|
|
900
|
-
var EVENT_TTL_MS = 10 * 60 * 1000;
|
|
901
|
-
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
902
|
-
var SWEEP_INTERVAL_MS = 60 * 1000;
|
|
903
|
-
var MAX_EVENTS = 1e4;
|
|
904
|
-
var MAX_SESSIONS = 1000;
|
|
905
|
-
|
|
906
|
-
class InMemoryEventStore {
|
|
907
|
-
events = new Map;
|
|
908
|
-
counter = 0;
|
|
909
|
-
async storeEvent(streamId, message) {
|
|
910
|
-
const eventId = String(++this.counter);
|
|
911
|
-
this.events.set(eventId, { streamId, message, timestamp: Date.now() });
|
|
912
|
-
if (this.events.size > MAX_EVENTS) {
|
|
913
|
-
const oldest = this.events.keys().next().value;
|
|
914
|
-
if (oldest !== undefined)
|
|
915
|
-
this.events.delete(oldest);
|
|
916
|
-
}
|
|
917
|
-
return eventId;
|
|
918
|
-
}
|
|
919
|
-
async getStreamIdForEventId(eventId) {
|
|
920
|
-
return this.events.get(eventId)?.streamId;
|
|
921
|
-
}
|
|
922
|
-
async replayEventsAfter(lastEventId, { send }) {
|
|
923
|
-
const anchor = this.events.get(lastEventId);
|
|
924
|
-
if (!anchor)
|
|
925
|
-
return "";
|
|
926
|
-
const lastIdNum = Number(lastEventId);
|
|
927
|
-
for (const [eventId, event] of this.events) {
|
|
928
|
-
if (event.streamId === anchor.streamId && Number(eventId) > lastIdNum) {
|
|
929
|
-
await send(eventId, event.message);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
return anchor.streamId;
|
|
933
|
-
}
|
|
934
|
-
cleanup() {
|
|
935
|
-
const cutoff = Date.now() - EVENT_TTL_MS;
|
|
936
|
-
for (const [eventId, event] of this.events) {
|
|
937
|
-
if (event.timestamp < cutoff)
|
|
938
|
-
this.events.delete(eventId);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
1229
|
function jsonRpcError(code, message, status, headers) {
|
|
943
1230
|
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
|
|
944
1231
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
1232
|
+
function defaultHostRejection(request) {
|
|
1233
|
+
const host = request.headers.get("host");
|
|
1234
|
+
if (!host || host === new URL(request.url).host)
|
|
1235
|
+
return;
|
|
1236
|
+
return jsonRpcError(-32000, "Invalid Host header", 403);
|
|
1237
|
+
}
|
|
1238
|
+
function defaultOriginRejection(request) {
|
|
1239
|
+
const origin = request.headers.get("origin");
|
|
1240
|
+
if (!origin)
|
|
1241
|
+
return;
|
|
1242
|
+
try {
|
|
1243
|
+
if (new URL(origin).origin === new URL(request.url).origin)
|
|
1244
|
+
return;
|
|
1245
|
+
} catch {}
|
|
1246
|
+
return jsonRpcError(-32000, "Invalid Origin header", 403);
|
|
1247
|
+
}
|
|
1248
|
+
function prepareStaticSurface(config) {
|
|
947
1249
|
const preparation = {
|
|
948
1250
|
logger: config.logger,
|
|
949
1251
|
extend: config.extend,
|
|
@@ -951,7 +1253,6 @@ function createMcpHandler(config) {
|
|
|
951
1253
|
schemaValidation: config.schemaValidation
|
|
952
1254
|
};
|
|
953
1255
|
if (config.surfaces && config.selectSurface) {
|
|
954
|
-
const selectSurface = config.selectSurface;
|
|
955
1256
|
const preparedByKey = new Map;
|
|
956
1257
|
const preparedBySurface = new WeakMap;
|
|
957
1258
|
for (const key in config.surfaces) {
|
|
@@ -966,110 +1267,111 @@ function createMcpHandler(config) {
|
|
|
966
1267
|
preparedBySurface.set(surface, prepared);
|
|
967
1268
|
preparedByKey.set(key, prepared);
|
|
968
1269
|
}
|
|
969
|
-
|
|
970
|
-
const key = selectSurface(auth);
|
|
1270
|
+
return (auth) => {
|
|
1271
|
+
const key = config.selectSurface(auth);
|
|
971
1272
|
const prepared = preparedByKey.get(key);
|
|
972
1273
|
if (!prepared)
|
|
973
1274
|
throw new Error(`[stitchkit] Unknown MCP surface "${key}"`);
|
|
974
1275
|
return buildMcpServerFromPrepared(config, auth, prepared);
|
|
975
1276
|
};
|
|
976
|
-
}
|
|
1277
|
+
}
|
|
1278
|
+
if (config.services && typeof config.services !== "function" && typeof config.runtimeTools !== "function") {
|
|
977
1279
|
const prepared = prepareMcpServerSurface({ services: config.services, runtimeTools: config.runtimeTools }, preparation);
|
|
978
|
-
|
|
979
|
-
} else {
|
|
980
|
-
buildServer = (auth) => buildMcpServer(config, auth);
|
|
1280
|
+
return (auth) => buildMcpServerFromPrepared(config, auth, prepared);
|
|
981
1281
|
}
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
function createMcpHandler(config) {
|
|
1285
|
+
const buildPrepared = prepareStaticSurface(config);
|
|
1286
|
+
const authByCarrier = new WeakMap;
|
|
1287
|
+
let closed = false;
|
|
1288
|
+
let resolveClosed;
|
|
1289
|
+
const closedSignal = new Promise((resolve) => {
|
|
1290
|
+
resolveClosed = resolve;
|
|
1291
|
+
});
|
|
1292
|
+
const sdkHandler = createSdkMcpHandler(({ authInfo }) => {
|
|
1293
|
+
const resolved = authInfo ? authByCarrier.get(authInfo) : undefined;
|
|
1294
|
+
if (!resolved) {
|
|
1295
|
+
throw new Error("[stitchkit] MCP request reached the SDK without resolved auth");
|
|
1296
|
+
}
|
|
1297
|
+
const auth = resolved.value;
|
|
1298
|
+
return buildPrepared ? buildPrepared(auth) : buildMcpServer(config, auth);
|
|
1299
|
+
}, {
|
|
1300
|
+
legacy: config.legacy === "reject" ? "reject" : "stateless",
|
|
1301
|
+
maxSubscriptions: 0
|
|
1302
|
+
});
|
|
982
1303
|
const unauthorized = () => {
|
|
983
1304
|
const headers = config.protectedResource ? { "WWW-Authenticate": wwwAuthenticateHeader(config.protectedResource.resource) } : undefined;
|
|
984
1305
|
return jsonRpcError(-32001, "Authorization required", 401, headers);
|
|
985
1306
|
};
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
992
|
-
sessionIdGenerator: undefined,
|
|
993
|
-
enableJsonResponse: true
|
|
994
|
-
});
|
|
995
|
-
const server = buildServer(auth);
|
|
996
|
-
await server.connect(transport);
|
|
997
|
-
return transport.handleRequest(req);
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
const eventStore = new InMemoryEventStore;
|
|
1001
|
-
const sessions = new Map;
|
|
1002
|
-
const closeTransport = (transport) => {
|
|
1003
|
-
transport.close().catch((err) => {
|
|
1004
|
-
console.error("[stitchkit] MCP transport close failed:", err);
|
|
1307
|
+
const observeRejection = (request, response) => {
|
|
1308
|
+
if (response.status < 400 || !config.onTransportRejected)
|
|
1309
|
+
return response;
|
|
1310
|
+
Promise.resolve(config.onTransportRejected({ request, response: response.clone() })).catch(() => {
|
|
1311
|
+
return;
|
|
1005
1312
|
});
|
|
1313
|
+
return response;
|
|
1006
1314
|
};
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (session.lastSeen < cutoff) {
|
|
1012
|
-
sessions.delete(id);
|
|
1013
|
-
closeTransport(session.transport);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}, SWEEP_INTERVAL_MS).unref();
|
|
1017
|
-
return async (req) => {
|
|
1018
|
-
const auth = await config.auth(req);
|
|
1019
|
-
if (!auth)
|
|
1020
|
-
return unauthorized();
|
|
1021
|
-
const sessionId = req.headers.get("mcp-session-id");
|
|
1022
|
-
if (sessionId) {
|
|
1023
|
-
const existing = sessions.get(sessionId);
|
|
1024
|
-
if (!existing) {
|
|
1025
|
-
return jsonRpcError(-32001, "Session not found", 404);
|
|
1026
|
-
}
|
|
1027
|
-
existing.lastSeen = Date.now();
|
|
1028
|
-
if (req.method === "GET") {
|
|
1029
|
-
existing.transport.closeStandaloneSSEStream();
|
|
1315
|
+
return {
|
|
1316
|
+
fetch: async (request) => {
|
|
1317
|
+
if (closed) {
|
|
1318
|
+
return observeRejection(request, jsonRpcError(-32000, "MCP handler is closed", 503));
|
|
1030
1319
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1320
|
+
const hostRejection = config.security?.allowedHosts ? hostHeaderValidationResponse(request, [...config.security.allowedHosts]) : defaultHostRejection(request);
|
|
1321
|
+
if (hostRejection)
|
|
1322
|
+
return observeRejection(request, hostRejection);
|
|
1323
|
+
const originRejection = config.security?.allowedOrigins ? originValidationResponse(request, [...config.security.allowedOrigins]) : defaultOriginRejection(request);
|
|
1324
|
+
if (originRejection)
|
|
1325
|
+
return observeRejection(request, originRejection);
|
|
1326
|
+
const auth = await Promise.race([
|
|
1327
|
+
Promise.resolve(config.auth(request)),
|
|
1328
|
+
closedSignal.then(() => null)
|
|
1329
|
+
]);
|
|
1330
|
+
if (closed) {
|
|
1331
|
+
return observeRejection(request, jsonRpcError(-32000, "MCP handler is closed", 503));
|
|
1041
1332
|
}
|
|
1042
|
-
if (
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1333
|
+
if (auth === null)
|
|
1334
|
+
return observeRejection(request, unauthorized());
|
|
1335
|
+
const authCarrier = {
|
|
1336
|
+
token: "",
|
|
1337
|
+
clientId: config.multiRound?.state.principal(auth) ?? "stitchkit",
|
|
1338
|
+
scopes: []
|
|
1339
|
+
};
|
|
1340
|
+
authByCarrier.set(authCarrier, { value: auth });
|
|
1341
|
+
try {
|
|
1342
|
+
return observeRejection(request, await sdkHandler.fetch(request, { authInfo: authCarrier }));
|
|
1343
|
+
} finally {
|
|
1344
|
+
authByCarrier.delete(authCarrier);
|
|
1047
1345
|
}
|
|
1346
|
+
},
|
|
1347
|
+
close: async () => {
|
|
1348
|
+
if (closed)
|
|
1349
|
+
return;
|
|
1350
|
+
closed = true;
|
|
1351
|
+
resolveClosed?.();
|
|
1352
|
+
await sdkHandler.close();
|
|
1048
1353
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
transport.onclose = () => {
|
|
1057
|
-
sessions.delete(newSessionId);
|
|
1058
|
-
};
|
|
1059
|
-
await server.connect(transport);
|
|
1060
|
-
return transport.handleRequest(req);
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function createMcpHttpRoute(config) {
|
|
1357
|
+
return {
|
|
1358
|
+
method: "ALL",
|
|
1359
|
+
path: config.path,
|
|
1360
|
+
handler: (request) => config.handler.fetch(request)
|
|
1061
1361
|
};
|
|
1062
1362
|
}
|
|
1063
1363
|
// src/tools/mcp-stdio.ts
|
|
1064
|
-
import {
|
|
1364
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
1065
1365
|
async function createStdioMcpServer(config) {
|
|
1066
1366
|
const auth = await config.auth;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1367
|
+
return serveStdio(() => buildMcpServer(config, auth), {
|
|
1368
|
+
legacy: config.legacy ?? "serve",
|
|
1369
|
+
maxSubscriptions: 0
|
|
1370
|
+
});
|
|
1070
1371
|
}
|
|
1071
1372
|
// src/tools/mount-download.ts
|
|
1072
1373
|
import { basename, extname, join } from "node:path";
|
|
1374
|
+
import { z as z3 } from "zod";
|
|
1073
1375
|
|
|
1074
1376
|
// src/tools/native-result.ts
|
|
1075
1377
|
function textResult(text, isError = false) {
|
|
@@ -1111,7 +1413,7 @@ function baseNameFor(url) {
|
|
|
1111
1413
|
function mountDownload(server, config) {
|
|
1112
1414
|
const name = config.name ?? "download";
|
|
1113
1415
|
assertToolName(name, "<native>", "download");
|
|
1114
|
-
server.registerTool(name, { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
|
|
1416
|
+
server.registerTool(name, { description: config.description, inputSchema: z3.object(config.inputSchema) }, async (rawArgs) => {
|
|
1115
1417
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1116
1418
|
try {
|
|
1117
1419
|
const url = await config.resolveUrl(args);
|
|
@@ -1140,13 +1442,13 @@ function mountDownload(server, config) {
|
|
|
1140
1442
|
});
|
|
1141
1443
|
}
|
|
1142
1444
|
// src/tools/mount-upload.ts
|
|
1143
|
-
import { z as
|
|
1445
|
+
import { z as z4 } from "zod";
|
|
1144
1446
|
function mountUpload(server, config) {
|
|
1145
1447
|
const name = config.name ?? "upload";
|
|
1146
1448
|
assertToolName(name, "<native>", "upload");
|
|
1147
1449
|
server.registerTool(name, {
|
|
1148
1450
|
description: config.description,
|
|
1149
|
-
inputSchema: { path:
|
|
1451
|
+
inputSchema: { path: z4.string().describe("Path to a local file on this machine") }
|
|
1150
1452
|
}, async (rawArgs) => {
|
|
1151
1453
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1152
1454
|
const path = typeof args.path === "string" ? args.path : "";
|
|
@@ -1161,10 +1463,11 @@ function mountUpload(server, config) {
|
|
|
1161
1463
|
});
|
|
1162
1464
|
}
|
|
1163
1465
|
// src/tools/mount-wait.ts
|
|
1466
|
+
import { z as z5 } from "zod";
|
|
1164
1467
|
function mountWait(server, config) {
|
|
1165
1468
|
const name = config.name ?? "wait";
|
|
1166
1469
|
assertToolName(name, "<native>", "wait");
|
|
1167
|
-
server.registerTool(name, { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
|
|
1470
|
+
server.registerTool(name, { description: config.description, inputSchema: z5.object(config.inputSchema) }, async (rawArgs) => {
|
|
1168
1471
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1169
1472
|
try {
|
|
1170
1473
|
const { state, timedOut } = await pollUntil({
|
|
@@ -1181,7 +1484,35 @@ function mountWait(server, config) {
|
|
|
1181
1484
|
});
|
|
1182
1485
|
}
|
|
1183
1486
|
// src/tools/oauth-provider.ts
|
|
1184
|
-
import { randomUUID
|
|
1487
|
+
import { randomUUID } from "node:crypto";
|
|
1488
|
+
import { z as z6 } from "zod";
|
|
1489
|
+
function isPlainDisplayCharacter(character) {
|
|
1490
|
+
const codePoint = character.codePointAt(0);
|
|
1491
|
+
return codePoint !== undefined && codePoint >= 32 && codePoint !== 127 && character !== "<" && character !== ">";
|
|
1492
|
+
}
|
|
1493
|
+
var CimdClientMetadataSchema = z6.object({
|
|
1494
|
+
client_id: z6.url(),
|
|
1495
|
+
redirect_uris: z6.array(z6.url()).min(1),
|
|
1496
|
+
client_name: z6.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
|
|
1497
|
+
message: "client_name must be plain display text"
|
|
1498
|
+
}),
|
|
1499
|
+
token_endpoint_auth_method: z6.literal("none"),
|
|
1500
|
+
application_type: z6.enum(["native", "web"]).optional()
|
|
1501
|
+
});
|
|
1502
|
+
function createSecureClientMetadataFetcher(policy = {}) {
|
|
1503
|
+
assertPositiveInteger("cimd.maxBytes", policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES);
|
|
1504
|
+
assertPositiveNumber("cimd.timeoutMs", policy.timeoutMs ?? DEFAULT_CIMD_TIMEOUT_MS);
|
|
1505
|
+
assertNonNegativeInteger("cimd.maxRedirects", policy.maxRedirects ?? 3);
|
|
1506
|
+
return {
|
|
1507
|
+
fetch: (url, headers) => fetchPinnedDocument(url, {
|
|
1508
|
+
maxBytes: policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES,
|
|
1509
|
+
timeoutMs: policy.timeoutMs ?? DEFAULT_CIMD_TIMEOUT_MS,
|
|
1510
|
+
maxRedirects: policy.maxRedirects ?? 3,
|
|
1511
|
+
headers,
|
|
1512
|
+
requireHttps: true
|
|
1513
|
+
})
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1185
1516
|
var PUBLIC_CORS2 = {
|
|
1186
1517
|
"Access-Control-Allow-Origin": "*",
|
|
1187
1518
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
@@ -1189,6 +1520,214 @@ var PUBLIC_CORS2 = {
|
|
|
1189
1520
|
};
|
|
1190
1521
|
var AS_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
1191
1522
|
var AUTH_CODE_TTL_MS = 60000;
|
|
1523
|
+
var DEFAULT_CIMD_MAX_BYTES = 64 * 1024;
|
|
1524
|
+
var DEFAULT_CIMD_TIMEOUT_MS = 5000;
|
|
1525
|
+
function assertPositiveInteger(name, value) {
|
|
1526
|
+
if (!Number.isInteger(value) || value < 1)
|
|
1527
|
+
throw new Error(`${name} must be a positive integer`);
|
|
1528
|
+
}
|
|
1529
|
+
function assertNonNegativeInteger(name, value) {
|
|
1530
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1531
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function assertPositiveNumber(name, value) {
|
|
1535
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
1536
|
+
throw new Error(`${name} must be positive`);
|
|
1537
|
+
}
|
|
1538
|
+
function absoluteClientUrl(clientId) {
|
|
1539
|
+
try {
|
|
1540
|
+
return new URL(clientId);
|
|
1541
|
+
} catch {
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
function assertCimdClientId(clientId) {
|
|
1546
|
+
const url = new URL(clientId);
|
|
1547
|
+
if (url.protocol !== "https:")
|
|
1548
|
+
throw new Error("CIMD client_id must use https");
|
|
1549
|
+
if (url.username || url.password)
|
|
1550
|
+
throw new Error("CIMD client_id cannot contain credentials");
|
|
1551
|
+
if (url.hash)
|
|
1552
|
+
throw new Error("CIMD client_id cannot contain a fragment");
|
|
1553
|
+
if (url.search)
|
|
1554
|
+
throw new Error("CIMD client_id cannot contain a query");
|
|
1555
|
+
if (url.toString() !== clientId) {
|
|
1556
|
+
throw new Error("CIMD client_id must be an exact canonical URL");
|
|
1557
|
+
}
|
|
1558
|
+
return url;
|
|
1559
|
+
}
|
|
1560
|
+
function responseFreshness(headers, policy, now) {
|
|
1561
|
+
const maxTtl = policy.maxTtlMs ?? 3600000;
|
|
1562
|
+
const cacheControl = headers.get("cache-control") ?? "";
|
|
1563
|
+
if (/(?:^|,)\s*no-store\s*(?:,|$)/i.test(cacheControl)) {
|
|
1564
|
+
return { freshnessMs: 0, store: false };
|
|
1565
|
+
}
|
|
1566
|
+
if (/(?:^|,)\s*no-cache\s*(?:,|$)/i.test(cacheControl)) {
|
|
1567
|
+
return { freshnessMs: 0, store: true };
|
|
1568
|
+
}
|
|
1569
|
+
const maxAge = cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1];
|
|
1570
|
+
if (maxAge !== undefined) {
|
|
1571
|
+
const age = Number(headers.get("age") ?? 0);
|
|
1572
|
+
return {
|
|
1573
|
+
freshnessMs: Math.max(0, Math.min(maxTtl, (Number(maxAge) - age) * 1000)),
|
|
1574
|
+
store: true
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
const expires = headers.get("expires");
|
|
1578
|
+
if (expires) {
|
|
1579
|
+
const expiresAt = Date.parse(expires);
|
|
1580
|
+
if (Number.isFinite(expiresAt)) {
|
|
1581
|
+
const dateAt = Date.parse(headers.get("date") ?? "");
|
|
1582
|
+
const ageMs = Number(headers.get("age") ?? 0) * 1000;
|
|
1583
|
+
const lifetime = Number.isFinite(dateAt) ? expiresAt - dateAt : expiresAt - now;
|
|
1584
|
+
return {
|
|
1585
|
+
freshnessMs: Math.max(0, Math.min(maxTtl, lifetime - ageMs)),
|
|
1586
|
+
store: true
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return { freshnessMs: Math.min(maxTtl, policy.defaultTtlMs ?? 300000), store: true };
|
|
1591
|
+
}
|
|
1592
|
+
function safeClientName(value) {
|
|
1593
|
+
if (value === undefined)
|
|
1594
|
+
return;
|
|
1595
|
+
const safe = Array.from(value).filter(isPlainDisplayCharacter).join("").trim().slice(0, 200);
|
|
1596
|
+
return safe || undefined;
|
|
1597
|
+
}
|
|
1598
|
+
function parseScopes(value) {
|
|
1599
|
+
if (!value)
|
|
1600
|
+
return [];
|
|
1601
|
+
return [...new Set(value.split(/\s+/u).filter(Boolean))];
|
|
1602
|
+
}
|
|
1603
|
+
function toRegisteredClient(metadata) {
|
|
1604
|
+
return {
|
|
1605
|
+
clientId: metadata.client_id,
|
|
1606
|
+
redirectUris: metadata.redirect_uris,
|
|
1607
|
+
...metadata.client_name !== undefined && { clientName: metadata.client_name },
|
|
1608
|
+
...metadata.application_type !== undefined && {
|
|
1609
|
+
applicationType: metadata.application_type
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function isJsonContentType(value) {
|
|
1614
|
+
const mediaType = value.split(";", 1)[0]?.trim().toLowerCase();
|
|
1615
|
+
return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
|
|
1616
|
+
}
|
|
1617
|
+
function createCimdResolver(options) {
|
|
1618
|
+
const policy = options.cache ?? {};
|
|
1619
|
+
const now = options.now ?? Date.now;
|
|
1620
|
+
const cache = new Map;
|
|
1621
|
+
const inflight = new Map;
|
|
1622
|
+
const maxEntries = policy.maxEntries ?? 256;
|
|
1623
|
+
assertPositiveInteger("cimd.cache.maxEntries", maxEntries);
|
|
1624
|
+
assertNonNegativeInteger("cimd.cache.defaultTtlMs", policy.defaultTtlMs ?? 300000);
|
|
1625
|
+
assertNonNegativeInteger("cimd.cache.maxTtlMs", policy.maxTtlMs ?? 3600000);
|
|
1626
|
+
assertNonNegativeInteger("cimd.cache.negativeTtlMs", policy.negativeTtlMs ?? 1e4);
|
|
1627
|
+
const emit = (event) => {
|
|
1628
|
+
try {
|
|
1629
|
+
options.onCacheEvent?.(event);
|
|
1630
|
+
} catch {}
|
|
1631
|
+
};
|
|
1632
|
+
const touch = (key, value) => {
|
|
1633
|
+
cache.delete(key);
|
|
1634
|
+
cache.set(key, value);
|
|
1635
|
+
while (cache.size > maxEntries) {
|
|
1636
|
+
const oldest = cache.keys().next().value;
|
|
1637
|
+
if (oldest === undefined)
|
|
1638
|
+
break;
|
|
1639
|
+
cache.delete(oldest);
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
const resolveOne = async (clientId) => {
|
|
1643
|
+
const url = assertCimdClientId(clientId);
|
|
1644
|
+
const cached = cache.get(clientId);
|
|
1645
|
+
if (cached && cached.expiresAt > now()) {
|
|
1646
|
+
touch(clientId, cached);
|
|
1647
|
+
if (cached.ok) {
|
|
1648
|
+
emit({ clientId, status: "hit", freshnessMs: cached.expiresAt - now() });
|
|
1649
|
+
return cached.client;
|
|
1650
|
+
}
|
|
1651
|
+
emit({ clientId, status: "negative", freshnessMs: cached.expiresAt - now() });
|
|
1652
|
+
throw new Error(cached.message);
|
|
1653
|
+
}
|
|
1654
|
+
emit({ clientId, status: "miss" });
|
|
1655
|
+
const conditionalHeaders = {};
|
|
1656
|
+
if (cached?.ok && cached.etag)
|
|
1657
|
+
conditionalHeaders["if-none-match"] = cached.etag;
|
|
1658
|
+
if (cached?.ok && cached.lastModified) {
|
|
1659
|
+
conditionalHeaders["if-modified-since"] = cached.lastModified;
|
|
1660
|
+
}
|
|
1661
|
+
let cacheFailure = true;
|
|
1662
|
+
try {
|
|
1663
|
+
const response = await options.fetcher.fetch(url, conditionalHeaders);
|
|
1664
|
+
const freshness = responseFreshness(response.headers, policy, now());
|
|
1665
|
+
cacheFailure = freshness.store;
|
|
1666
|
+
if (response.status === 304 && cached?.ok) {
|
|
1667
|
+
const refreshed = {
|
|
1668
|
+
...cached,
|
|
1669
|
+
expiresAt: now() + freshness.freshnessMs
|
|
1670
|
+
};
|
|
1671
|
+
if (freshness.store)
|
|
1672
|
+
touch(clientId, refreshed);
|
|
1673
|
+
else
|
|
1674
|
+
cache.delete(clientId);
|
|
1675
|
+
emit({ clientId, status: "revalidated", freshnessMs: freshness.freshnessMs });
|
|
1676
|
+
return refreshed.client;
|
|
1677
|
+
}
|
|
1678
|
+
if (response.status !== 200) {
|
|
1679
|
+
throw new Error(`CIMD endpoint returned HTTP ${response.status}`);
|
|
1680
|
+
}
|
|
1681
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1682
|
+
if (!isJsonContentType(contentType)) {
|
|
1683
|
+
throw new Error("CIMD endpoint must return a JSON media type");
|
|
1684
|
+
}
|
|
1685
|
+
const decoded = JSON.parse(new TextDecoder().decode(response.body));
|
|
1686
|
+
const metadata = CimdClientMetadataSchema.parse(decoded);
|
|
1687
|
+
if (metadata.client_id !== clientId) {
|
|
1688
|
+
throw new Error("CIMD client_id does not exactly match the requested URL");
|
|
1689
|
+
}
|
|
1690
|
+
if (!metadata.redirect_uris.every((uri) => isRegistrableRedirectUri(uri, metadata.application_type))) {
|
|
1691
|
+
throw new Error("CIMD redirect_uris contains a URI forbidden for the application type");
|
|
1692
|
+
}
|
|
1693
|
+
const client = toRegisteredClient(metadata);
|
|
1694
|
+
if (freshness.store) {
|
|
1695
|
+
touch(clientId, {
|
|
1696
|
+
ok: true,
|
|
1697
|
+
client,
|
|
1698
|
+
expiresAt: now() + freshness.freshnessMs,
|
|
1699
|
+
...response.headers.get("etag") !== null && {
|
|
1700
|
+
etag: response.headers.get("etag") ?? undefined
|
|
1701
|
+
},
|
|
1702
|
+
...response.headers.get("last-modified") !== null && {
|
|
1703
|
+
lastModified: response.headers.get("last-modified") ?? undefined
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
} else {
|
|
1707
|
+
cache.delete(clientId);
|
|
1708
|
+
}
|
|
1709
|
+
return client;
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
const message = error instanceof Error ? error.message : "CIMD metadata resolution failed";
|
|
1712
|
+
if (cacheFailure) {
|
|
1713
|
+
touch(clientId, {
|
|
1714
|
+
ok: false,
|
|
1715
|
+
message,
|
|
1716
|
+
expiresAt: now() + (policy.negativeTtlMs ?? 1e4)
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
throw error;
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
return async (clientId) => {
|
|
1723
|
+
const pending = inflight.get(clientId);
|
|
1724
|
+
if (pending)
|
|
1725
|
+
return pending;
|
|
1726
|
+
const created = resolveOne(clientId).finally(() => inflight.delete(clientId));
|
|
1727
|
+
inflight.set(clientId, created);
|
|
1728
|
+
return created;
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1192
1731
|
function json(body, status = 200, extraHeaders) {
|
|
1193
1732
|
return new Response(JSON.stringify(body), {
|
|
1194
1733
|
status,
|
|
@@ -1201,6 +1740,8 @@ function oauthError(error, description, status = 400) {
|
|
|
1201
1740
|
function isRegistrableRedirectUri(value, applicationType) {
|
|
1202
1741
|
try {
|
|
1203
1742
|
const url = new URL(value);
|
|
1743
|
+
if (url.username || url.password || url.hash)
|
|
1744
|
+
return false;
|
|
1204
1745
|
if (url.protocol === "https:")
|
|
1205
1746
|
return true;
|
|
1206
1747
|
if (url.protocol !== "http:")
|
|
@@ -1213,6 +1754,17 @@ function isRegistrableRedirectUri(value, applicationType) {
|
|
|
1213
1754
|
return false;
|
|
1214
1755
|
}
|
|
1215
1756
|
}
|
|
1757
|
+
function isLoopbackRedirectUri(value) {
|
|
1758
|
+
try {
|
|
1759
|
+
const url = new URL(value);
|
|
1760
|
+
if (url.protocol !== "http:")
|
|
1761
|
+
return false;
|
|
1762
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
1763
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1764
|
+
} catch {
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1216
1768
|
function redirectWith(uri, params) {
|
|
1217
1769
|
const url = new URL(uri);
|
|
1218
1770
|
for (const [key, value] of Object.entries(params))
|
|
@@ -1240,6 +1792,33 @@ function mountOAuthProvider(config) {
|
|
|
1240
1792
|
const registerPath = `${base}/register`;
|
|
1241
1793
|
const authorizePath = `${base}/authorize`;
|
|
1242
1794
|
const tokenPath = `${base}/token`;
|
|
1795
|
+
const clientRegistration = config.clientRegistration ?? {};
|
|
1796
|
+
const dcrRegistry = clientRegistration.dcr || undefined;
|
|
1797
|
+
const dcrEnabled = dcrRegistry !== undefined;
|
|
1798
|
+
const cimdConfig = clientRegistration.cimd === false ? undefined : clientRegistration.cimd ?? {};
|
|
1799
|
+
const cimdEnabled = cimdConfig !== undefined;
|
|
1800
|
+
const resolveCimd = cimdEnabled ? createCimdResolver({
|
|
1801
|
+
cache: cimdConfig.cache,
|
|
1802
|
+
fetcher: cimdConfig.fetcher ?? createSecureClientMetadataFetcher(),
|
|
1803
|
+
onCacheEvent: cimdConfig.onCacheEvent
|
|
1804
|
+
}) : undefined;
|
|
1805
|
+
const resolveClient = async (clientId) => {
|
|
1806
|
+
const preRegistered = await clientRegistration.preRegistered?.get(clientId);
|
|
1807
|
+
if (preRegistered)
|
|
1808
|
+
return preRegistered.clientId === clientId ? preRegistered : null;
|
|
1809
|
+
const clientUrl = absoluteClientUrl(clientId);
|
|
1810
|
+
if (clientUrl) {
|
|
1811
|
+
if (!resolveCimd || clientUrl.protocol !== "https:")
|
|
1812
|
+
return null;
|
|
1813
|
+
try {
|
|
1814
|
+
return await resolveCimd(clientId);
|
|
1815
|
+
} catch {
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const dcrClient = await dcrRegistry?.get(clientId);
|
|
1820
|
+
return dcrClient?.clientId === clientId ? dcrClient : null;
|
|
1821
|
+
};
|
|
1243
1822
|
const redirectToClient = (uri, params) => redirectWith(uri, { ...params, iss: config.issuer });
|
|
1244
1823
|
const metadataRoute = {
|
|
1245
1824
|
method: "ALL",
|
|
@@ -1251,11 +1830,12 @@ function mountOAuthProvider(config) {
|
|
|
1251
1830
|
issuer: config.issuer,
|
|
1252
1831
|
authorization_endpoint: `${config.issuer}${authorizePath}`,
|
|
1253
1832
|
token_endpoint: `${config.issuer}${tokenPath}`,
|
|
1254
|
-
registration_endpoint: `${config.issuer}${registerPath}
|
|
1833
|
+
...dcrEnabled && { registration_endpoint: `${config.issuer}${registerPath}` },
|
|
1255
1834
|
response_types_supported: ["code"],
|
|
1256
1835
|
grant_types_supported: config.refreshTokens ? ["authorization_code", "refresh_token"] : ["authorization_code"],
|
|
1257
1836
|
code_challenge_methods_supported: ["S256"],
|
|
1258
1837
|
token_endpoint_auth_methods_supported: ["none"],
|
|
1838
|
+
...cimdEnabled && { client_id_metadata_document_supported: true },
|
|
1259
1839
|
authorization_response_iss_parameter_supported: true,
|
|
1260
1840
|
...config.scopesSupported && { scopes_supported: config.scopesSupported }
|
|
1261
1841
|
});
|
|
@@ -1279,13 +1859,19 @@ function mountOAuthProvider(config) {
|
|
|
1279
1859
|
}
|
|
1280
1860
|
const applicationType = rawAppType;
|
|
1281
1861
|
const redirectUris = meta.redirect_uris;
|
|
1862
|
+
const tokenEndpointAuthMethod = meta.token_endpoint_auth_method ?? "none";
|
|
1863
|
+
if (tokenEndpointAuthMethod !== "none") {
|
|
1864
|
+
return oauthError("invalid_client_metadata", 'Only public clients with token_endpoint_auth_method "none" are supported');
|
|
1865
|
+
}
|
|
1282
1866
|
if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every((u) => typeof u === "string" && isRegistrableRedirectUri(u, applicationType))) {
|
|
1283
1867
|
return oauthError("invalid_redirect_uri", applicationType === "web" ? "redirect_uris must be a non-empty array of absolute https URLs (a web client cannot register an http loopback URI)" : "redirect_uris must be a non-empty array of absolute https URLs (http is allowed only on a loopback host)");
|
|
1284
1868
|
}
|
|
1285
|
-
|
|
1869
|
+
if (!dcrRegistry)
|
|
1870
|
+
throw new Error("DCR route mounted without a DCR registry");
|
|
1871
|
+
const client = await dcrRegistry.register({
|
|
1286
1872
|
redirectUris,
|
|
1287
1873
|
clientName: typeof meta.client_name === "string" ? meta.client_name : undefined,
|
|
1288
|
-
tokenEndpointAuthMethod
|
|
1874
|
+
tokenEndpointAuthMethod,
|
|
1289
1875
|
...applicationType && { applicationType }
|
|
1290
1876
|
});
|
|
1291
1877
|
return json({
|
|
@@ -1318,7 +1904,7 @@ function mountOAuthProvider(config) {
|
|
|
1318
1904
|
if (!clientId || !redirectUri) {
|
|
1319
1905
|
return oauthError("invalid_request", "client_id and redirect_uri are required");
|
|
1320
1906
|
}
|
|
1321
|
-
const client = await
|
|
1907
|
+
const client = await resolveClient(clientId);
|
|
1322
1908
|
if (!client)
|
|
1323
1909
|
return oauthError("invalid_client", "Unknown client_id", 401);
|
|
1324
1910
|
if (!client.redirectUris.includes(redirectUri)) {
|
|
@@ -1351,18 +1937,49 @@ function mountOAuthProvider(config) {
|
|
|
1351
1937
|
...state && { state }
|
|
1352
1938
|
});
|
|
1353
1939
|
}
|
|
1354
|
-
const
|
|
1940
|
+
const requestedScopes = parseScopes(scope);
|
|
1941
|
+
const supportedScopes = new Set(config.scopesSupported ?? []);
|
|
1942
|
+
if (config.scopesSupported && requestedScopes.some((requestedScope2) => !supportedScopes.has(requestedScope2))) {
|
|
1943
|
+
return redirectToClient(redirectUri, {
|
|
1944
|
+
error: "invalid_scope",
|
|
1945
|
+
error_description: "One or more requested scopes are not supported",
|
|
1946
|
+
...state && { state }
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
const requestedScope = requestedScopes.length > 0 ? requestedScopes.join(" ") : undefined;
|
|
1950
|
+
const authRequest = {
|
|
1951
|
+
clientId,
|
|
1952
|
+
redirectUri,
|
|
1953
|
+
scope: requestedScope,
|
|
1954
|
+
resource,
|
|
1955
|
+
state,
|
|
1956
|
+
isLoopbackRedirect: isLoopbackRedirectUri(redirectUri),
|
|
1957
|
+
...safeClientName(client.clientName) !== undefined && {
|
|
1958
|
+
clientName: safeClientName(client.clientName)
|
|
1959
|
+
},
|
|
1960
|
+
...client.applicationType !== undefined && {
|
|
1961
|
+
applicationType: client.applicationType
|
|
1962
|
+
},
|
|
1963
|
+
...absoluteClientUrl(clientId)?.protocol === "https:" && {
|
|
1964
|
+
clientOrigin: new URL(clientId).origin
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1355
1967
|
const result = await config.authorizeUser(req, authRequest);
|
|
1356
1968
|
if (result instanceof Response)
|
|
1357
1969
|
return result;
|
|
1358
|
-
const
|
|
1970
|
+
const approvedScopes = [...new Set(result.approvedScopes)];
|
|
1971
|
+
if (approvedScopes.some((approvedScope2) => !requestedScopes.includes(approvedScope2) || config.scopesSupported !== undefined && !supportedScopes.has(approvedScope2))) {
|
|
1972
|
+
throw new Error("authorizeUser approvedScopes must be a subset of requested scopes");
|
|
1973
|
+
}
|
|
1974
|
+
const approvedScope = approvedScopes.length > 0 ? approvedScopes.join(" ") : undefined;
|
|
1975
|
+
const code = randomUUID();
|
|
1359
1976
|
await config.codes.save(code, {
|
|
1360
1977
|
clientId,
|
|
1361
1978
|
redirectUri,
|
|
1362
1979
|
codeChallenge,
|
|
1363
1980
|
codeChallengeMethod: "S256",
|
|
1364
1981
|
resource,
|
|
1365
|
-
scope,
|
|
1982
|
+
scope: approvedScope,
|
|
1366
1983
|
userId: result.userId,
|
|
1367
1984
|
expiresAt: Date.now() + AUTH_CODE_TTL_MS
|
|
1368
1985
|
});
|
|
@@ -1408,7 +2025,7 @@ function mountOAuthProvider(config) {
|
|
|
1408
2025
|
const accessToken = await issueAccessToken(data.userId, data.resource, data.clientId, data.scope);
|
|
1409
2026
|
let refreshToken;
|
|
1410
2027
|
if (config.refreshTokens) {
|
|
1411
|
-
refreshToken =
|
|
2028
|
+
refreshToken = randomUUID();
|
|
1412
2029
|
await config.refreshTokens.save(refreshToken, {
|
|
1413
2030
|
clientId: data.clientId,
|
|
1414
2031
|
resource: data.resource,
|
|
@@ -1438,7 +2055,7 @@ function mountOAuthProvider(config) {
|
|
|
1438
2055
|
if (data.clientId !== clientId)
|
|
1439
2056
|
return oauthError("invalid_grant", "client_id mismatch");
|
|
1440
2057
|
const accessToken = await issueAccessToken(data.userId, data.resource, data.clientId, data.scope);
|
|
1441
|
-
const newRefresh =
|
|
2058
|
+
const newRefresh = randomUUID();
|
|
1442
2059
|
await config.refreshTokens.save(newRefresh, data);
|
|
1443
2060
|
return json({
|
|
1444
2061
|
access_token: accessToken,
|
|
@@ -1451,7 +2068,7 @@ function mountOAuthProvider(config) {
|
|
|
1451
2068
|
return oauthError("unsupported_grant_type", `Unsupported grant_type: ${grantType ?? "none"}`);
|
|
1452
2069
|
}
|
|
1453
2070
|
};
|
|
1454
|
-
return [metadataRoute, registerRoute, authorizeRoute, tokenRoute];
|
|
2071
|
+
return dcrEnabled ? [metadataRoute, registerRoute, authorizeRoute, tokenRoute] : [metadataRoute, authorizeRoute, tokenRoute];
|
|
1455
2072
|
}
|
|
1456
2073
|
// src/browser/client-multipart.ts
|
|
1457
2074
|
function isFileDescriptor(value) {
|
|
@@ -1936,7 +2553,7 @@ function summarizeTransports(surface) {
|
|
|
1936
2553
|
// src/tools/view-file.ts
|
|
1937
2554
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
1938
2555
|
import { extname as extname2, resolve } from "node:path";
|
|
1939
|
-
import { z as
|
|
2556
|
+
import { z as z7 } from "zod";
|
|
1940
2557
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
1941
2558
|
var EXT_MIME = {
|
|
1942
2559
|
".png": "image/png",
|
|
@@ -2036,7 +2653,7 @@ function mountViewFile(server, options = {}) {
|
|
|
2036
2653
|
server.registerTool("view_file", {
|
|
2037
2654
|
description: "View media (image, audio, video) by URL or local path — returns it as content you can SEE / HEAR. Pass several paths to view multiple files at once. Use it on a generation `output` url to inspect the result.",
|
|
2038
2655
|
inputSchema: {
|
|
2039
|
-
paths:
|
|
2656
|
+
paths: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
|
|
2040
2657
|
},
|
|
2041
2658
|
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
2042
2659
|
}, async (args) => {
|
|
@@ -2081,7 +2698,9 @@ export {
|
|
|
2081
2698
|
createToolLogger,
|
|
2082
2699
|
createToolInvoker,
|
|
2083
2700
|
createStdioMcpServer,
|
|
2701
|
+
createSecureClientMetadataFetcher,
|
|
2084
2702
|
createRuntimeToolFactory,
|
|
2703
|
+
createMcpHttpRoute,
|
|
2085
2704
|
createMcpHandler,
|
|
2086
2705
|
createCli,
|
|
2087
2706
|
collectTools,
|