stitchkit 0.45.0 → 0.47.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 (122) hide show
  1. package/README.md +5 -4
  2. package/dist/browser/cancellation.d.ts +14 -0
  3. package/dist/browser/cancellation.d.ts.map +1 -0
  4. package/dist/browser/client-multipart.d.ts +3 -1
  5. package/dist/browser/client-multipart.d.ts.map +1 -1
  6. package/dist/browser/client.d.ts +1 -0
  7. package/dist/browser/client.d.ts.map +1 -1
  8. package/dist/browser/http.d.ts +1 -0
  9. package/dist/browser/http.d.ts.map +1 -1
  10. package/dist/browser/socket-io.d.ts +3 -1
  11. package/dist/browser/socket-io.d.ts.map +1 -1
  12. package/dist/cli.js +3 -4
  13. package/dist/contract/define.d.ts +59 -15
  14. package/dist/contract/define.d.ts.map +1 -1
  15. package/dist/contract/errors.d.ts +1 -0
  16. package/dist/contract/errors.d.ts.map +1 -1
  17. package/dist/contract/index.d.ts +1 -1
  18. package/dist/contract/index.d.ts.map +1 -1
  19. package/dist/contract/index.js +2 -2
  20. package/dist/{index-w0mec79h.js → index-44xysy8r.js} +697 -301
  21. package/dist/{index-r6czv7yt.js → index-45dz4m51.js} +50 -3
  22. package/dist/index-8ekq6res.js +181 -0
  23. package/dist/{index-yh0p5ddn.js → index-ee621cmy.js} +39 -19
  24. package/dist/{index-mvjyq9h9.js → index-kp8xamqp.js} +309 -144
  25. package/dist/{index-36f1xkdb.js → index-nrytvb30.js} +5 -4
  26. package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
  27. package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
  28. package/dist/index-yxpe3phd.js +63 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +315 -134
  32. package/dist/internal/errors.d.ts.map +1 -1
  33. package/dist/internal/secure-fetch.d.ts +23 -3
  34. package/dist/internal/secure-fetch.d.ts.map +1 -1
  35. package/dist/internal/typed.d.ts +7 -0
  36. package/dist/internal/typed.d.ts.map +1 -1
  37. package/dist/internal/within-dir.d.ts +6 -0
  38. package/dist/internal/within-dir.d.ts.map +1 -1
  39. package/dist/logger.d.ts +8 -0
  40. package/dist/logger.d.ts.map +1 -0
  41. package/dist/node.js +3 -4
  42. package/dist/observability/audit.d.ts +22 -3
  43. package/dist/observability/audit.d.ts.map +1 -1
  44. package/dist/observability/event.d.ts +7 -0
  45. package/dist/observability/event.d.ts.map +1 -1
  46. package/dist/observability/index.d.ts +1 -1
  47. package/dist/observability/index.d.ts.map +1 -1
  48. package/dist/observability/index.js +130 -134
  49. package/dist/observability/sanitize.d.ts +8 -1
  50. package/dist/observability/sanitize.d.ts.map +1 -1
  51. package/dist/observability/trace.d.ts +3 -1
  52. package/dist/observability/trace.d.ts.map +1 -1
  53. package/dist/react/cache-bridge.d.ts +4 -0
  54. package/dist/react/cache-bridge.d.ts.map +1 -1
  55. package/dist/react.js +46 -5
  56. package/dist/realtime/contract.d.ts +4 -1
  57. package/dist/realtime/contract.d.ts.map +1 -1
  58. package/dist/realtime/rejection.d.ts +10 -0
  59. package/dist/realtime/rejection.d.ts.map +1 -0
  60. package/dist/realtime/socket.d.ts +14 -1
  61. package/dist/realtime/socket.d.ts.map +1 -1
  62. package/dist/server/cache.d.ts +5 -1
  63. package/dist/server/cache.d.ts.map +1 -1
  64. package/dist/server/context.d.ts +8 -5
  65. package/dist/server/context.d.ts.map +1 -1
  66. package/dist/server/create.d.ts.map +1 -1
  67. package/dist/server/error-hook.d.ts +1 -0
  68. package/dist/server/error-hook.d.ts.map +1 -1
  69. package/dist/server/file.d.ts +6 -0
  70. package/dist/server/file.d.ts.map +1 -1
  71. package/dist/server/implement.d.ts +33 -2
  72. package/dist/server/implement.d.ts.map +1 -1
  73. package/dist/server/index.d.ts +5 -6
  74. package/dist/server/index.d.ts.map +1 -1
  75. package/dist/server/index.js +133 -14
  76. package/dist/server/middleware/auth.d.ts +7 -4
  77. package/dist/server/middleware/auth.d.ts.map +1 -1
  78. package/dist/server/middleware/cors.d.ts.map +1 -1
  79. package/dist/server/multipart.d.ts +13 -18
  80. package/dist/server/multipart.d.ts.map +1 -1
  81. package/dist/server/openapi.d.ts.map +1 -1
  82. package/dist/server/realtime.d.ts +2 -0
  83. package/dist/server/realtime.d.ts.map +1 -1
  84. package/dist/server/router.d.ts +0 -8
  85. package/dist/server/router.d.ts.map +1 -1
  86. package/dist/server/stream.d.ts.map +1 -1
  87. package/dist/server/types.d.ts +50 -22
  88. package/dist/server/types.d.ts.map +1 -1
  89. package/dist/tools/cli-args.d.ts +11 -3
  90. package/dist/tools/cli-args.d.ts.map +1 -1
  91. package/dist/tools/cli.d.ts +5 -0
  92. package/dist/tools/cli.d.ts.map +1 -1
  93. package/dist/tools/coerce.d.ts.map +1 -1
  94. package/dist/tools/json-schema.d.ts.map +1 -1
  95. package/dist/tools/mcp-handler.d.ts +4 -3
  96. package/dist/tools/mcp-handler.d.ts.map +1 -1
  97. package/dist/tools/mcp-prepare.d.ts +95 -0
  98. package/dist/tools/mcp-prepare.d.ts.map +1 -0
  99. package/dist/tools/mcp-round.d.ts +4 -0
  100. package/dist/tools/mcp-round.d.ts.map +1 -1
  101. package/dist/tools/mcp-stdio.d.ts +2 -1
  102. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  103. package/dist/tools/mcp.d.ts +2 -78
  104. package/dist/tools/mcp.d.ts.map +1 -1
  105. package/dist/tools/mount-download.d.ts +5 -0
  106. package/dist/tools/mount-download.d.ts.map +1 -1
  107. package/dist/tools/native-mcp.d.ts +1 -1
  108. package/dist/tools/native-mcp.d.ts.map +1 -1
  109. package/dist/tools/oauth-provider.d.ts +20 -1
  110. package/dist/tools/oauth-provider.d.ts.map +1 -1
  111. package/dist/tools/tool-logger.d.ts +1 -1
  112. package/dist/tools/tool-logger.d.ts.map +1 -1
  113. package/dist/tools/view-file.d.ts +9 -0
  114. package/dist/tools/view-file.d.ts.map +1 -1
  115. package/dist/tools.d.ts +2 -1
  116. package/dist/tools.d.ts.map +1 -1
  117. package/dist/tools.js +505 -278
  118. package/llms-full.txt +473 -85
  119. package/llms.txt +1 -1
  120. package/package.json +2 -2
  121. package/dist/index-frfyw9fa.js +0 -33
  122. 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-nrytvb30.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-kp8xamqp.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-8ekq6res.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-ee621cmy.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");
