stitchkit 0.44.1 → 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.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/browser/socket-io.d.ts +16 -0
  4. package/dist/browser/socket-io.d.ts.map +1 -1
  5. package/dist/cli.js +3 -4
  6. package/dist/contract/errors.d.ts +1 -0
  7. package/dist/contract/errors.d.ts.map +1 -1
  8. package/dist/contract/index.js +2 -2
  9. package/dist/{index-yh0p5ddn.js → index-5s8b7z6q.js} +31 -11
  10. package/dist/{index-mvjyq9h9.js → index-62pqb23z.js} +309 -144
  11. package/dist/index-c40tkxcd.js +181 -0
  12. package/dist/{index-r6czv7yt.js → index-pwyedf7b.js} +4 -3
  13. package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
  14. package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
  15. package/dist/{index-36f1xkdb.js → index-w1s873ng.js} +2 -2
  16. package/dist/index-yxpe3phd.js +63 -0
  17. package/dist/{index-4fyn9f09.js → index-zwqty9zf.js} +396 -246
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +266 -17
  21. package/dist/internal/errors.d.ts.map +1 -1
  22. package/dist/internal/secure-fetch.d.ts +23 -3
  23. package/dist/internal/secure-fetch.d.ts.map +1 -1
  24. package/dist/internal/typed.d.ts +7 -0
  25. package/dist/internal/typed.d.ts.map +1 -1
  26. package/dist/internal/within-dir.d.ts +6 -0
  27. package/dist/internal/within-dir.d.ts.map +1 -1
  28. package/dist/logger.d.ts +8 -0
  29. package/dist/logger.d.ts.map +1 -0
  30. package/dist/node.d.ts +1 -0
  31. package/dist/node.d.ts.map +1 -1
  32. package/dist/node.js +5 -4
  33. package/dist/observability/audit.d.ts.map +1 -1
  34. package/dist/observability/event.d.ts +7 -0
  35. package/dist/observability/event.d.ts.map +1 -1
  36. package/dist/observability/index.js +54 -125
  37. package/dist/observability/sanitize.d.ts +8 -1
  38. package/dist/observability/sanitize.d.ts.map +1 -1
  39. package/dist/observability/trace.d.ts +3 -1
  40. package/dist/observability/trace.d.ts.map +1 -1
  41. package/dist/react/cache-bridge.d.ts +4 -0
  42. package/dist/react/cache-bridge.d.ts.map +1 -1
  43. package/dist/react.js +46 -5
  44. package/dist/realtime/contract.d.ts +39 -0
  45. package/dist/realtime/contract.d.ts.map +1 -0
  46. package/dist/realtime/index.d.ts +3 -0
  47. package/dist/realtime/index.d.ts.map +1 -0
  48. package/dist/realtime/rejection.d.ts +10 -0
  49. package/dist/realtime/rejection.d.ts.map +1 -0
  50. package/dist/realtime/socket.d.ts +30 -0
  51. package/dist/realtime/socket.d.ts.map +1 -0
  52. package/dist/server/cache.d.ts +5 -1
  53. package/dist/server/cache.d.ts.map +1 -1
  54. package/dist/server/error-hook.d.ts +1 -0
  55. package/dist/server/error-hook.d.ts.map +1 -1
  56. package/dist/server/file.d.ts +6 -0
  57. package/dist/server/file.d.ts.map +1 -1
  58. package/dist/server/implement.d.ts.map +1 -1
  59. package/dist/server/index.d.ts +3 -3
  60. package/dist/server/index.d.ts.map +1 -1
  61. package/dist/server/index.js +110 -11
  62. package/dist/server/middleware/cors.d.ts.map +1 -1
  63. package/dist/server/openapi.d.ts.map +1 -1
  64. package/dist/server/realtime.d.ts +24 -0
  65. package/dist/server/realtime.d.ts.map +1 -0
  66. package/dist/server/router.d.ts +0 -8
  67. package/dist/server/router.d.ts.map +1 -1
  68. package/dist/server/stream.d.ts.map +1 -1
  69. package/dist/server/types.d.ts +2 -7
  70. package/dist/server/types.d.ts.map +1 -1
  71. package/dist/tools/cli-args.d.ts +11 -3
  72. package/dist/tools/cli-args.d.ts.map +1 -1
  73. package/dist/tools/cli.d.ts +5 -0
  74. package/dist/tools/cli.d.ts.map +1 -1
  75. package/dist/tools/coerce.d.ts.map +1 -1
  76. package/dist/tools/json-schema.d.ts.map +1 -1
  77. package/dist/tools/mcp-handler.d.ts +4 -3
  78. package/dist/tools/mcp-handler.d.ts.map +1 -1
  79. package/dist/tools/mcp-prepare.d.ts +95 -0
  80. package/dist/tools/mcp-prepare.d.ts.map +1 -0
  81. package/dist/tools/mcp-round.d.ts +4 -0
  82. package/dist/tools/mcp-round.d.ts.map +1 -1
  83. package/dist/tools/mcp-stdio.d.ts +2 -1
  84. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  85. package/dist/tools/mcp.d.ts +2 -78
  86. package/dist/tools/mcp.d.ts.map +1 -1
  87. package/dist/tools/mount-download.d.ts +5 -0
  88. package/dist/tools/mount-download.d.ts.map +1 -1
  89. package/dist/tools/native-mcp.d.ts +1 -1
  90. package/dist/tools/native-mcp.d.ts.map +1 -1
  91. package/dist/tools/oauth-provider.d.ts +20 -1
  92. package/dist/tools/oauth-provider.d.ts.map +1 -1
  93. package/dist/tools/tool-logger.d.ts +1 -1
  94. package/dist/tools/tool-logger.d.ts.map +1 -1
  95. package/dist/tools/view-file.d.ts +9 -0
  96. package/dist/tools/view-file.d.ts.map +1 -1
  97. package/dist/tools.d.ts +2 -1
  98. package/dist/tools.d.ts.map +1 -1
  99. package/dist/tools.js +342 -214
  100. package/llms-full.txt +175 -44
  101. package/llms.txt +1 -1
  102. package/package.json +2 -2
  103. package/dist/index-frfyw9fa.js +0 -33
  104. 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-36f1xkdb.js";
