@adcp/sdk 16.0.0-beta.2 → 16.0.0-beta.3

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 (87) hide show
  1. package/README.md +54 -0
  2. package/dist/lib/core/ConversationTypes.d.mts +8 -3
  3. package/dist/lib/core/ConversationTypes.d.ts +8 -3
  4. package/dist/lib/core/SingleAgentClient.d.mts +5 -3
  5. package/dist/lib/core/SingleAgentClient.d.ts +5 -3
  6. package/dist/lib/core/TaskExecutor.js +5 -1
  7. package/dist/lib/core/TaskExecutor.mjs +5 -1
  8. package/dist/lib/errors/index.d.mts +9 -4
  9. package/dist/lib/errors/index.d.ts +9 -4
  10. package/dist/lib/index.d.mts +2 -2
  11. package/dist/lib/index.d.ts +2 -2
  12. package/dist/lib/index.js +6 -0
  13. package/dist/lib/index.mjs +7 -1
  14. package/dist/lib/negotiation/types.d.mts +20 -39
  15. package/dist/lib/negotiation/types.d.ts +20 -39
  16. package/dist/lib/protocols/index.d.mts +60 -24
  17. package/dist/lib/protocols/index.d.ts +60 -24
  18. package/dist/lib/protocols/index.js +9 -4
  19. package/dist/lib/protocols/index.mjs +3 -0
  20. package/dist/lib/protocols/mcp-modern.js +1 -0
  21. package/dist/lib/protocols/mcp-modern.mjs +2 -1
  22. package/dist/lib/protocols/mcp.js +1 -0
  23. package/dist/lib/protocols/mcp.mjs +2 -1
  24. package/dist/lib/protocols/responseSizeLimit.d.mts +91 -5
  25. package/dist/lib/protocols/responseSizeLimit.d.ts +91 -5
  26. package/dist/lib/protocols/responseSizeLimit.js +195 -31
  27. package/dist/lib/protocols/responseSizeLimit.mjs +192 -31
  28. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  29. package/dist/lib/signing/crypto.d.mts +15 -0
  30. package/dist/lib/signing/crypto.d.ts +15 -0
  31. package/dist/lib/signing/crypto.js +75 -3
  32. package/dist/lib/signing/crypto.mjs +78 -3
  33. package/dist/lib/signing/server.d.mts +1 -1
  34. package/dist/lib/signing/server.d.ts +1 -1
  35. package/dist/lib/signing/server.js +2 -0
  36. package/dist/lib/signing/server.mjs +2 -1
  37. package/dist/lib/signing/signer.js +2 -8
  38. package/dist/lib/signing/signer.mjs +3 -9
  39. package/dist/lib/signing/testing.js +2 -6
  40. package/dist/lib/signing/testing.mjs +3 -7
  41. package/dist/lib/signing/verifier.js +2 -2
  42. package/dist/lib/signing/verifier.mjs +3 -3
  43. package/dist/lib/signing/web-bot-auth.js +20 -7
  44. package/dist/lib/signing/web-bot-auth.mjs +21 -8
  45. package/dist/lib/signing/webhook-verifier.js +2 -2
  46. package/dist/lib/signing/webhook-verifier.mjs +3 -3
  47. package/dist/lib/substitution/compile.js +2 -1
  48. package/dist/lib/substitution/compile.mjs +2 -1
  49. package/dist/lib/substitution/consent-macros.d.mts +4 -0
  50. package/dist/lib/substitution/consent-macros.d.ts +4 -0
  51. package/dist/lib/substitution/consent-macros.js +45 -0
  52. package/dist/lib/substitution/consent-macros.mjs +20 -0
  53. package/dist/lib/substitution/index.d.mts +1 -1
  54. package/dist/lib/substitution/index.d.ts +1 -1
  55. package/dist/lib/substitution/index.js +2 -0
  56. package/dist/lib/substitution/index.mjs +2 -1
  57. package/dist/lib/substitution/translate.d.mts +15 -2
  58. package/dist/lib/substitution/translate.d.ts +15 -2
  59. package/dist/lib/substitution/translate.js +148 -26
  60. package/dist/lib/substitution/translate.mjs +147 -26
  61. package/dist/lib/testing/client.js +2 -1
  62. package/dist/lib/testing/client.mjs +2 -1
  63. package/dist/lib/testing/scenarios/capabilities.js +1 -1
  64. package/dist/lib/testing/scenarios/capabilities.mjs +1 -1
  65. package/dist/lib/testing/storyboard/request-signing/builder.js +2 -5
  66. package/dist/lib/testing/storyboard/request-signing/builder.mjs +3 -6
  67. package/dist/lib/testing/storyboard/signer-grader/grader.js +2 -7
  68. package/dist/lib/testing/storyboard/signer-grader/grader.mjs +2 -7
  69. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  70. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  71. package/dist/lib/types/enums.generated.d.mts +3 -0
  72. package/dist/lib/types/enums.generated.d.ts +3 -0
  73. package/dist/lib/types/enums.generated.js +9 -0
  74. package/dist/lib/types/enums.generated.mjs +6 -0
  75. package/dist/lib/validation/schema-loader.js +25 -0
  76. package/dist/lib/validation/schema-loader.mjs +24 -0
  77. package/dist/lib/version.d.mts +3 -3
  78. package/dist/lib/version.d.ts +3 -3
  79. package/dist/lib/version.js +3 -3
  80. package/dist/lib/version.mjs +3 -3
  81. package/docs/TYPE-SUMMARY.md +6 -1
  82. package/docs/guides/DISCOVERY-CACHE-AND-SESSION-REUSE.md +3 -2
  83. package/docs/guides/UNIVERSAL-MACRO-COMPILATION.md +38 -0
  84. package/docs/guides/VALIDATE-YOUR-AGENT.md +12 -0
  85. package/docs/llms.txt +1 -1
  86. package/docs/migration-14.x-rc-worksheet.md +4 -4
  87. package/package.json +2 -1