@@ -2070,6 +2158,63 @@ function mountOAuthProvider(config) {
2070
2158
  };
2071
2159
  return dcrEnabled ? [metadataRoute, registerRoute, authorizeRoute, tokenRoute] : [metadataRoute, authorizeRoute, tokenRoute];
2072
2160
  }
2161
+ // src/browser/cancellation.ts
2162
+ class RequestCancellationError extends Error {
2163
+ cause;
2164
+ constructor(cause) {
2165
+ super(cause === "caller" ? "Request was aborted" : "Request timed out");
2166
+ this.cause = cause;
2167
+ this.name = "RequestCancellationError";
2168
+ }
2169
+ }
2170
+ function createRequestCancellation(caller, timeoutMs) {
2171
+ if (!caller && timeoutMs === undefined) {
2172
+ return { signal: undefined, run: (operation) => operation() };
2173
+ }
2174
+ const controller = new AbortController;
2175
+ let cause;
2176
+ let timer;
2177
+ const abortFromCaller = () => {
2178
+ if (cause)
2179
+ return;
2180
+ cause = "caller";
2181
+ controller.abort(caller?.reason);
2182
+ };
2183
+ if (caller?.aborted)
2184
+ abortFromCaller();
2185
+ else
2186
+ caller?.addEventListener("abort", abortFromCaller, { once: true });
2187
+ if (timeoutMs !== undefined) {
2188
+ timer = setTimeout(() => {
2189
+ if (cause)
2190
+ return;
2191
+ cause = "timeout";
2192
+ controller.abort(new DOMException("Request timed out", "TimeoutError"));
2193
+ }, timeoutMs);
2194
+ }
2195
+ return {
2196
+ signal: controller.signal,
2197
+ async run(operation) {
2198
+ try {
2199
+ if (cause === "caller")
2200
+ throw cancellationError(cause);
2201
+ return await operation(controller.signal);
2202
+ } catch (error) {
2203
+ if (cause)
2204
+ throw cancellationError(cause);
2205
+ throw error;
2206
+ } finally {
2207
+ if (timer !== undefined)
2208
+ clearTimeout(timer);
2209
+ caller?.removeEventListener("abort", abortFromCaller);
2210
+ }
2211
+ }
2212
+ };
2213
+ }
2214
+ function cancellationError(cause) {
2215
+ return new RequestCancellationError(cause);
2216
+ }
2217
+
2073
2218
  // src/browser/client-multipart.ts