5
+ } from "./index-w1s873ng.js";
6
6
  import {
7
7
  DEFAULT_CORS_ALLOW_HEADERS
8
- } from "./index-6jypn22c.js";
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-mvjyq9h9.js";
29
+ } from "./index-62pqb23z.js";
30
30
  import {
31
+ isWithinDir,
32
+ realPathWithinDir,
31
33
  toJsonSchema
32
- } from "./index-frfyw9fa.js";
34
+ } from "./index-yxpe3phd.js";
33
35
  import {
34
- isWithinDir
35
- } from "./index-x3fcszf8.js";
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-yh0p5ddn.js";
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 validatePolicy(tool2, policy, maxRounds) {
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
- validatePolicy(options.tool, policy, options.runtime.maxRounds);
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
- const serialized = JSON.stringify(result.data, null, 2);
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
- throw new Error(`[stitchkit] MCP tool "${toolName ?? "unknown"}" produced a non-JSON output`);
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 defaultHostRejection(request) {
1233
- const host = request.headers.get("host");
1234
- if (!host || host === new URL(request.url).host)
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 hostRejection = config.security?.allowedHosts ? hostHeaderValidationResponse(request, [...config.security.allowedHosts]) : defaultHostRejection(request);
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 originRejection = config.security?.allowedOrigins ? originValidationResponse(request, [...config.security.allowedOrigins]) : defaultOriginRejection(request);
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 === null)
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, join } from "node:path";
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 = join(dir, `${baseNameFor(url)}${extensionFor(url, mimeType)}`);
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=(\d+)/i)?.[1];
1604
+ const maxAge = cacheControl.match(/(?:^|,)\s*max-age=([^,\s]*)/i)?.[1];
1570
1605
  if (maxAge !== undefined) {
1571
- const age = Number(headers.get("age") ?? 0);
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
- 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
- }
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 cache = new Map;
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 touch = (key, value) => {
1633
- cache.delete(key);
1634
- cache.set(key, value);
1635
- while (cache.size > maxEntries) {
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
- cache.delete(oldest);
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 = cache.get(clientId);
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
- cache.delete(clientId);
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
- cache.delete(clientId);
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
- if (cacheFailure) {
1713
- touch(clientId, {
1714
- ok: false,
1715
- message,
1716
- expiresAt: now() + (policy.negativeTtlMs ?? 1e4)
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.info(line));
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, realpath, stat } from "node:fs/promises";
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 > MAX_INLINE_BYTES) {
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 buffer = await readCapped(res, MAX_INLINE_BYTES);
2587
- return buffer ? { buffer, mimeType } : { tooLarge: true, mimeType };
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 = resolve(options.baseDir);
2596
- const target = resolve(root, pathOrUrl);
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 realpath(target).catch(() => null);
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 > MAX_INLINE_BYTES)
2611
- return { tooLarge: true, mimeType: extMime };
2612
- return { buffer: await readFile(realTarget), mimeType: extMime };
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 resolveMedia(pathOrUrl, options = {}) {
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 [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }];
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 [{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }];
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
- type: "image",
2632
- data: buffer.toString("base64"),
2633
- mimeType,
2634
- annotations: { audience: ["user", "assistant"], priority: 0.9 }
2635
- },
2636
- { type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
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
- type: "audio",
2643
- data: buffer.toString("base64"),
2644
- mimeType,
2645
- annotations: { audience: ["user", "assistant"], priority: 0.9 }
2646
- },
2647
- { type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
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 [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }];
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
- content.push(...await resolveMedia(pathOrUrl, options));
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",