stitchkit 0.45.0 → 0.46.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 +1 -1
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +3 -1
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +3 -4
- package/dist/contract/errors.d.ts +1 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +2 -2
- package/dist/{index-yh0p5ddn.js → index-5s8b7z6q.js} +31 -11
- package/dist/{index-mvjyq9h9.js → index-62pqb23z.js} +309 -144
- package/dist/index-c40tkxcd.js +181 -0
- package/dist/{index-r6czv7yt.js → index-pwyedf7b.js} +4 -3
- package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
- package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
- package/dist/{index-36f1xkdb.js → index-w1s873ng.js} +2 -2
- package/dist/index-yxpe3phd.js +63 -0
- package/dist/{index-w0mec79h.js → index-zwqty9zf.js} +261 -281
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +129 -51
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/secure-fetch.d.ts +23 -3
- package/dist/internal/secure-fetch.d.ts.map +1 -1
- package/dist/internal/typed.d.ts +7 -0
- package/dist/internal/typed.d.ts.map +1 -1
- package/dist/internal/within-dir.d.ts +6 -0
- package/dist/internal/within-dir.d.ts.map +1 -1
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/node.js +3 -4
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +7 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +54 -125
- package/dist/observability/sanitize.d.ts +8 -1
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/observability/trace.d.ts +3 -1
- package/dist/observability/trace.d.ts.map +1 -1
- package/dist/react/cache-bridge.d.ts +4 -0
- package/dist/react/cache-bridge.d.ts.map +1 -1
- package/dist/react.js +46 -5
- package/dist/realtime/contract.d.ts +4 -1
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/rejection.d.ts +10 -0
- package/dist/realtime/rejection.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +14 -1
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/server/cache.d.ts +5 -1
- package/dist/server/cache.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +1 -0
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/file.d.ts +6 -0
- package/dist/server/file.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +108 -11
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/realtime.d.ts +2 -0
- package/dist/server/realtime.d.ts.map +1 -1
- package/dist/server/router.d.ts +0 -8
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +2 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/cli-args.d.ts +11 -3
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +5 -0
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +4 -3
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp-prepare.d.ts +95 -0
- package/dist/tools/mcp-prepare.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +4 -0
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.d.ts +2 -1
- package/dist/tools/mcp-stdio.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +2 -78
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +5 -0
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +1 -1
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/oauth-provider.d.ts +20 -1
- package/dist/tools/oauth-provider.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts +1 -1
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +9 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +2 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +342 -214
- package/llms-full.txt +107 -19
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/dist/index-frfyw9fa.js +0 -33
- package/dist/index-x3fcszf8.js +0 -8
package/dist/tools.js
CHANGED
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
inputIsQuery,
|
|
3
3
|
signJwt,
|
|
4
4
|
verifyPkce
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-w1s873ng.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CORS_ALLOW_HEADERS
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-r1qp4rve.js";
|
|
9
9
|
import {
|
|
10
10
|
assertToolName,
|
|
11
11
|
assertUniqueToolName,
|
|
@@ -26,13 +26,15 @@ import {
|
|
|
26
26
|
toolErrorFromResult,
|
|
27
27
|
toolResultFromError,
|
|
28
28
|
writeDownload
|
|
29
|
-
} from "./index-
|
|
29
|
+
} from "./index-62pqb23z.js";
|
|
30
30
|
import {
|
|
31
|
+
isWithinDir,
|
|
32
|
+
realPathWithinDir,
|
|
31
33
|
toJsonSchema
|
|
32
|
-
} from "./index-
|
|
34
|
+
} from "./index-yxpe3phd.js";
|
|
33
35
|
import {
|
|
34
|
-
|
|
35
|
-
} from "./index-
|
|
36
|
+
redact
|
|
37
|
+
} from "./index-c40tkxcd.js";
|
|
36
38
|
import {
|
|
37
39
|
AppError,
|
|
38
40
|
getRequestContext,
|
|
@@ -43,7 +45,7 @@ import {
|
|
|
43
45
|
resolvePropagationContext,
|
|
44
46
|
runWithRequestContext,
|
|
45
47
|
typedEntries
|
|
46
|
-
} from "./index-
|
|
48
|
+
} from "./index-5s8b7z6q.js";
|
|
47
49
|
|
|
48
50
|
// src/tools/agent.ts
|
|
49
51
|
import { jsonSchema, tool } from "ai";
|
|
@@ -452,7 +454,11 @@ async function runRoundFailure(tool2, rawArgs, runTool, context, code, message)
|
|
|
452
454
|
}
|
|
453
455
|
}, rawArgs, context);
|
|
454
456
|
}
|
|
455
|
-
function
|
|
457
|
+
function validateMcpRoundPolicy(tool2, policy, runtime) {
|
|
458
|
+
if (!runtime?.stateConfigured) {
|
|
459
|
+
throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares inputRequired but no multiRound.state key is configured`);
|
|
460
|
+
}
|
|
461
|
+
const { maxRounds } = runtime;
|
|
456
462
|
if (!Number.isInteger(maxRounds) || maxRounds < 1) {
|
|
457
463
|
throw new Error("[stitchkit] multiRound.serving.maxRounds must be a positive integer");
|
|
458
464
|
}
|
|
@@ -485,7 +491,10 @@ async function resolveMcpRound(options) {
|
|
|
485
491
|
if (!options.runtime) {
|
|
486
492
|
throw new Error(`[stitchkit] MCP tool "${options.tool.name}" declares inputRequired but no multiRound.state key is configured`);
|
|
487
493
|
}
|
|
488
|
-
|
|
494
|
+
validateMcpRoundPolicy(options.tool, policy, {
|
|
495
|
+
stateConfigured: true,
|
|
496
|
+
maxRounds: options.runtime.maxRounds
|
|
497
|
+
});
|
|
489
498
|
const requests = policy.inputRequired;
|
|
490
499
|
const state = options.context.mcpReq.requestState();
|
|
491
500
|
const digest = await argumentsDigest(options.rawArgs);
|
|
@@ -599,104 +608,6 @@ async function resolveMcpRound(options) {
|
|
|
599
608
|
};
|
|
600
609
|
}
|
|
601
610
|
|
|
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
|
-
|
|
634
|
-
// src/tools/native-mcp.ts
|
|
635
|
-
import {
|
|
636
|
-
isCallToolResult
|
|
637
|
-
} from "@modelcontextprotocol/server";
|
|
638
|
-
import { z } from "zod";
|
|
639
|
-
function mountPreparedRuntimeMcp(server, tools, config) {
|
|
640
|
-
for (const { definition, descriptor } of tools) {
|
|
641
|
-
const { mountable } = descriptor;
|
|
642
|
-
const toolConfig = {
|
|
643
|
-
description: definition.description,
|
|
644
|
-
inputSchema: z.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
645
|
-
};
|
|
646
|
-
if (descriptor.outputSchema)
|
|
647
|
-
toolConfig.outputSchema = descriptor.outputSchema;
|
|
648
|
-
if (definition.annotations)
|
|
649
|
-
toolConfig.annotations = definition.annotations;
|
|
650
|
-
if (definition.ui) {
|
|
651
|
-
toolConfig._meta = {
|
|
652
|
-
ui: definition.ui,
|
|
653
|
-
"ui/resourceUri": definition.ui.resourceUri
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
server.registerTool(definition.name, toolConfig, async (rawArgs, mcpContext) => runInMcpRequestContext(mcpContext, definition.name, async () => {
|
|
657
|
-
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
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);
|
|
672
|
-
return config.formatResult(result, descriptor.outputMode, definition.name);
|
|
673
|
-
}
|
|
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
|
-
}
|
|
687
|
-
return response;
|
|
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
|
-
}));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
611
|
// src/tools/portable-formats.ts
|
|
701
612
|
var PORTABLE_JSON_SCHEMA_FORMATS = new Set([
|
|
702
613
|
"date",
|
|
@@ -854,7 +765,7 @@ function findUntypedProperties(schema, prefix = "") {
|
|
|
854
765
|
return found;
|
|
855
766
|
}
|
|
856
767
|
|
|
857
|
-
// src/tools/mcp.ts
|
|
768
|
+
// src/tools/mcp-prepare.ts
|
|
858
769
|
function textBlock(text) {
|
|
859
770
|
return [{ type: "text", text }];
|
|
860
771
|
}
|
|
@@ -862,9 +773,15 @@ function formatMcpResult(result, mode, toolName, errorHint) {
|
|
|
862
773
|
if (result.ok) {
|
|
863
774
|
if (mode === "none")
|
|
864
775
|
return { content: [] };
|
|
865
|
-
|
|
776
|
+
let serialized;
|
|
777
|
+
try {
|
|
778
|
+
serialized = JSON.stringify(result.data, null, 2);
|
|
779
|
+
} catch {
|
|
780
|
+
serialized = undefined;
|
|
781
|
+
}
|
|
866
782
|
if (serialized === undefined) {
|
|
867
|
-
|
|
783
|
+
const safe = redact(result.data, { sensitiveKeys: /(?!)/ });
|
|
784
|
+
return { content: textBlock(JSON.stringify(safe, null, 2)), structuredContent: safe };
|
|
868
785
|
}
|
|
869
786
|
return { content: textBlock(serialized), structuredContent: result.data };
|
|
870
787
|
}
|
|
@@ -907,6 +824,9 @@ function prepareMcpTool(mountable, config, validation, logger, failures, seen) {
|
|
|
907
824
|
const policy = validation.policy ?? "throw";
|
|
908
825
|
assertUniqueToolName(mountable.name, seen.has(mountable.name), "MCP tool name");
|
|
909
826
|
seen.add(mountable.name);
|
|
827
|
+
if (mountable.method.mcp) {
|
|
828
|
+
validateMcpRoundPolicy(mountable, mountable.method.mcp, config.multiRound);
|
|
829
|
+
}
|
|
910
830
|
let inputJsonSchema;
|
|
911
831
|
try {
|
|
912
832
|
inputJsonSchema = buildToolPresentationSchema({
|
|
@@ -998,7 +918,8 @@ function prepareMcpServerSurface(surface, config = {}) {
|
|
|
998
918
|
const contractTools = prepareMcpTools(contractMountables, config);
|
|
999
919
|
const runtimeDescriptors = prepareMcpTools(runtimeMountables, {
|
|
1000
920
|
schemaValidation: config.schemaValidation,
|
|
1001
|
-
logger: config.logger
|
|
921
|
+
logger: config.logger,
|
|
922
|
+
multiRound: config.multiRound
|
|
1002
923
|
});
|
|
1003
924
|
const descriptorsByName = new Map(runtimeDescriptors.map((descriptor) => [descriptor.mountable.name, descriptor]));
|
|
1004
925
|
const runtimeTools = [];
|
|
@@ -1009,6 +930,106 @@ function prepareMcpServerSurface(surface, config = {}) {
|
|
|
1009
930
|
}
|
|
1010
931
|
return Object.freeze({ contractTools, runtimeTools: Object.freeze(runtimeTools) });
|
|
1011
932
|
}
|
|
933
|
+
|
|
934
|
+
// src/tools/mcp-trace.ts
|
|
935
|
+
import {
|
|
936
|
+
BAGGAGE_META_KEY,
|
|
937
|
+
TRACEPARENT_META_KEY,
|
|
938
|
+
TRACESTATE_META_KEY
|
|
939
|
+
} from "@modelcontextprotocol/server";
|
|
940
|
+
function runInMcpRequestContext(context, toolName, body) {
|
|
941
|
+
const ambient = getRequestContext();
|
|
942
|
+
const metadata = context.mcpReq._meta;
|
|
943
|
+
const propagation = metadata ? {
|
|
944
|
+
traceparent: metadata[TRACEPARENT_META_KEY],
|
|
945
|
+
tracestate: metadata[TRACESTATE_META_KEY],
|
|
946
|
+
baggage: metadata[BAGGAGE_META_KEY]
|
|
947
|
+
} : undefined;
|
|
948
|
+
const request = {
|
|
949
|
+
...ambient ?? {
|
|
950
|
+
source: "mcp",
|
|
951
|
+
method: "MCP",
|
|
952
|
+
path: `/mcp/${toolName}`,
|
|
953
|
+
startedAt: process.hrtime.bigint()
|
|
954
|
+
},
|
|
955
|
+
trace: resolvePropagationContext(propagation, ambient?.trace),
|
|
956
|
+
source: "mcp",
|
|
957
|
+
method: "MCP",
|
|
958
|
+
path: `/mcp/${toolName}`,
|
|
959
|
+
startedAt: process.hrtime.bigint(),
|
|
960
|
+
dimensions: ambient?.dimensions ? { ...ambient.dimensions } : undefined,
|
|
961
|
+
error: undefined
|
|
962
|
+
};
|
|
963
|
+
return runWithRequestContext(request, body);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/tools/native-mcp.ts
|
|
967
|
+
import {
|
|
968
|
+
isCallToolResult
|
|
969
|
+
} from "@modelcontextprotocol/server";
|
|
970
|
+
import { z } from "zod";
|
|
971
|
+
function mountPreparedRuntimeMcp(server, tools, config) {
|
|
972
|
+
for (const { definition, descriptor } of tools) {
|
|
973
|
+
const { mountable } = descriptor;
|
|
974
|
+
const toolConfig = {
|
|
975
|
+
description: definition.description,
|
|
976
|
+
inputSchema: z.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
977
|
+
};
|
|
978
|
+
if (descriptor.outputSchema)
|
|
979
|
+
toolConfig.outputSchema = descriptor.outputSchema;
|
|
980
|
+
if (definition.annotations)
|
|
981
|
+
toolConfig.annotations = definition.annotations;
|
|
982
|
+
if (definition.ui) {
|
|
983
|
+
toolConfig._meta = {
|
|
984
|
+
ui: definition.ui,
|
|
985
|
+
"ui/resourceUri": definition.ui.resourceUri
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
server.registerTool(definition.name, toolConfig, async (rawArgs, mcpContext) => runInMcpRequestContext(mcpContext, definition.name, async () => {
|
|
989
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
990
|
+
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);
|
|
991
|
+
const round = await resolveMcpRound({
|
|
992
|
+
tool: mountable,
|
|
993
|
+
rawArgs: args,
|
|
994
|
+
context: mcpContext,
|
|
995
|
+
policy: mountable.method.mcp,
|
|
996
|
+
runtime: config.multiRoundRuntime,
|
|
997
|
+
runTool,
|
|
998
|
+
formatFailure: (result) => config.formatResult(result, "none", definition.name)
|
|
999
|
+
});
|
|
1000
|
+
if (round.kind === "response")
|
|
1001
|
+
return round.response;
|
|
1002
|
+
if (!definition.present?.mcp) {
|
|
1003
|
+
const result = await runTool(mountable, args, round.context);
|
|
1004
|
+
return config.formatResult(result, descriptor.outputMode, definition.name);
|
|
1005
|
+
}
|
|
1006
|
+
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) => {
|
|
1007
|
+
const presented = await definition.present?.mcp?.(data);
|
|
1008
|
+
if (!presented) {
|
|
1009
|
+
throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter did not return a result", 500);
|
|
1010
|
+
}
|
|
1011
|
+
if ("structuredContent" in presented || "isError" in presented) {
|
|
1012
|
+
throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter cannot set framework-owned structuredContent or isError", 500);
|
|
1013
|
+
}
|
|
1014
|
+
const structured = config.formatResult({ ok: true, data }, descriptor.outputMode, definition.name).structuredContent;
|
|
1015
|
+
const response = structured === undefined ? presented : { ...presented, structuredContent: structured };
|
|
1016
|
+
if (!isCallToolResult(response)) {
|
|
1017
|
+
throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter returned an invalid CallToolResult", 500);
|
|
1018
|
+
}
|
|
1019
|
+
return response;
|
|
1020
|
+
});
|
|
1021
|
+
if (!presentedResult.ok) {
|
|
1022
|
+
return config.formatResult(presentedResult, "none", definition.name);
|
|
1023
|
+
}
|
|
1024
|
+
if (!isCallToolResult(presentedResult.data)) {
|
|
1025
|
+
throw new Error("[stitchkit] Runtime MCP presenter invariant failed");
|
|
1026
|
+
}
|
|
1027
|
+
return presentedResult.data;
|
|
1028
|
+
}));
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/tools/mcp.ts
|
|
1012
1033
|
function validateMcpSchemas(config) {
|
|
1013
1034
|
prepareMcpSurface(config.services, {
|
|
1014
1035
|
extend: config.extend,
|
|
@@ -1096,7 +1117,11 @@ function buildMcpServer(config, auth) {
|
|
|
1096
1117
|
extend: config.extend,
|
|
1097
1118
|
flattenUnionInput: config.flattenUnionInput,
|
|
1098
1119
|
schemaValidation: config.schemaValidation,
|
|
1099
|
-
logger: config.logger
|
|
1120
|
+
logger: config.logger,
|
|
1121
|
+
multiRound: {
|
|
1122
|
+
stateConfigured: config.multiRound !== undefined,
|
|
1123
|
+
maxRounds: config.multiRound?.serving?.maxRounds ?? 10
|
|
1124
|
+
}
|
|
1100
1125
|
});
|
|
1101
1126
|
return buildMcpServerFromPrepared(config, auth, prepared);
|
|
1102
1127
|
}
|
|
@@ -1182,6 +1207,8 @@ function mountMcpResource(server, resource) {
|
|
|
1182
1207
|
import {
|
|
1183
1208
|
createMcpHandler as createSdkMcpHandler,
|
|
1184
1209
|
hostHeaderValidationResponse,
|
|
1210
|
+
localhostAllowedHostnames,
|
|
1211
|
+
localhostAllowedOrigins,
|
|
1185
1212
|
originValidationResponse
|
|
1186
1213
|
} from "@modelcontextprotocol/server";
|
|
1187
1214
|
|
|
@@ -1229,28 +1256,25 @@ function oauthProtectedResourceRoute(config) {
|
|
|
1229
1256
|
function jsonRpcError(code, message, status, headers) {
|
|
1230
1257
|
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
|
|
1231
1258
|
}
|
|
1232
|
-
function
|
|
1233
|
-
|
|
1234
|
-
|
|
1259
|
+
function hostValidationResponse(request, allowedHosts) {
|
|
1260
|
+
if (request.headers.has("host")) {
|
|
1261
|
+
return hostHeaderValidationResponse(request, [...allowedHosts]);
|
|
1262
|
+
}
|
|
1263
|
+
const hostname = new URL(request.url).hostname;
|
|
1264
|
+
if (allowedHosts.includes(hostname))
|
|
1235
1265
|
return;
|
|
1236
1266
|
return jsonRpcError(-32000, "Invalid Host header", 403);
|
|
1237
1267
|
}
|
|
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
1268
|
function prepareStaticSurface(config) {
|
|
1249
1269
|
const preparation = {
|
|
1250
1270
|
logger: config.logger,
|
|
1251
1271
|
extend: config.extend,
|
|
1252
1272
|
flattenUnionInput: config.flattenUnionInput,
|
|
1253
|
-
schemaValidation: config.schemaValidation
|
|
1273
|
+
schemaValidation: config.schemaValidation,
|
|
1274
|
+
multiRound: {
|
|
1275
|
+
stateConfigured: config.multiRound !== undefined,
|
|
1276
|
+
maxRounds: config.multiRound?.serving?.maxRounds ?? 10
|
|
1277
|
+
}
|
|
1254
1278
|
};
|
|
1255
1279
|
if (config.surfaces && config.selectSurface) {
|
|
1256
1280
|
const preparedByKey = new Map;
|
|
@@ -1317,10 +1341,12 @@ function createMcpHandler(config) {
|
|
|
1317
1341
|
if (closed) {
|
|
1318
1342
|
return observeRejection(request, jsonRpcError(-32000, "MCP handler is closed", 503));
|
|
1319
1343
|
}
|
|
1320
|
-
const
|
|
1344
|
+
const allowedHosts = config.security?.allowedHosts ?? localhostAllowedHostnames();
|
|
1345
|
+
const hostRejection = hostValidationResponse(request, allowedHosts);
|
|
1321
1346
|
if (hostRejection)
|
|
1322
1347
|
return observeRejection(request, hostRejection);
|
|
1323
|
-
const
|
|
1348
|
+
const allowedOrigins = config.security?.allowedOrigins ?? localhostAllowedOrigins();
|
|
1349
|
+
const originRejection = originValidationResponse(request, [...allowedOrigins]);
|
|
1324
1350
|
if (originRejection)
|
|
1325
1351
|
return observeRejection(request, originRejection);
|
|
1326
1352
|
const auth = await Promise.race([
|
|
@@ -1330,7 +1356,7 @@ function createMcpHandler(config) {
|
|
|
1330
1356
|
if (closed) {
|
|
1331
1357
|
return observeRejection(request, jsonRpcError(-32000, "MCP handler is closed", 503));
|
|
1332
1358
|
}
|
|
1333
|
-
if (auth
|
|
1359
|
+
if (auth == null)
|
|
1334
1360
|
return observeRejection(request, unauthorized());
|
|
1335
1361
|
const authCarrier = {
|
|
1336
1362
|
token: "",
|
|
@@ -1370,7 +1396,7 @@ async function createStdioMcpServer(config) {
|
|
|
1370
1396
|
});
|
|
1371
1397
|
}
|
|
1372
1398
|
// src/tools/mount-download.ts
|
|
1373
|
-
import { basename, extname,
|
|
1399
|
+
import { basename, extname, resolve } from "node:path";
|
|
1374
1400
|
import { z as z3 } from "zod";
|
|
1375
1401
|
|
|
1376
1402
|
// src/tools/native-result.ts
|
|
@@ -1419,7 +1445,9 @@ function mountDownload(server, config) {
|
|
|
1419
1445
|
const url = await config.resolveUrl(args);
|
|
1420
1446
|
if (!url)
|
|
1421
1447
|
return textResult("Nothing to download.", true);
|
|
1422
|
-
const res = await fetchGuarded(new URL(url), config.allowPrivateHosts ?? false
|
|
1448
|
+
const res = await fetchGuarded(new URL(url), config.allowPrivateHosts ?? false, {
|
|
1449
|
+
timeoutMs: config.timeoutMs
|
|
1450
|
+
});
|
|
1423
1451
|
if (!res.ok)
|
|
1424
1452
|
return textResult(`Download failed: HTTP ${res.status}`, true);
|
|
1425
1453
|
const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
|
|
@@ -1432,8 +1460,8 @@ function mountDownload(server, config) {
|
|
|
1432
1460
|
const buffer = await readCapped(res, max);
|
|
1433
1461
|
if (!buffer)
|
|
1434
1462
|
return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
|
|
1435
|
-
const dir = config.dirFromArgs?.(args) ?? config.defaultDir;
|
|
1436
|
-
const filePath =
|
|
1463
|
+
const dir = resolve(config.dirFromArgs?.(args) ?? config.defaultDir);
|
|
1464
|
+
const filePath = resolve(dir, `${baseNameFor(url)}${extensionFor(url, mimeType)}`);
|
|
1437
1465
|
await writeDownload(dir, filePath, buffer);
|
|
1438
1466
|
return textResult(JSON.stringify({ path: filePath, size: buffer.length, mimeType }, null, 2));
|
|
1439
1467
|
} catch (err) {
|
|
@@ -1557,6 +1585,13 @@ function assertCimdClientId(clientId) {
|
|
|
1557
1585
|
}
|
|
1558
1586
|
return url;
|
|
1559
1587
|
}
|
|
1588
|
+
function parseAgeSeconds(headers) {
|
|
1589
|
+
const raw = headers.get("age");
|
|
1590
|
+
if (raw === null)
|
|
1591
|
+
return 0;
|
|
1592
|
+
const first = raw.split(",")[0]?.trim() ?? "";
|
|
1593
|
+
return /^\d+$/.test(first) ? Number(first) : 0;
|
|
1594
|
+
}
|
|
1560
1595
|
function responseFreshness(headers, policy, now) {
|
|
1561
1596
|
const maxTtl = policy.maxTtlMs ?? 3600000;
|
|
1562
1597
|
const cacheControl = headers.get("cache-control") ?? "";
|
|
@@ -1566,26 +1601,28 @@ function responseFreshness(headers, policy, now) {
|
|
|
1566
1601
|
if (/(?:^|,)\s*no-cache\s*(?:,|$)/i.test(cacheControl)) {
|
|
1567
1602
|
return { freshnessMs: 0, store: true };
|
|
1568
1603
|
}
|
|
1569
|
-
const maxAge = cacheControl.match(/(?:^|,)\s*max-age=(
|
|
1604
|
+
const maxAge = cacheControl.match(/(?:^|,)\s*max-age=([^,\s]*)/i)?.[1];
|
|
1570
1605
|
if (maxAge !== undefined) {
|
|
1571
|
-
|
|
1606
|
+
if (!/^\d+$/.test(maxAge))
|
|
1607
|
+
return { freshnessMs: 0, store: true };
|
|
1608
|
+
const age = parseAgeSeconds(headers);
|
|
1572
1609
|
return {
|
|
1573
1610
|
freshnessMs: Math.max(0, Math.min(maxTtl, (Number(maxAge) - age) * 1000)),
|
|
1574
1611
|
store: true
|
|
1575
1612
|
};
|
|
1576
1613
|
}
|
|
1577
1614
|
const expires = headers.get("expires");
|
|
1578
|
-
if (expires) {
|
|
1615
|
+
if (expires !== null) {
|
|
1579
1616
|
const expiresAt = Date.parse(expires);
|
|
1580
|
-
if (Number.isFinite(expiresAt))
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1617
|
+
if (!Number.isFinite(expiresAt))
|
|
1618
|
+
return { freshnessMs: 0, store: true };
|
|
1619
|
+
const dateAt = Date.parse(headers.get("date") ?? "");
|
|
1620
|
+
const ageMs = parseAgeSeconds(headers) * 1000;
|
|
1621
|
+
const lifetime = Number.isFinite(dateAt) ? expiresAt - dateAt : expiresAt - now;
|
|
1622
|
+
return {
|
|
1623
|
+
freshnessMs: Math.max(0, Math.min(maxTtl, lifetime - ageMs)),
|
|
1624
|
+
store: true
|
|
1625
|
+
};
|
|
1589
1626
|
}
|
|
1590
1627
|
return { freshnessMs: Math.min(maxTtl, policy.defaultTtlMs ?? 300000), store: true };
|
|
1591
1628
|
}
|
|
@@ -1617,10 +1654,21 @@ function isJsonContentType(value) {
|
|
|
1617
1654
|
function createCimdResolver(options) {
|
|
1618
1655
|
const policy = options.cache ?? {};
|
|
1619
1656
|
const now = options.now ?? Date.now;
|
|
1620
|
-
const
|
|
1657
|
+
const positives = new Map;
|
|
1658
|
+
const negatives = new Map;
|
|
1621
1659
|
const inflight = new Map;
|
|
1622
1660
|
const maxEntries = policy.maxEntries ?? 256;
|
|
1661
|
+
const maxResolutions = policy.maxResolutions ?? 120;
|
|
1662
|
+
const maxResolutionsPerClient = policy.maxResolutionsPerClient ?? 10;
|
|
1663
|
+
const resolutionWindowMs = policy.resolutionWindowMs ?? 60000;
|
|
1664
|
+
let resolutionWindowStartedAt = now();
|
|
1665
|
+
let resolutionsInWindow = 0;
|
|
1666
|
+
const clientWindows = new Map;
|
|
1667
|
+
const CLIENT_WINDOW_CAP = 4096;
|
|
1623
1668
|
assertPositiveInteger("cimd.cache.maxEntries", maxEntries);
|
|
1669
|
+
assertPositiveInteger("cimd.cache.maxResolutions", maxResolutions);
|
|
1670
|
+
assertPositiveInteger("cimd.cache.maxResolutionsPerClient", maxResolutionsPerClient);
|
|
1671
|
+
assertPositiveInteger("cimd.cache.resolutionWindowMs", resolutionWindowMs);
|
|
1624
1672
|
assertNonNegativeInteger("cimd.cache.defaultTtlMs", policy.defaultTtlMs ?? 300000);
|
|
1625
1673
|
assertNonNegativeInteger("cimd.cache.maxTtlMs", policy.maxTtlMs ?? 3600000);
|
|
1626
1674
|
assertNonNegativeInteger("cimd.cache.negativeTtlMs", policy.negativeTtlMs ?? 1e4);
|
|
@@ -1629,19 +1677,35 @@ function createCimdResolver(options) {
|
|
|
1629
1677
|
options.onCacheEvent?.(event);
|
|
1630
1678
|
} catch {}
|
|
1631
1679
|
};
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
const oldest = cache.keys().next().value;
|
|
1680
|
+
const readCache = (key) => positives.get(key) ?? negatives.get(key);
|
|
1681
|
+
const trimOldest = (pool) => {
|
|
1682
|
+
while (pool.size > maxEntries) {
|
|
1683
|
+
const oldest = pool.keys().next().value;
|
|
1637
1684
|
if (oldest === undefined)
|
|
1638
1685
|
break;
|
|
1639
|
-
|
|
1686
|
+
pool.delete(oldest);
|
|
1640
1687
|
}
|
|
1641
1688
|
};
|
|
1689
|
+
const touch = (key, value) => {
|
|
1690
|
+
if (value.ok) {
|
|
1691
|
+
negatives.delete(key);
|
|
1692
|
+
positives.delete(key);
|
|
1693
|
+
positives.set(key, value);
|
|
1694
|
+
trimOldest(positives);
|
|
1695
|
+
} else {
|
|
1696
|
+
positives.delete(key);
|
|
1697
|
+
negatives.delete(key);
|
|
1698
|
+
negatives.set(key, value);
|
|
1699
|
+
trimOldest(negatives);
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
const dropCache = (key) => {
|
|
1703
|
+
positives.delete(key);
|
|
1704
|
+
negatives.delete(key);
|
|
1705
|
+
};
|
|
1642
1706
|
const resolveOne = async (clientId) => {
|
|
1643
1707
|
const url = assertCimdClientId(clientId);
|
|
1644
|
-
const cached =
|
|
1708
|
+
const cached = readCache(clientId);
|
|
1645
1709
|
if (cached && cached.expiresAt > now()) {
|
|
1646
1710
|
touch(clientId, cached);
|
|
1647
1711
|
if (cached.ok) {
|
|
@@ -1652,17 +1716,40 @@ function createCimdResolver(options) {
|
|
|
1652
1716
|
throw new Error(cached.message);
|
|
1653
1717
|
}
|
|
1654
1718
|
emit({ clientId, status: "miss" });
|
|
1719
|
+
const currentTime = now();
|
|
1720
|
+
if (currentTime - resolutionWindowStartedAt >= resolutionWindowMs) {
|
|
1721
|
+
resolutionWindowStartedAt = currentTime;
|
|
1722
|
+
resolutionsInWindow = 0;
|
|
1723
|
+
}
|
|
1724
|
+
let clientWindow = clientWindows.get(clientId);
|
|
1725
|
+
if (!clientWindow || currentTime - clientWindow.startedAt >= resolutionWindowMs) {
|
|
1726
|
+
clientWindow = { startedAt: currentTime, count: 0 };
|
|
1727
|
+
clientWindows.delete(clientId);
|
|
1728
|
+
clientWindows.set(clientId, clientWindow);
|
|
1729
|
+
while (clientWindows.size > CLIENT_WINDOW_CAP) {
|
|
1730
|
+
const oldest = clientWindows.keys().next().value;
|
|
1731
|
+
if (oldest === undefined)
|
|
1732
|
+
break;
|
|
1733
|
+
clientWindows.delete(oldest);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (clientWindow.count >= maxResolutionsPerClient) {
|
|
1737
|
+
throw new Error("CIMD metadata resolution rate limit exceeded for this client_id");
|
|
1738
|
+
}
|
|
1739
|
+
if (resolutionsInWindow >= maxResolutions) {
|
|
1740
|
+
throw new Error("CIMD metadata resolution rate limit exceeded");
|
|
1741
|
+
}
|
|
1742
|
+
clientWindow.count += 1;
|
|
1743
|
+
resolutionsInWindow += 1;
|
|
1655
1744
|
const conditionalHeaders = {};
|
|
1656
1745
|
if (cached?.ok && cached.etag)
|
|
1657
1746
|
conditionalHeaders["if-none-match"] = cached.etag;
|
|
1658
1747
|
if (cached?.ok && cached.lastModified) {
|
|
1659
1748
|
conditionalHeaders["if-modified-since"] = cached.lastModified;
|
|
1660
1749
|
}
|
|
1661
|
-
let cacheFailure = true;
|
|
1662
1750
|
try {
|
|
1663
1751
|
const response = await options.fetcher.fetch(url, conditionalHeaders);
|
|
1664
1752
|
const freshness = responseFreshness(response.headers, policy, now());
|
|
1665
|
-
cacheFailure = freshness.store;
|
|
1666
1753
|
if (response.status === 304 && cached?.ok) {
|
|
1667
1754
|
const refreshed = {
|
|
1668
1755
|
...cached,
|
|
@@ -1671,7 +1758,7 @@ function createCimdResolver(options) {
|
|
|
1671
1758
|
if (freshness.store)
|
|
1672
1759
|
touch(clientId, refreshed);
|
|
1673
1760
|
else
|
|
1674
|
-
|
|
1761
|
+
dropCache(clientId);
|
|
1675
1762
|
emit({ clientId, status: "revalidated", freshnessMs: freshness.freshnessMs });
|
|
1676
1763
|
return refreshed.client;
|
|
1677
1764
|
}
|
|
@@ -1691,7 +1778,7 @@ function createCimdResolver(options) {
|
|
|
1691
1778
|
throw new Error("CIMD redirect_uris contains a URI forbidden for the application type");
|
|
1692
1779
|
}
|
|
1693
1780
|
const client = toRegisteredClient(metadata);
|
|
1694
|
-
if (freshness.store) {
|
|
1781
|
+
if (freshness.store && Number.isFinite(freshness.freshnessMs)) {
|
|
1695
1782
|
touch(clientId, {
|
|
1696
1783
|
ok: true,
|
|
1697
1784
|
client,
|
|
@@ -1704,18 +1791,16 @@ function createCimdResolver(options) {
|
|
|
1704
1791
|
}
|
|
1705
1792
|
});
|
|
1706
1793
|
} else {
|
|
1707
|
-
|
|
1794
|
+
dropCache(clientId);
|
|
1708
1795
|
}
|
|
1709
1796
|
return client;
|
|
1710
1797
|
} catch (error) {
|
|
1711
1798
|
const message = error instanceof Error ? error.message : "CIMD metadata resolution failed";
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1799
|
+
touch(clientId, {
|
|
1800
|
+
ok: false,
|
|
1801
|
+
message,
|
|
1802
|
+
expiresAt: now() + (policy.negativeTtlMs ?? 1e4)
|
|
1803
|
+
});
|
|
1719
1804
|
throw error;
|
|
1720
1805
|
}
|
|
1721
1806
|
};
|
|
@@ -1967,6 +2052,9 @@ function mountOAuthProvider(config) {
|
|
|
1967
2052
|
const result = await config.authorizeUser(req, authRequest);
|
|
1968
2053
|
if (result instanceof Response)
|
|
1969
2054
|
return result;
|
|
2055
|
+
if (!Array.isArray(result.approvedScopes) || !result.approvedScopes.every((scope2) => typeof scope2 === "string")) {
|
|
2056
|
+
throw new Error("authorizeUser must return approvedScopes as an array of strings");
|
|
2057
|
+
}
|
|
1970
2058
|
const approvedScopes = [...new Set(result.approvedScopes)];
|
|
1971
2059
|
if (approvedScopes.some((approvedScope2) => !requestedScopes.includes(approvedScope2) || config.scopesSupported !== undefined && !supportedScopes.has(approvedScope2))) {
|
|
1972
2060
|
throw new Error("authorizeUser approvedScopes must be a subset of requested scopes");
|
|
@@ -2453,7 +2541,7 @@ function implementRemote(contract, http, options) {
|
|
|
2453
2541
|
}
|
|
2454
2542
|
// src/tools/tool-logger.ts
|
|
2455
2543
|
function createToolLogger(config = {}) {
|
|
2456
|
-
const log = config.log ?? ((line) => console.
|
|
2544
|
+
const log = config.log ?? ((line) => console.error(line));
|
|
2457
2545
|
return {
|
|
2458
2546
|
afterToolCall: ({ toolName, result, durationMs, context, endpoint }) => {
|
|
2459
2547
|
const record = {
|
|
@@ -2551,10 +2639,11 @@ function summarizeTransports(surface) {
|
|
|
2551
2639
|
};
|
|
2552
2640
|
}
|
|
2553
2641
|
// src/tools/view-file.ts
|
|
2554
|
-
import { readFile,
|
|
2555
|
-
import { extname as extname2, resolve } from "node:path";
|
|
2642
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2643
|
+
import { extname as extname2, resolve as resolve2 } from "node:path";
|
|
2556
2644
|
import { z as z7 } from "zod";
|
|
2557
2645
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
2646
|
+
var MAX_VIEW_FILES = 20;
|
|
2558
2647
|
var EXT_MIME = {
|
|
2559
2648
|
".png": "image/png",
|
|
2560
2649
|
".jpg": "image/jpeg",
|
|
@@ -2569,22 +2658,28 @@ var EXT_MIME = {
|
|
|
2569
2658
|
".webm": "video/webm",
|
|
2570
2659
|
".mov": "video/quicktime"
|
|
2571
2660
|
};
|
|
2572
|
-
async function fetchSource(pathOrUrl, options) {
|
|
2661
|
+
async function fetchSource(pathOrUrl, options, maxBytes) {
|
|
2573
2662
|
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
2574
2663
|
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
2575
2664
|
const url = new URL(pathOrUrl);
|
|
2576
|
-
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false
|
|
2665
|
+
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
|
|
2666
|
+
timeoutMs: options.timeoutMs
|
|
2667
|
+
});
|
|
2577
2668
|
if (!res.ok)
|
|
2578
2669
|
throw new Error(`HTTP ${res.status}`);
|
|
2579
2670
|
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
2580
2671
|
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
2672
|
+
if (mimeType.startsWith("video/")) {
|
|
2673
|
+
await res.body?.cancel();
|
|
2674
|
+
return { video: true, mimeType, bytesRead: 0 };
|
|
2675
|
+
}
|
|
2581
2676
|
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
2582
|
-
if (declared >
|
|
2677
|
+
if (declared > maxBytes) {
|
|
2583
2678
|
await res.body?.cancel();
|
|
2584
|
-
return { tooLarge: true, mimeType };
|
|
2679
|
+
return { tooLarge: true, mimeType, bytesRead: 0 };
|
|
2585
2680
|
}
|
|
2586
|
-
const
|
|
2587
|
-
return
|
|
2681
|
+
const buffer2 = await readCapped(res, maxBytes);
|
|
2682
|
+
return buffer2 ? { buffer: buffer2, mimeType, bytesRead: buffer2.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
|
|
2588
2683
|
}
|
|
2589
2684
|
if (!options.baseDir) {
|
|
2590
2685
|
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
@@ -2592,76 +2687,109 @@ async function fetchSource(pathOrUrl, options) {
|
|
|
2592
2687
|
if (!extMime) {
|
|
2593
2688
|
throw new Error("refusing to read a non-media file");
|
|
2594
2689
|
}
|
|
2595
|
-
const root =
|
|
2596
|
-
const target =
|
|
2690
|
+
const root = resolve2(options.baseDir);
|
|
2691
|
+
const target = resolve2(root, pathOrUrl);
|
|
2597
2692
|
if (!isWithinDir(root, target)) {
|
|
2598
2693
|
throw new Error("path escapes the allowed directory");
|
|
2599
2694
|
}
|
|
2600
|
-
const realTarget = await
|
|
2601
|
-
if (realTarget === null)
|
|
2602
|
-
throw new Error("file not found");
|
|
2603
|
-
const realRoot = await realpath(root).catch(() => root);
|
|
2604
|
-
if (!isWithinDir(realRoot, realTarget)) {
|
|
2695
|
+
const realTarget = await realPathWithinDir(root, target);
|
|
2696
|
+
if (realTarget === null) {
|
|
2605
2697
|
throw new Error("path escapes the allowed directory");
|
|
2606
2698
|
}
|
|
2607
2699
|
const info = await stat(realTarget).catch(() => null);
|
|
2608
2700
|
if (!info?.isFile())
|
|
2609
2701
|
throw new Error("file not found");
|
|
2610
|
-
if (info.size >
|
|
2611
|
-
return { tooLarge: true, mimeType: extMime };
|
|
2612
|
-
|
|
2702
|
+
if (info.size > maxBytes)
|
|
2703
|
+
return { tooLarge: true, mimeType: extMime, bytesRead: 0 };
|
|
2704
|
+
const buffer = await readFile(realTarget);
|
|
2705
|
+
return { buffer, mimeType: extMime, bytesRead: buffer.length };
|
|
2613
2706
|
}
|
|
2614
|
-
async function
|
|
2707
|
+
async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
|
|
2708
|
+
if (maxBytes <= 0) {
|
|
2709
|
+
return {
|
|
2710
|
+
content: [
|
|
2711
|
+
{ type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
|
|
2712
|
+
],
|
|
2713
|
+
bytes: 0
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2615
2716
|
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
2616
2717
|
if (extMime?.startsWith("video/")) {
|
|
2617
|
-
return
|
|
2718
|
+
return {
|
|
2719
|
+
content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
|
|
2720
|
+
bytes: 0
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
const result = await fetchSource(pathOrUrl, options, maxBytes);
|
|
2724
|
+
if ("video" in result) {
|
|
2725
|
+
return {
|
|
2726
|
+
content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
|
|
2727
|
+
bytes: result.bytesRead
|
|
2728
|
+
};
|
|
2618
2729
|
}
|
|
2619
|
-
const result = await fetchSource(pathOrUrl, options);
|
|
2620
2730
|
if ("tooLarge" in result) {
|
|
2621
|
-
return
|
|
2731
|
+
return {
|
|
2732
|
+
content: [
|
|
2733
|
+
{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
|
|
2734
|
+
],
|
|
2735
|
+
bytes: result.bytesRead
|
|
2736
|
+
};
|
|
2622
2737
|
}
|
|
2623
2738
|
const { buffer, mimeType } = result;
|
|
2624
2739
|
const sizeKb = (buffer.length / 1024).toFixed(0);
|
|
2625
|
-
if (mimeType.startsWith("video/")) {
|
|
2626
|
-
return [{ type: "text", text: `[video] ${mimeType}, ${sizeKb}KB — ${pathOrUrl}` }];
|
|
2627
|
-
}
|
|
2628
2740
|
if (mimeType.startsWith("image/")) {
|
|
2629
|
-
return
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2741
|
+
return {
|
|
2742
|
+
content: [
|
|
2743
|
+
{
|
|
2744
|
+
type: "image",
|
|
2745
|
+
data: buffer.toString("base64"),
|
|
2746
|
+
mimeType,
|
|
2747
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
2748
|
+
},
|
|
2749
|
+
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
2750
|
+
],
|
|
2751
|
+
bytes: buffer.length
|
|
2752
|
+
};
|
|
2638
2753
|
}
|
|
2639
2754
|
if (mimeType.startsWith("audio/")) {
|
|
2640
|
-
return
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2755
|
+
return {
|
|
2756
|
+
content: [
|
|
2757
|
+
{
|
|
2758
|
+
type: "audio",
|
|
2759
|
+
data: buffer.toString("base64"),
|
|
2760
|
+
mimeType,
|
|
2761
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
2762
|
+
},
|
|
2763
|
+
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
2764
|
+
],
|
|
2765
|
+
bytes: buffer.length
|
|
2766
|
+
};
|
|
2649
2767
|
}
|
|
2650
|
-
return
|
|
2768
|
+
return {
|
|
2769
|
+
content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
|
|
2770
|
+
bytes: buffer.length
|
|
2771
|
+
};
|
|
2651
2772
|
}
|
|
2773
|
+
async function resolveMedia(pathOrUrl, options = {}) {
|
|
2774
|
+
return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
|
|
2775
|
+
}
|
|
2776
|
+
var ViewFileInputSchema = z7.object({
|
|
2777
|
+
paths: z7.union([z7.string(), z7.array(z7.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
|
|
2778
|
+
});
|
|
2652
2779
|
function mountViewFile(server, options = {}) {
|
|
2653
2780
|
server.registerTool("view_file", {
|
|
2654
2781
|
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.",
|
|
2655
|
-
inputSchema:
|
|
2656
|
-
paths: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
|
|
2657
|
-
},
|
|
2782
|
+
inputSchema: ViewFileInputSchema.shape,
|
|
2658
2783
|
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
2659
2784
|
}, async (args) => {
|
|
2660
2785
|
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
2661
2786
|
const content = [];
|
|
2787
|
+
let remainingBytes = MAX_INLINE_BYTES;
|
|
2662
2788
|
for (const pathOrUrl of list) {
|
|
2663
2789
|
try {
|
|
2664
|
-
|
|
2790
|
+
const resolved = await resolveMediaWithinBudget(pathOrUrl, options, remainingBytes);
|
|
2791
|
+
remainingBytes -= resolved.bytes;
|
|
2792
|
+
content.push(...resolved.content);
|
|
2665
2793
|
} catch (err) {
|
|
2666
2794
|
content.push({
|
|
2667
2795
|
type: "text",
|