2074
2219
  function isFileDescriptor(value) {
2075
2220
  return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
@@ -2088,6 +2233,35 @@ function appendFormFields(formData, values, skipKeys) {
2088
2233
  formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
2089
2234
  }
2090
2235
  }
2236
+ function buildMultipartForm(descriptor, values) {
2237
+ const formData = new FormData;
2238
+ const fileFields = new Set(Object.keys(descriptor.files));
2239
+ for (const [field, policy] of Object.entries(descriptor.files)) {
2240
+ const value = values[field];
2241
+ if (value === undefined) {
2242
+ if (policy.required !== false)
2243
+ throw new Error(`Missing multipart file field: ${field}`);
2244
+ continue;
2245
+ }
2246
+ if (policy.multiple === true) {
2247
+ if (!Array.isArray(value) || value.length === 0) {
2248
+ throw new Error(`Multipart file field "${field}" must be a non-empty array`);
2249
+ }
2250
+ for (const file of value) {
2251
+ if (!isMultipartFile(file)) {
2252
+ throw new Error(`Invalid multipart file field: ${field}`);
2253
+ }
2254
+ appendMultipartFile(formData, field, file);
2255
+ }
2256
+ continue;
2257
+ }
2258
+ if (!isMultipartFile(value))
2259
+ throw new Error(`Invalid multipart file field: ${field}`);
2260
+ appendMultipartFile(formData, field, value);
2261
+ }
2262
+ appendFormFields(formData, values, fileFields);
2263
+ return formData;
2264
+ }
2091
2265
 
2092
2266
  // src/browser/client-url.ts