@@ -11,6 +11,7 @@ export { withMCPConnectionScope };
11
11
  export type { MCPCallOptions, MCPConnectionResult } from './mcp.mjs';
12
12
  export { callA2ATool, createA2AClientFromCardUrl } from './a2a.mjs';
13
13
  export { DEFAULT_REQUEST_TIMEOUT_MS } from './abort.mjs';
14
+ export { DEFAULT_MAX_RESPONSE_BYTES, SSE_CUMULATIVE_BUDGET_MULTIPLIER } from './responseSizeLimit.mjs';
14
15
  export { callMCPToolWithTasks, getMCPTaskStatus, getMCPTaskResult, listMCPTasks, cancelMCPTask, mapMCPTaskStatus, serverSupportsTasks, } from './mcp-tasks.mjs';
15
16
  import type { AgentConfig, DebugLogEntry } from '../types/index.mjs';
16
17
  import type { PushNotificationConfig } from '../types/tools.generated.mjs';
@@ -36,36 +37,71 @@ export declare function bundleSupportsAdcpVersionField(bundleKey: string): boole
36
37
  export interface TransportOptions {
37
38
  /**
38
39
  * Maximum response body size (in octets) the SDK will read before aborting
39
- * with `ResponseTooLargeError`. When unset, the SDK does not impose a cap —
40
- * matches the underlying MCP / A2A transport defaults.
40
+ * with `ResponseTooLargeError`.
41
41
  *
42
- * Set this when crawling **untrusted** agents (registries, federated
43
- * discovery layers, monitoring tools) to prevent a hostile vendor from
44
- * buffering a large reply before any application-layer schema validation
45
- * runs. Counted across response chunks; pre-cancels when `Content-Length`
46
- * exceeds the cap. Applies to A2A agent-card discovery
47
- * (`/.well-known/agent.json`) on the same call as well.
42
+ * **Defaults to 4 MiB** (`DEFAULT_MAX_RESPONSE_BYTES`). Agent responses are
43
+ * untrusted input, so the cap is on by default on every MCP and A2A call
44
+ * path — tool calls, endpoint discovery, `tools/list`, and A2A agent-card
45
+ * lookup (`/.well-known/agent.json`). Counted across response chunks as
46
+ * they arrive; pre-cancels when `Content-Length` already exceeds the cap.
48
47
  *
49
- * Per-call override (`TaskOptions.transport.maxResponseBytes`) beats the
50
- * value set on the client constructor (`SingleAgentClientConfig.transport`).
48
+ * Raise it for an agent that legitimately publishes a large catalog; lower
49
+ * it when crawling **untrusted** agents (registries, federated discovery
50
+ * layers, monitoring tools). Set it to exactly `0` to opt out of
51
+ * enforcement entirely — an explicit opt-out, never the default. `Infinity`
52
+ * and negative values are *not* opt-outs: they fall back to the 4 MiB
53
+ * default, because they almost always arrive from a misparsed env var or a
54
+ * budget subtraction rather than an intention. Use `0`.
55
+ *
56
+ * Per-call `transport` **replaces** the client constructor's `transport`
57
+ * object rather than merging field by field
58
+ * (`TaskOptions.transport` ?? `SingleAgentClientConfig.transport`), so a
59
+ * per-call object that omits `maxResponseBytes` gets the default back, not
60
+ * the constructor's value. Repeat the field when you override the object.
61
+ *
62
+ * Cached MCP connections are keyed on the cap, so varying it per call opens
63
+ * a separate connection (and MCP session) per distinct value against the
64
+ * same agent. That is deliberate — a connection established under one
65
+ * policy must not serve a call that configured another — but it means
66
+ * raising the cap for one tool is not free. Prefer a single client-level
67
+ * value where you can, or pin the session explicitly with
68
+ * `withMCPConnectionScope` when continuity matters.
51
69
  *
52
70
  * @remarks
53
- * **Safe to set on all calls.** SSE responses (`text/event-stream`) are
54
- * passed through unchanged — a single tool call legitimately emits N status
55
- * frames + a final result, bounded by protocol-level framing rather than
56
- * cumulative byte counts. The cap applies to one-shot JSON responses
57
- * (`get_adcp_capabilities`, agent-card lookup, tool result payloads on
58
- * non-streaming transports) where the body is bounded by definition.
71
+ * **SSE is covered too.** `text/event-stream` responses used to be exempt,
72
+ * which let any peer opt itself out of the cap by changing one response
73
+ * header. They now carry two bounds: no single event frame may exceed
74
+ * `maxResponseBytes` (the memory bound — an SSE consumer buffers one frame
75
+ * at a time), and the cumulative event-stream octets read during one call
76
+ * may not exceed `maxResponseBytes * 2`
77
+ * (`SSE_CUMULATIVE_BUDGET_MULTIPLIER`, the liveness bound that stops an
78
+ * endless drip of individually legal frames). Both failures raise the same
79
+ * `ResponseTooLargeError`; nothing is silently truncated.
80
+ *
81
+ * A long-lived `GET` event-stream — MCP's standalone notification listener,
82
+ * the legacy SSE transport — outlives the call that opened it, so it meters
83
+ * its cumulative allowance per *connection* instead of per call. That makes
84
+ * the cumulative number a memory bound there, not a liveness bound: a peer
85
+ * that keeps tripping it gets a reconnect cycle at a bounded cost per
86
+ * cycle. Cached connections are keyed on the cap in effect, so a listener
87
+ * is only ever reused by calls that configured the same policy as the call
88
+ * that opened it.
59
89
  *
60
90
  * @remarks
61
- * **Hostile-peer note:** A peer can opt itself out of this cap by responding
62
- * with `Content-Type: text/event-stream`. SSE is bypassed because cumulative
63
- * event-frame bytes are unbounded by spec — MCP and A2A both stream tool
64
- * responses this way. The MCP/A2A SDKs consume SSE incrementally and frame
65
- * termination bounds memory in practice, so this is not a memory-bomb risk
66
- * for well-formed transports. Adopters relying on `maxResponseBytes` as a
67
- * hostile-server defense should treat it as best-effort for non-SSE
68
- * responses only.
91
+ * **Compression and streaming failures.** The byte counter runs downstream
92
+ * of the runtime's content-decoder, so it measures decompressed octets: a
93
+ * small gzip blob that inflates to gigabytes still trips the cap after
94
+ * `maxResponseBytes` of output. The `Content-Length` pre-check, by
95
+ * contrast, sees the compressed length and is therefore only a fast path,
96
+ * not the guarantee.
97
+ *
98
+ * On a streaming transport the cap errors the response body. Some
99
+ * transports classify that as a stream disconnect and retry rather than
100
+ * propagating it, so the call would otherwise run out its deadline: the SDK
101
+ * records the violation and, if the call then fails, raises
102
+ * `ResponseTooLargeError` with the transport's own error as `cause` instead
103
+ * of the timeout. A buyer never gets a timeout for what is really an
104
+ * over-cap response.
69
105
  *
70
106
  * @remarks
71
107
  * Future hardening knobs (DNS-rebind defense, scheme allow-list, request
@@ -11,6 +11,7 @@ export { withMCPConnectionScope };
11
11
  export type { MCPCallOptions, MCPConnectionResult } from './mcp';
12
12
  export { callA2ATool, createA2AClientFromCardUrl } from './a2a';
13
13
  export { DEFAULT_REQUEST_TIMEOUT_MS } from './abort';
14
+ export { DEFAULT_MAX_RESPONSE_BYTES, SSE_CUMULATIVE_BUDGET_MULTIPLIER } from './responseSizeLimit';
14
15
  export { callMCPToolWithTasks, getMCPTaskStatus, getMCPTaskResult, listMCPTasks, cancelMCPTask, mapMCPTaskStatus, serverSupportsTasks, } from './mcp-tasks';
15
16
  import type { AgentConfig, DebugLogEntry } from '../types';
16
17
  import type { PushNotificationConfig } from '../types/tools.generated';
@@ -36,36 +37,71 @@ export declare function bundleSupportsAdcpVersionField(bundleKey: string): boole
36
37
  export interface TransportOptions {
37
38
  /**
38
39
  * Maximum response body size (in octets) the SDK will read before aborting
39
- * with `ResponseTooLargeError`. When unset, the SDK does not impose a cap —
40
- * matches the underlying MCP / A2A transport defaults.
40
+ * with `ResponseTooLargeError`.
41
41
  *
42
- * Set this when crawling **untrusted** agents (registries, federated
43
- * discovery layers, monitoring tools) to prevent a hostile vendor from
44
- * buffering a large reply before any application-layer schema validation
45
- * runs. Counted across response chunks; pre-cancels when `Content-Length`
46
- * exceeds the cap. Applies to A2A agent-card discovery
47
- * (`/.well-known/agent.json`) on the same call as well.
42
+ * **Defaults to 4 MiB** (`DEFAULT_MAX_RESPONSE_BYTES`). Agent responses are
43
+ * untrusted input, so the cap is on by default on every MCP and A2A call
44
+ * path — tool calls, endpoint discovery, `tools/list`, and A2A agent-card
45
+ * lookup (`/.well-known/agent.json`). Counted across response chunks as
46
+ * they arrive; pre-cancels when `Content-Length` already exceeds the cap.
48
47
  *
49
- * Per-call override (`TaskOptions.transport.maxResponseBytes`) beats the
50
- * value set on the client constructor (`SingleAgentClientConfig.transport`).
48
+ * Raise it for an agent that legitimately publishes a large catalog; lower
49
+ * it when crawling **untrusted** agents (registries, federated discovery
50
+ * layers, monitoring tools). Set it to exactly `0` to opt out of
51
+ * enforcement entirely — an explicit opt-out, never the default. `Infinity`
52
+ * and negative values are *not* opt-outs: they fall back to the 4 MiB
53
+ * default, because they almost always arrive from a misparsed env var or a
54
+ * budget subtraction rather than an intention. Use `0`.
55
+ *
56
+ * Per-call `transport` **replaces** the client constructor's `transport`
57
+ * object rather than merging field by field
58
+ * (`TaskOptions.transport` ?? `SingleAgentClientConfig.transport`), so a
59
+ * per-call object that omits `maxResponseBytes` gets the default back, not
60
+ * the constructor's value. Repeat the field when you override the object.
61
+ *
62
+ * Cached MCP connections are keyed on the cap, so varying it per call opens
63
+ * a separate connection (and MCP session) per distinct value against the
64
+ * same agent. That is deliberate — a connection established under one
65
+ * policy must not serve a call that configured another — but it means
66
+ * raising the cap for one tool is not free. Prefer a single client-level
67
+ * value where you can, or pin the session explicitly with
68
+ * `withMCPConnectionScope` when continuity matters.
51
69
  *
52
70
  * @remarks
53
- * **Safe to set on all calls.** SSE responses (`text/event-stream`) are
54
- * passed through unchanged — a single tool call legitimately emits N status
55
- * frames + a final result, bounded by protocol-level framing rather than
56
- * cumulative byte counts. The cap applies to one-shot JSON responses
57
- * (`get_adcp_capabilities`, agent-card lookup, tool result payloads on
58
- * non-streaming transports) where the body is bounded by definition.
71
+ * **SSE is covered too.** `text/event-stream` responses used to be exempt,
72
+ * which let any peer opt itself out of the cap by changing one response
73
+ * header. They now carry two bounds: no single event frame may exceed
74
+ * `maxResponseBytes` (the memory bound — an SSE consumer buffers one frame
75
+ * at a time), and the cumulative event-stream octets read during one call
76
+ * may not exceed `maxResponseBytes * 2`
77
+ * (`SSE_CUMULATIVE_BUDGET_MULTIPLIER`, the liveness bound that stops an
78
+ * endless drip of individually legal frames). Both failures raise the same
79
+ * `ResponseTooLargeError`; nothing is silently truncated.
80
+ *
81
+ * A long-lived `GET` event-stream — MCP's standalone notification listener,
82
+ * the legacy SSE transport — outlives the call that opened it, so it meters
83
+ * its cumulative allowance per *connection* instead of per call. That makes
84
+ * the cumulative number a memory bound there, not a liveness bound: a peer
85
+ * that keeps tripping it gets a reconnect cycle at a bounded cost per
86
+ * cycle. Cached connections are keyed on the cap in effect, so a listener
87
+ * is only ever reused by calls that configured the same policy as the call
88
+ * that opened it.
59
89
  *
60
90
  * @remarks
61
- * **Hostile-peer note:** A peer can opt itself out of this cap by responding
62
- * with `Content-Type: text/event-stream`. SSE is bypassed because cumulative
63
- * event-frame bytes are unbounded by spec — MCP and A2A both stream tool
64
- * responses this way. The MCP/A2A SDKs consume SSE incrementally and frame
65
- * termination bounds memory in practice, so this is not a memory-bomb risk
66
- * for well-formed transports. Adopters relying on `maxResponseBytes` as a
67
- * hostile-server defense should treat it as best-effort for non-SSE
68
- * responses only.
91
+ * **Compression and streaming failures.** The byte counter runs downstream
92
+ * of the runtime's content-decoder, so it measures decompressed octets: a
93
+ * small gzip blob that inflates to gigabytes still trips the cap after
94
+ * `maxResponseBytes` of output. The `Content-Length` pre-check, by
95
+ * contrast, sees the compressed length and is therefore only a fast path,
96
+ * not the guarantee.
97
+ *
98
+ * On a streaming transport the cap errors the response body. Some
99
+ * transports classify that as a stream disconnect and retry rather than
100
+ * propagating it, so the call would otherwise run out its deadline: the SDK
101
+ * records the violation and, if the call then fails, raises
102
+ * `ResponseTooLargeError` with the transport's own error as `cause` instead
103
+ * of the timeout. A buyer never gets a timeout for what is really an
104
+ * over-cap response.
69
105
  *
70
106
  * @remarks
71
107
  * Future hardening knobs (DNS-rebind defense, scheme allow-list, request
@@ -19,9 +19,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var protocols_exports = {};
20
20
  __export(protocols_exports, {
21
21
  BODY_SNIPPET_TIMEOUT_MS: () => import_transportDiagnostics2.BODY_SNIPPET_TIMEOUT_MS,
22
+ DEFAULT_MAX_RESPONSE_BYTES: () => import_responseSizeLimit.DEFAULT_MAX_RESPONSE_BYTES,
22
23
  DEFAULT_REQUEST_TIMEOUT_MS: () => import_abort.DEFAULT_REQUEST_TIMEOUT_MS,
23
24
  OBSERVER_FLUSH_TIMEOUT_MS: () => import_transportDiagnostics2.OBSERVER_FLUSH_TIMEOUT_MS,
24
25
  ProtocolClient: () => ProtocolClient,
26
+ SSE_CUMULATIVE_BUDGET_MULTIPLIER: () => import_responseSizeLimit.SSE_CUMULATIVE_BUDGET_MULTIPLIER,
25
27
  UnauthorizedError: () => import_mcp.UnauthorizedError,
26
28
  applyVersionEnvelope: () => applyVersionEnvelope,
27
29
  bundleSupportsAdcpVersionField: () => bundleSupportsAdcpVersionField,
@@ -59,6 +61,7 @@ var import_a2a = require('./a2a.js');
59
61
  var import_node_crypto = require("node:crypto");
60
62
  var import_a2a2 = require('./a2a.js');
61
63
  var import_abort = require('./abort.js');
64
+ var import_responseSizeLimit = require('./responseSizeLimit.js');
62
65
  var import_mcp_tasks = require('./mcp-tasks.js');
63
66
  var import_mcp_tasks2 = require('./mcp-tasks.js');
64
67
  var import_mcp3 = require('./mcp.js');
@@ -76,7 +79,7 @@ var import_version = require('../version.js');
76
79
  var import_errors2 = require('../errors/index.js');
77
80
  var import_schema_loader = require('../validation/schema-loader.js');
78
81
  var import_client = require('../signing/client.js');
79
- var import_responseSizeLimit = require('./responseSizeLimit.js');
82
+ var import_responseSizeLimit2 = require('./responseSizeLimit.js');
80
83
  var import_prepared_call_context = require('./prepared-call-context.js');
81
84
  var import_adcp_version_config = require('../utils/adcp-version-config.js');
82
85
  var import_transportDiagnostics = require('./transportDiagnostics.js');
@@ -259,7 +262,7 @@ class ProtocolClient {
259
262
  wireAdcpVersion,
260
263
  versionEnvelope: versionEnvelopeMode
261
264
  });
262
- return (0, import_responseSizeLimit.withResponseSizeLimit)(
265
+ return (0, import_responseSizeLimit2.withResponseSizeLimit)(
263
266
  transport?.maxResponseBytes,
264
267
  () => (0, import_transportDiagnostics.withTransportDiagnostics)(
265
268
  {
@@ -523,7 +526,7 @@ const createMCPClient = (agentUrl, authToken, headers, serverVersion, adcpVersio
523
526
  transport = normalizeTransportOptions(transport);
524
527
  const versionEnvelope = buildVersionEnvelopeForMode(versionEnvelopeMode, adcpVersion, serverVersion);
525
528
  return {
526
- callTool: (toolName, args, debugLogs) => (0, import_responseSizeLimit.withResponseSizeLimit)(
529
+ callTool: (toolName, args, debugLogs) => (0, import_responseSizeLimit2.withResponseSizeLimit)(
527
530
  transport?.maxResponseBytes,
528
531
  () => (0, import_mcp_tasks2.callMCPToolWithTasks)(
529
532
  agentUrl,
@@ -545,7 +548,7 @@ const createA2AClient = (agentUrl, authToken, headers, serverVersion, adcpVersio
545
548
  transport = normalizeTransportOptions(transport);
546
549
  const versionEnvelope = buildVersionEnvelopeForMode(versionEnvelopeMode, adcpVersion, serverVersion);
547
550
  return {
548
- callTool: (toolName, parameters, debugLogs) => (0, import_responseSizeLimit.withResponseSizeLimit)(
551
+ callTool: (toolName, parameters, debugLogs) => (0, import_responseSizeLimit2.withResponseSizeLimit)(
549
552
  transport?.maxResponseBytes,
550
553
  () => (0, import_a2a3.callA2ATool)(
551
554
  agentUrl,
@@ -569,9 +572,11 @@ const createA2AClient = (agentUrl, authToken, headers, serverVersion, adcpVersio
569
572
  // Annotate the CommonJS export names for ESM import in node:
570
573
  0 && (module.exports = {
571
574
  BODY_SNIPPET_TIMEOUT_MS,
575
+ DEFAULT_MAX_RESPONSE_BYTES,
572
576
  DEFAULT_REQUEST_TIMEOUT_MS,
573
577
  OBSERVER_FLUSH_TIMEOUT_MS,
574
578
  ProtocolClient,
579
+ SSE_CUMULATIVE_BUDGET_MULTIPLIER,
575
580
  UnauthorizedError,
576
581
  applyVersionEnvelope,
577
582
  bundleSupportsAdcpVersionField,
@@ -31,6 +31,7 @@ async function closeScopedConnections(protocol = "mcp") {
31
31
  }
32
32
  import { callA2ATool, createA2AClientFromCardUrl } from "./a2a.mjs";
33
33
  import { DEFAULT_REQUEST_TIMEOUT_MS } from "./abort.mjs";
34
+ import { DEFAULT_MAX_RESPONSE_BYTES, SSE_CUMULATIVE_BUDGET_MULTIPLIER } from "./responseSizeLimit.mjs";
34
35
  import {
35
36
  callMCPToolWithTasks,
36
37
  getMCPTaskStatus,
@@ -548,9 +549,11 @@ const createA2AClient = (agentUrl, authToken, headers, serverVersion, adcpVersio
548
549
  };
549
550
  export {
550
551
  BODY_SNIPPET_TIMEOUT_MS,
552
+ DEFAULT_MAX_RESPONSE_BYTES,
551
553
  DEFAULT_REQUEST_TIMEOUT_MS,
552
554
  OBSERVER_FLUSH_TIMEOUT_MS,
553
555
  ProtocolClient,
556
+ SSE_CUMULATIVE_BUDGET_MULTIPLIER,
554
557
  UnauthorizedError,
555
558
  applyVersionEnvelope,
556
559
  bundleSupportsAdcpVersionField,
@@ -102,6 +102,7 @@ function connectionCacheKey(agentUrl, headers, signingCacheKey, authProvider, fe
102
102
  if (fetchKey) parts.push(fetchKey);
103
103
  if (handleLegacy !== void 0) parts.push(`handle-legacy:${handleLegacy}`);
104
104
  if (allowPrivateIp !== void 0) parts.push(`allow-private-ip:${allowPrivateIp}`);
105
+ parts.push((0, import_responseSizeLimit.responseSizeCapCacheKey)());
105
106
  const scopeKey = (0, import_mcp_scope.currentMCPConnectionScopeKey)();
106
107
  if (scopeKey) parts.push(scopeKey);
107
108
  return parts.join("::");
@@ -43,7 +43,7 @@ import {
43
43
  } from "./abort.mjs";
44
44
  import { wrapFetchWithCapture } from "./rawResponseCapture.mjs";
45
45
  import { wrapFetchWithSignedRequestRejection } from "./signedRequestRejection.mjs";
46
- import { wrapFetchWithSizeLimit } from "./responseSizeLimit.mjs";
46
+ import { responseSizeCapCacheKey, wrapFetchWithSizeLimit } from "./responseSizeLimit.mjs";
47
47
  import { wrapFetchWithTransportDiagnostics } from "./transportDiagnostics.mjs";
48
48
  import { createAgentTransportFetch } from "../net/agent-transport-fetch.mjs";
49
49
  const modernConnections = /* @__PURE__ */ new Map();
@@ -109,6 +109,7 @@ function connectionCacheKey(agentUrl, headers, signingCacheKey, authProvider, fe
109
109
  if (fetchKey) parts.push(fetchKey);
110
110
  if (handleLegacy !== void 0) parts.push(`handle-legacy:${handleLegacy}`);
111
111
  if (allowPrivateIp !== void 0) parts.push(`allow-private-ip:${allowPrivateIp}`);
112
+ parts.push(responseSizeCapCacheKey());
112
113
  const scopeKey = currentMCPConnectionScopeKey();
113
114
  if (scopeKey) parts.push(scopeKey);
114
115
  return parts.join("::");
@@ -89,6 +89,7 @@ function connectionCacheKey(agentUrl, authToken, signingCacheKey, authHeaders, t
89
89
  if (signingCacheKey) parts.push(signingCacheKey);
90
90
  if (transportFetch) parts.push(`fetch:${getTransportFetchFnDisambiguator(transportFetch)}`);
91
91
  if (allowPrivateIp !== void 0) parts.push(`allow-private-ip:${allowPrivateIp}`);
92
+ parts.push((0, import_responseSizeLimit.responseSizeCapCacheKey)());
92
93
  const scopeKey = (0, import_mcp_scope.currentMCPConnectionScopeKey)();
93
94
  if (scopeKey) parts.push(scopeKey);
94
95
  return parts.join("::");
@@ -19,7 +19,7 @@ import { buildAgentSigningFetch, signingContextStorage } from "../signing/client
19
19
  import { redactArgsForLog } from "../utils/redact-args.mjs";
20
20
  import { wrapFetchWithCapture } from "./rawResponseCapture.mjs";
21
21
  import { getSignedRequestRejection, wrapFetchWithSignedRequestRejection } from "./signedRequestRejection.mjs";
22
- import { wrapFetchWithSizeLimit } from "./responseSizeLimit.mjs";
22
+ import { responseSizeCapCacheKey, wrapFetchWithSizeLimit } from "./responseSizeLimit.mjs";
23
23
  import { sanitizeTransportUrl, wrapFetchWithTransportDiagnostics } from "./transportDiagnostics.mjs";
24
24
  import {
25
25
  isAbortOrTimeoutError,
@@ -88,6 +88,7 @@ function connectionCacheKey(agentUrl, authToken, signingCacheKey, authHeaders, t
88
88
  if (signingCacheKey) parts.push(signingCacheKey);
89
89
  if (transportFetch) parts.push(`fetch:${getTransportFetchFnDisambiguator(transportFetch)}`);
90
90
  if (allowPrivateIp !== void 0) parts.push(`allow-private-ip:${allowPrivateIp}`);
91
+ parts.push(responseSizeCapCacheKey());
91
92
  const scopeKey = currentMCPConnectionScopeKey();
92
93
  if (scopeKey) parts.push(scopeKey);
93
94
  return parts.join("::");
@@ -1,23 +1,109 @@
1
+ import { ResponseTooLargeError } from '../errors/index.mjs';
2
+ /**
3
+ * Default response-body cap, in octets. Applied to every agent response on
4
+ * the MCP and A2A call paths (tool calls, discovery, `tools/list`, agent
5
+ * card) unless the caller overrides it.
6
+ *
7
+ * 4 MiB matches the hosted compliance grader's cap and the SDK's own probe
8
+ * caps. Raise it per client or per call with `transport.maxResponseBytes`
9
+ * when an agent legitimately publishes a large catalog.
10
+ */
11
+ export declare const DEFAULT_MAX_RESPONSE_BYTES: number;
12
+ /**
13
+ * Multiplier applied to the per-response cap to derive the cumulative
14
+ * byte budget for `text/event-stream` responses in a single call.
15
+ *
16
+ * SSE needs two numbers, not one. The per-event cap is the memory bound —
17
+ * a consumer buffers one event frame before dispatching it, so no single
18
+ * event may exceed `maxResponseBytes`. The cumulative budget is the
19
+ * liveness bound — it stops a peer from holding a connection open and
20
+ * streaming forever in well-formed small frames.
21
+ *
22
+ * Deliberately small. Every multiple is a multiple a hostile peer can claim
23
+ * for free by putting `Content-Type: text/event-stream` on an ordinary reply
24
+ * and inserting a blank line every kilobyte, so the gap between the headline
25
+ * cap and the worst case an adopter can actually suffer should stay narrow.
26
+ * `2` leaves room for a normal tool call (many small status frames plus one
27
+ * result frame) without turning the advertised 4 MiB into 32 MiB.
28
+ */
29
+ export declare const SSE_CUMULATIVE_BUDGET_MULTIPLIER = 2;
30
+ /**
31
+ * Cumulative `text/event-stream` octets consumed so far. Shared by reference
32
+ * across nested `withResponseSizeLimit` scopes so a call spends one
33
+ * allowance, not one per SDK layer it passes through.
34
+ */
35
+ interface StreamMeter {
36
+ consumed: number;
37
+ }
1
38
  interface SizeLimitSlot {
2
- maxResponseBytes: number;
39
+ /**
40
+ * Per-response cap in octets, or `null` when the caller explicitly opted
41
+ * out of enforcement for this scope.
42
+ */
43
+ maxResponseBytes: number | null;
44
+ /**
45
+ * Cumulative SSE allowance for this scope, in octets. Absent when
46
+ * enforcement is disabled.
47
+ */
48
+ streamLimit?: number;
49
+ /**
50
+ * Shared consumption counter for `text/event-stream` responses. Inherited
51
+ * from the enclosing scope so nesting narrows the allowance but never
52
+ * refills it. Absent when enforcement is disabled.
53
+ */
54
+ streamMeter?: StreamMeter;
55
+ /**
56
+ * First cap violation observed in this scope. A `TransformStream` error
57
+ * reaches whoever reads the body, and some transports swallow it: the MCP
58
+ * SDK treats a failed SSE read as a stream disconnect, retries, and lets
59
+ * the request time out. Recording the typed error here lets
60
+ * `withResponseSizeLimit` re-surface it instead of handing the caller an
61
+ * unrelated timeout. See `rethrowRecordedViolation`.
62
+ */
63
+ violation?: ResponseTooLargeError;
3
64
  }
4
65
  export declare const responseSizeLimitStorage: import("async_hooks").AsyncLocalStorage<SizeLimitSlot>;
5
66
  /**
6
67
  * Run `fn` with a response body byte cap active. Every fetch made through
7
68
  * a wrapped transport inside `fn` aborts with `ResponseTooLargeError` when
8
- * the response body exceeds `maxResponseBytes`. Non-positive caps disable
9
- * enforcement (ALS slot is not entered).
69
+ * the response body exceeds the cap.
70
+ *
71
+ * Passing `undefined` installs `DEFAULT_MAX_RESPONSE_BYTES` — unless an
72
+ * enclosing scope already set one, in which case the enclosing cap and its
73
+ * cumulative SSE allowance are inherited. More generally, **nesting only ever
74
+ * tightens**: both numbers take the minimum of the enclosing scope's and the
75
+ * new one's, and the cumulative SSE meter is shared rather than refilled.
76
+ *
77
+ * Nested SDK layers re-enter this helper on the way down (`callTool` →
78
+ * `callMCPToolWithTasks`), normally with the same value. Narrow-only means an
79
+ * inner layer that wasn't handed the caller's `transport` can't widen a tight
80
+ * outer cap back to the default, and re-entering with the same number can't
81
+ * multiply the cumulative allowance by the nesting depth. Per-call vs
82
+ * constructor `transport` is resolved with `??` before any scope is entered,
83
+ * so a per-call override still beats the constructor exactly as documented.
84
+ *
85
+ * Passing exactly `0` is the explicit opt-out: the slot is entered with
86
+ * enforcement disabled so an enclosing scope's cap (and the default the fetch
87
+ * wrapper would otherwise apply) is suppressed for the duration. `Infinity`
88
+ * and negative numbers are *not* opt-outs — they fall back to the default.
10
89
  */
11
90
  export declare function withResponseSizeLimit<T>(maxResponseBytes: number | undefined, fn: () => Promise<T>): Promise<T>;
12
91
  /**
13
- * Wrap a fetch implementation so its responses honor the size cap from the
14
- * active `responseSizeLimitStorage` slot. Pass-through when no slot is set.
92
+ * Wrap a fetch implementation so its responses honor the response size cap.
93
+ *
94
+ * Uses the active `responseSizeLimitStorage` slot when there is one, and
95
+ * `DEFAULT_MAX_RESPONSE_BYTES` when there isn't — the cap is on by default,
96
+ * not conditional on a caller having opened a scope. Only an explicit
97
+ * opt-out slot (`withResponseSizeLimit(0, …)`) disables it.
15
98
  *
16
99
  * Pre-checks `Content-Length` and refuses to read the body when the declared
17
100
  * size already exceeds the cap. Otherwise wraps `Response.body` in a
18
101
  * `TransformStream` that counts bytes and errors with `ResponseTooLargeError`
19
102
  * at the cap boundary, propagating to anyone reading the response (including
20
103
  * `Response.clone()` branches used by the diagnostic capture wrapper).
104
+ *
105
+ * `text/event-stream` responses are enforced too, with a per-event cap and a
106
+ * cumulative per-call budget — see `enforceEventStreamLimit`.
21
107
  */
22
108
  export declare function wrapFetchWithSizeLimit(upstream: typeof fetch): typeof fetch;
23
109
  export {};
@@ -1,23 +1,109 @@
1
+ import { ResponseTooLargeError } from '../errors';
2
+ /**
3
+ * Default response-body cap, in octets. Applied to every agent response on
4
+ * the MCP and A2A call paths (tool calls, discovery, `tools/list`, agent
5
+ * card) unless the caller overrides it.
6
+ *
7
+ * 4 MiB matches the hosted compliance grader's cap and the SDK's own probe
8
+ * caps. Raise it per client or per call with `transport.maxResponseBytes`
9
+ * when an agent legitimately publishes a large catalog.
10
+ */
11
+ export declare const DEFAULT_MAX_RESPONSE_BYTES: number;
12
+ /**
13
+ * Multiplier applied to the per-response cap to derive the cumulative
14
+ * byte budget for `text/event-stream` responses in a single call.
15
+ *
16
+ * SSE needs two numbers, not one. The per-event cap is the memory bound —
17
+ * a consumer buffers one event frame before dispatching it, so no single
18
+ * event may exceed `maxResponseBytes`. The cumulative budget is the
19
+ * liveness bound — it stops a peer from holding a connection open and
20
+ * streaming forever in well-formed small frames.
21
+ *
22
+ * Deliberately small. Every multiple is a multiple a hostile peer can claim
23
+ * for free by putting `Content-Type: text/event-stream` on an ordinary reply
24
+ * and inserting a blank line every kilobyte, so the gap between the headline
25
+ * cap and the worst case an adopter can actually suffer should stay narrow.
26
+ * `2` leaves room for a normal tool call (many small status frames plus one
27
+ * result frame) without turning the advertised 4 MiB into 32 MiB.
28
+ */
29
+ export declare const SSE_CUMULATIVE_BUDGET_MULTIPLIER = 2;
30
+ /**
31
+ * Cumulative `text/event-stream` octets consumed so far. Shared by reference
32
+ * across nested `withResponseSizeLimit` scopes so a call spends one
33
+ * allowance, not one per SDK layer it passes through.
34
+ */
35
+ interface StreamMeter {
36
+ consumed: number;
37
+ }
1
38
  interface SizeLimitSlot {
2
- maxResponseBytes: number;
39
+ /**
40
+ * Per-response cap in octets, or `null` when the caller explicitly opted
41
+ * out of enforcement for this scope.
42
+ */
43
+ maxResponseBytes: number | null;
44
+ /**
45
+ * Cumulative SSE allowance for this scope, in octets. Absent when
46
+ * enforcement is disabled.
47
+ */
48
+ streamLimit?: number;
49
+ /**
50
+ * Shared consumption counter for `text/event-stream` responses. Inherited
51
+ * from the enclosing scope so nesting narrows the allowance but never
52
+ * refills it. Absent when enforcement is disabled.
53
+ */
54
+ streamMeter?: StreamMeter;
55
+ /**
56
+ * First cap violation observed in this scope. A `TransformStream` error
57
+ * reaches whoever reads the body, and some transports swallow it: the MCP
58
+ * SDK treats a failed SSE read as a stream disconnect, retries, and lets
59
+ * the request time out. Recording the typed error here lets
60
+ * `withResponseSizeLimit` re-surface it instead of handing the caller an
61
+ * unrelated timeout. See `rethrowRecordedViolation`.
62
+ */
63
+ violation?: ResponseTooLargeError;
3
64
  }
4
65
  export declare const responseSizeLimitStorage: import("async_hooks").AsyncLocalStorage<SizeLimitSlot>;
5
66
  /**
6
67
  * Run `fn` with a response body byte cap active. Every fetch made through
7
68
  * a wrapped transport inside `fn` aborts with `ResponseTooLargeError` when
8
- * the response body exceeds `maxResponseBytes`. Non-positive caps disable
9
- * enforcement (ALS slot is not entered).
69
+ * the response body exceeds the cap.
70
+ *
71
+ * Passing `undefined` installs `DEFAULT_MAX_RESPONSE_BYTES` — unless an
72
+ * enclosing scope already set one, in which case the enclosing cap and its
73
+ * cumulative SSE allowance are inherited. More generally, **nesting only ever
74
+ * tightens**: both numbers take the minimum of the enclosing scope's and the
75
+ * new one's, and the cumulative SSE meter is shared rather than refilled.
76
+ *
77
+ * Nested SDK layers re-enter this helper on the way down (`callTool` →
78
+ * `callMCPToolWithTasks`), normally with the same value. Narrow-only means an
79
+ * inner layer that wasn't handed the caller's `transport` can't widen a tight
80
+ * outer cap back to the default, and re-entering with the same number can't
81
+ * multiply the cumulative allowance by the nesting depth. Per-call vs
82
+ * constructor `transport` is resolved with `??` before any scope is entered,
83
+ * so a per-call override still beats the constructor exactly as documented.
84
+ *
85
+ * Passing exactly `0` is the explicit opt-out: the slot is entered with
86
+ * enforcement disabled so an enclosing scope's cap (and the default the fetch
87
+ * wrapper would otherwise apply) is suppressed for the duration. `Infinity`
88
+ * and negative numbers are *not* opt-outs — they fall back to the default.
10
89
  */
11
90
  export declare function withResponseSizeLimit<T>(maxResponseBytes: number | undefined, fn: () => Promise<T>): Promise<T>;
12
91
  /**
13
- * Wrap a fetch implementation so its responses honor the size cap from the
14
- * active `responseSizeLimitStorage` slot. Pass-through when no slot is set.
92
+ * Wrap a fetch implementation so its responses honor the response size cap.
93
+ *
94
+ * Uses the active `responseSizeLimitStorage` slot when there is one, and
95
+ * `DEFAULT_MAX_RESPONSE_BYTES` when there isn't — the cap is on by default,
96
+ * not conditional on a caller having opened a scope. Only an explicit
97
+ * opt-out slot (`withResponseSizeLimit(0, …)`) disables it.
15
98
  *
16
99
  * Pre-checks `Content-Length` and refuses to read the body when the declared
17
100
  * size already exceeds the cap. Otherwise wraps `Response.body` in a
18
101
  * `TransformStream` that counts bytes and errors with `ResponseTooLargeError`
19
102
  * at the cap boundary, propagating to anyone reading the response (including
20
103
  * `Response.clone()` branches used by the diagnostic capture wrapper).
104
+ *
105
+ * `text/event-stream` responses are enforced too, with a per-event cap and a
106
+ * cumulative per-call budget — see `enforceEventStreamLimit`.
21
107
  */
22
108
  export declare function wrapFetchWithSizeLimit(upstream: typeof fetch): typeof fetch;
23
109
  export {};