2093
2267
  function isParamArray(value) {
@@ -2291,95 +2465,108 @@ function setClientMethod(target, key, method) {
2291
2465
  function createHttpMethod(endpoint, prefix, client, config) {
2292
2466
  const httpMethod = endpoint.method.toLowerCase();
2293
2467
  return (...args) => {
2294
- const firstArg = args[0] ?? {};
2468
+ const { requestArgs: firstArg, options } = splitClientCall(endpoint, args, config);
2295
2469
  const plan = planClientRequest(endpoint, prefix, firstArg, config);
2296
2470
  if (endpoint.multipart) {
2297
- const file = firstArg[endpoint.multipart];
2298
- if (!isMultipartFile(file)) {
2299
- throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
2300
- }
2301
2471
  if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
2302
2472
  throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
2303
2473
  }
2304
- const formData = new FormData;
2305
- appendMultipartFile(formData, endpoint.multipart, file);
2306
- appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
2307
- return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
2474
+ const formData = buildMultipartForm(endpoint.multipart, plan.remainingArgs);
2475
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(options, endpoint)));
2308
2476
  }
2309
2477
  if (httpMethod === "get" || httpMethod === "head") {
2310
- return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(undefined, endpoint)));
2478
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(options, endpoint)));
2311
2479
  }
2312
2480
  if (httpMethod === "delete") {
2313
- return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
2481
+ return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(options, endpoint)));
2314
2482
  }
2315
- return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(undefined, endpoint)));
2483
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(options, endpoint)));
2316
2484
  };
2317
2485
  }
2318
2486
  function createFetchMethod(endpoint, prefix, config, contractConfig) {
2319
- return async (args) => {
2320
- const plan = planClientRequest(endpoint, prefix, args ?? {}, contractConfig);
2487
+ return async (...callArgs) => {
2488
+ const { requestArgs, options } = splitClientCall(endpoint, callArgs, contractConfig);
2489
+ const plan = planClientRequest(endpoint, prefix, requestArgs, contractConfig);
2321
2490
  const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
2322
2491
  const headers = {
2323
2492
  Accept: "application/json",
2324
2493
  ...typeof config.headers === "function" ? config.headers() : config.headers
2325
2494
  };
2326
- const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
2327
- const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
2495
+ const cancellation = createRequestCancellation(options?.signal, endpoint.timeout ?? config.timeout ?? 30000);
2496
+ const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && Object.keys(plan.remainingArgs).length > 0;
2328
2497
  if (hasBody)
2329
2498
  headers["Content-Type"] = "application/json";
2330
- if (endpoint.multipart && args) {
2331
- const file = args[endpoint.multipart];
2332
- if (!isMultipartFile(file)) {
2333
- throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
2334
- }
2335
- const formData = new FormData;
2336
- appendMultipartFile(formData, endpoint.multipart, file);
2337
- appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
2338
- const res2 = await fetch(url, {
2339
- method: endpoint.method,
2340
- headers,
2341
- credentials: config.credentials,
2342
- body: formData,
2343
- signal
2344
- });
2345
- if (!res2.ok) {
2346
- await throwForErrorResponse(res2, config, null);
2347
- }
2348
- if (endpoint.rawResponse)
2349
- return res2;
2350
- if (!endpoint.output) {
2351
- const text = await res2.text();
2352
- if (text.length > 0) {
2353
- throw new Error("Server returned data for an endpoint with no output contract");
2499
+ try {
2500
+ return await cancellation.run(async (signal) => {
2501
+ const body = endpoint.multipart ? buildMultipartForm(endpoint.multipart, plan.remainingArgs) : hasBody ? JSON.stringify(plan.remainingArgs) : undefined;
2502
+ const res = await fetch(url, {
2503
+ method: endpoint.method,
2504
+ headers,
2505
+ credentials: config.credentials,
2506
+ signal,
2507
+ ...body !== undefined && { body }
2508
+ });
2509
+ if (!res.ok) {
2510
+ await throwForErrorResponse(res, config, { error: res.statusText });
2354
2511
  }
2355
- return;
2356
- }
2357
- return endpoint.output.parse(await res2.json());
2358
- }
2359
- const res = await fetch(url, {
2360
- method: endpoint.method,
2361
- headers,
2362
- credentials: config.credentials,
2363
- signal,
2364
- ...hasBody && {
2365
- body: JSON.stringify(plan.remainingArgs)
2366
- }
2367
- });
2368
- if (!res.ok) {
2369
- await throwForErrorResponse(res, config, { error: res.statusText });
2370
- }
2371
- if (endpoint.rawResponse)
2372
- return res;
2373
- if (!endpoint.output) {
2374
- const text = await res.text();
2375
- if (text.length > 0) {
2376
- throw new Error("Server returned data for an endpoint with no output contract");
2512
+ if (endpoint.rawResponse)
2513
+ return res;
2514
+ if (!endpoint.output) {
2515
+ const text = await res.text();
2516
+ if (text.length > 0) {
2517
+ throw new Error("Server returned data for an endpoint with no output contract");
2518
+ }
2519
+ return;
2520
+ }
2521
+ return endpoint.output.parse(await res.json());
2522
+ });
2523
+ } catch (error) {
2524
+ if (error instanceof RequestCancellationError) {
2525
+ throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
2377
2526
  }
2378
- return;
2527
+ if (ApiError.is(error))
2528
+ throw error;
2529
+ const message = error instanceof Error ? error.message : undefined;
2530
+ throw new ApiError("UNKNOWN_ERROR", 0, message ? { message } : undefined, message);
2379
2531
  }
2380
- return endpoint.output.parse(await res.json());
2381
2532
  };
2382
2533
  }
2534
+ function endpointHasArguments(endpoint) {
2535
+ return Boolean(endpoint.params || endpoint.input || endpoint.multipart);
2536
+ }
2537
+ function splitClientCall(endpoint, args, contractConfig) {
2538
+ const hasScopedArguments = (contractConfig?.stripPrefixKeys?.length ?? 0) > 0;
2539
+ if (!endpointHasArguments(endpoint) && !hasScopedArguments) {
2540
+ return {
2541
+ requestArgs: {},
2542
+ options: readClientRequestOptions(args[0])
2543
+ };
2544
+ }
2545
+ const requestArgs = args[0];
2546
+ if (requestArgs !== undefined && !isRecord(requestArgs)) {
2547
+ throw new TypeError("Endpoint arguments must be an object");
2548
+ }
2549
+ return {
2550
+ requestArgs: requestArgs ?? {},
2551
+ options: readClientRequestOptions(args[1])
2552
+ };
2553
+ }
2554
+ function readClientRequestOptions(value) {
2555
+ if (value === undefined)
2556
+ return;
2557
+ if (!isRecord(value))
2558
+ throw new TypeError("Client request options must be an object");
2559
+ const signal = value.signal;
2560
+ if (signal === undefined)
2561
+ return {};
2562
+ if (!isAbortSignal(signal)) {
2563
+ throw new TypeError("Client request signal must be an AbortSignal");
2564
+ }
2565
+ return { signal };
2566
+ }
2567
+ function isAbortSignal(value) {
2568
+ return typeof value === "object" && value !== null && "aborted" in value && typeof value.aborted === "boolean" && "addEventListener" in value && typeof value.addEventListener === "function" && "removeEventListener" in value && typeof value.removeEventListener === "function";
2569
+ }
2383
2570
  async function throwForErrorResponse(res, config, fallbackBody) {
2384
2571
  const body = await res.json().catch(() => fallbackBody);
2385
2572
  config.onError?.(res.status, body);
@@ -2453,7 +2640,7 @@ function implementRemote(contract, http, options) {
2453
2640
  }
2454
2641
  // src/tools/tool-logger.ts
2455
2642
  function createToolLogger(config = {}) {
2456
- const log = config.log ?? ((line) => console.info(line));
2643
+ const log = config.log ?? ((line) => console.error(line));
2457
2644
  return {
2458
2645
  afterToolCall: ({ toolName, result, durationMs, context, endpoint }) => {
2459
2646
  const record = {
@@ -2551,10 +2738,11 @@ function summarizeTransports(surface) {
2551
2738
  };
2552
2739
  }
2553
2740
  // src/tools/view-file.ts
2554
- import { readFile, realpath, stat } from "node:fs/promises";
2555
- import { extname as extname2, resolve } from "node:path";
2741
+ import { readFile, stat } from "node:fs/promises";
2742
+ import { extname as extname2, resolve as resolve2 } from "node:path";
2556
2743
  import { z as z7 } from "zod";
2557
2744
  var MAX_INLINE_BYTES = 20 * 1024 * 1024;
2745
+ var MAX_VIEW_FILES = 20;
2558
2746
  var EXT_MIME = {
2559
2747
  ".png": "image/png",
2560
2748
  ".jpg": "image/jpeg",
@@ -2569,22 +2757,28 @@ var EXT_MIME = {
2569
2757
  ".webm": "video/webm",
2570
2758
  ".mov": "video/quicktime"
2571
2759
  };
2572
- async function fetchSource(pathOrUrl, options) {
2760
+ async function fetchSource(pathOrUrl, options, maxBytes) {
2573
2761
  const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
2574
2762
  if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
2575
2763
  const url = new URL(pathOrUrl);
2576
- const res = await fetchGuarded(url, options.allowPrivateHosts ?? false);
2764
+ const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
2765
+ timeoutMs: options.timeoutMs
2766
+ });
2577
2767
  if (!res.ok)
2578
2768
  throw new Error(`HTTP ${res.status}`);
2579
2769
  const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
2580
2770
  const mimeType = headerMime || extMime || "application/octet-stream";
2771
+ if (mimeType.startsWith("video/")) {
2772
+ await res.body?.cancel();
2773
+ return { video: true, mimeType, bytesRead: 0 };
2774
+ }
2581
2775
  const declared = Number(res.headers.get("content-length") ?? 0);
2582
- if (declared > MAX_INLINE_BYTES) {
2776
+ if (declared > maxBytes) {
2583
2777
  await res.body?.cancel();
2584
- return { tooLarge: true, mimeType };
2778
+ return { tooLarge: true, mimeType, bytesRead: 0 };
2585
2779
  }
2586
- const buffer = await readCapped(res, MAX_INLINE_BYTES);
2587
- return buffer ? { buffer, mimeType } : { tooLarge: true, mimeType };
2780
+ const buffer2 = await readCapped(res, maxBytes);
2781
+ return buffer2 ? { buffer: buffer2, mimeType, bytesRead: buffer2.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
2588
2782
  }
2589
2783
  if (!options.baseDir) {
2590
2784
  throw new Error("local file paths are disabled — set baseDir to allow them");
@@ -2592,76 +2786,109 @@ async function fetchSource(pathOrUrl, options) {
2592
2786
  if (!extMime) {
2593
2787
  throw new Error("refusing to read a non-media file");
2594
2788
  }
2595
- const root = resolve(options.baseDir);
2596
- const target = resolve(root, pathOrUrl);
2789
+ const root = resolve2(options.baseDir);
2790
+ const target = resolve2(root, pathOrUrl);
2597
2791
  if (!isWithinDir(root, target)) {
2598
2792
  throw new Error("path escapes the allowed directory");
2599
2793
  }
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)) {
2794
+ const realTarget = await realPathWithinDir(root, target);
2795
+ if (realTarget === null) {
2605
2796
  throw new Error("path escapes the allowed directory");
2606
2797
  }
2607
2798
  const info = await stat(realTarget).catch(() => null);
2608
2799
  if (!info?.isFile())
2609
2800
  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 };
2801
+ if (info.size > maxBytes)
2802
+ return { tooLarge: true, mimeType: extMime, bytesRead: 0 };
2803
+ const buffer = await readFile(realTarget);
2804
+ return { buffer, mimeType: extMime, bytesRead: buffer.length };
2613
2805
  }
2614
- async function resolveMedia(pathOrUrl, options = {}) {
2806
+ async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
2807
+ if (maxBytes <= 0) {
2808
+ return {
2809
+ content: [
2810
+ { type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
2811
+ ],
2812
+ bytes: 0
2813
+ };
2814
+ }
2615
2815
  const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
2616
2816
  if (extMime?.startsWith("video/")) {
2617
- return [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }];
2817
+ return {
2818
+ content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
2819
+ bytes: 0
2820
+ };
2821
+ }
2822
+ const result = await fetchSource(pathOrUrl, options, maxBytes);
2823
+ if ("video" in result) {
2824
+ return {
2825
+ content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
2826
+ bytes: result.bytesRead
2827
+ };
2618
2828
  }
2619
- const result = await fetchSource(pathOrUrl, options);
2620
2829
  if ("tooLarge" in result) {
2621
- return [{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }];
2830
+ return {
2831
+ content: [
2832
+ { type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
2833
+ ],
2834
+ bytes: result.bytesRead
2835
+ };
2622
2836
  }
2623
2837
  const { buffer, mimeType } = result;
2624
2838
  const sizeKb = (buffer.length / 1024).toFixed(0);
2625
- if (mimeType.startsWith("video/")) {
2626
- return [{ type: "text", text: `[video] ${mimeType}, ${sizeKb}KB — ${pathOrUrl}` }];
2627
- }
2628
2839
  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
- ];
2840
+ return {
2841
+ content: [
2842
+ {
2843
+ type: "image",
2844
+ data: buffer.toString("base64"),
2845
+ mimeType,
2846
+ annotations: { audience: ["user", "assistant"], priority: 0.9 }
2847
+ },
2848
+ { type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
2849
+ ],
2850
+ bytes: buffer.length
2851
+ };
2638
2852
  }
2639
2853
  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
- ];
2854
+ return {
2855
+ content: [
2856
+ {
2857
+ type: "audio",
2858
+ data: buffer.toString("base64"),
2859
+ mimeType,
2860
+ annotations: { audience: ["user", "assistant"], priority: 0.9 }
2861
+ },
2862
+ { type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
2863
+ ],
2864
+ bytes: buffer.length
2865
+ };
2649
2866
  }
2650
- return [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }];
2867
+ return {
2868
+ content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
2869
+ bytes: buffer.length
2870
+ };
2871
+ }
2872
+ async function resolveMedia(pathOrUrl, options = {}) {
2873
+ return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
2651
2874
  }
2875
+ var ViewFileInputSchema = z7.object({
2876
+ paths: z7.union([z7.string(), z7.array(z7.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
2877
+ });
2652
2878
  function mountViewFile(server, options = {}) {
2653
2879
  server.registerTool("view_file", {
2654
2880
  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
- },
2881
+ inputSchema: ViewFileInputSchema.shape,
2658
2882
  annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
2659
2883
  }, async (args) => {
2660
2884
  const list = Array.isArray(args.paths) ? args.paths : [args.paths];
2661
2885
  const content = [];
2886
+ let remainingBytes = MAX_INLINE_BYTES;
2662
2887
  for (const pathOrUrl of list) {
2663
2888
  try {
2664
- content.push(...await resolveMedia(pathOrUrl, options));
2889
+ const resolved = await resolveMediaWithinBudget(pathOrUrl, options, remainingBytes);
2890
+ remainingBytes -= resolved.bytes;
2891
+ content.push(...resolved.content);
2665
2892
  } catch (err) {
2666
2893
  content.push({
2667
2894
  type: "text",