@haven_ai/sdk 0.0.0-dev.202609190936.6937e31 → 0.0.0-dev.202609191414.43bc74c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -147,6 +147,23 @@ if (apiResponse.status === 402) {
147
147
  }
148
148
  ```
149
149
 
150
+ **MCP merchants, both dialects (#3118).** A merchant may serve x402 over MCP
151
+ in Haven's HTTP layering (HTTP 402 with the `PAYMENT-REQUIRED` header or JSON
152
+ body, payment in `PAYMENT-SIGNATURE` / `X-PAYMENT`, settlement in
153
+ `PAYMENT-RESPONSE`) or in the official x402 MCP transport profile (a tool
154
+ result with `isError: true` and the `PaymentRequired` as `structuredContent`,
155
+ payment in `params._meta["x402/payment"]`, settlement in
156
+ `result._meta["x402/payment-response"]`). `fetch()`, `quoteX402()`,
157
+ `quoteMcpX402()` and `completeX402MerchantCall()` handle both: a payment-
158
+ required tool result quotes like a 402, the paid retry carries the header and
159
+ — for a `tools/call` body — the `_meta` object, and settlement is read from the
160
+ header first, the `_meta` second. An in-band refusal (an `isError` challenge on
161
+ the paid retry, or `success: false`) is a rejection even under HTTP 200. An
162
+ ordinary tool error, or a successful result that merely resembles a challenge,
163
+ is never paid for. The tool-result challenge is read only from a response
164
+ declaring `application/json` or `text/event-stream`; any other content type is
165
+ passed through untouched rather than buffered.
166
+
150
167
  ### Idempotency: what the key guarantees, and what it costs
151
168
 
152
169
  **Omit `idempotencyKey` and the SDK synthesises one** from the merchant's
package/dist/index.cjs CHANGED
@@ -1488,6 +1488,9 @@ var MCP_NOTIFICATION_TIMEOUT = 1e4;
1488
1488
  var MCP_PROTOCOL_VERSION = "2025-06-18";
1489
1489
  var MCP_ACCEPT = "application/json, text/event-stream";
1490
1490
  var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
1491
+ var MCP_X402_PAYMENT_META_KEY = "x402/payment";
1492
+ var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
1493
+ var X402_RETRY_REJECTED_STATUS = 402;
1491
1494
  var McpMerchantTransport = class {
1492
1495
  merchantTimeout;
1493
1496
  fetchImpl;
@@ -1583,7 +1586,7 @@ var McpMerchantTransport = class {
1583
1586
  return response;
1584
1587
  }
1585
1588
  const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
1586
- if (!message) return response;
1589
+ if (!message || !("result" in message || "error" in message)) return response;
1587
1590
  const body = "result" in message ? message.result : message;
1588
1591
  const headers = new Headers(response.headers);
1589
1592
  headers.set("content-type", "application/json");
@@ -1646,7 +1649,34 @@ var McpMerchantTransport = class {
1646
1649
  if (send.includes(name)) headers.set(name, paymentHeader);
1647
1650
  else headers.delete(name);
1648
1651
  }
1649
- return this.fetch(url, { ...init, headers });
1652
+ return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader));
1653
+ }
1654
+ /**
1655
+ * #3118: read a native MCP payment-required tool result from a response
1656
+ * WITHOUT consuming it — JSON or SSE framed. `undefined` for anything that
1657
+ * is not an `isError: true` tool result carrying a valid `PaymentRequired`
1658
+ * (structured content first, text fallback second), so an ordinary tool
1659
+ * error, a successful result that merely resembles a challenge, or an
1660
+ * unparseable body is never mistaken for a payment demand.
1661
+ */
1662
+ async extractToolResultChallenge(response, init) {
1663
+ const toolResult = await this.readToolResult(response, init);
1664
+ return toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
1665
+ }
1666
+ /**
1667
+ * #3118: the tool RESULT a response carries, read WITHOUT consuming it, or
1668
+ * `undefined`. Only a JSON or SSE body is read at all (#3155 review S2):
1669
+ * a non-402 answer used to be returned untouched, and buffering an
1670
+ * arbitrary body — a streaming or binary paid resource — to look for a
1671
+ * challenge would block the caller until the stream ended or the merchant
1672
+ * timeout fired. A merchant that speaks JSON-RPC declares one of those two
1673
+ * content types.
1674
+ */
1675
+ async readToolResult(response, init) {
1676
+ if (init !== void 0 && !isJsonRpcToolsCallBody(init.body)) return void 0;
1677
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
1678
+ if (!contentType.includes("application/json") && !contentType.includes("text/event-stream")) return void 0;
1679
+ return mcpToolResultOf(await this.readMessage(response));
1650
1680
  }
1651
1681
  async notifyInitialized(url, init, sessionId, wallet) {
1652
1682
  try {
@@ -1668,6 +1698,88 @@ var McpMerchantTransport = class {
1668
1698
  }
1669
1699
  }
1670
1700
  };
1701
+ function mcpToolResultOf(value) {
1702
+ if (!isRecord(value)) return void 0;
1703
+ if ("jsonrpc" in value) {
1704
+ return isRecord(value.result) ? value.result : void 0;
1705
+ }
1706
+ if ("isError" in value || Array.isArray(value.content) || isRecord(value._meta)) return value;
1707
+ return void 0;
1708
+ }
1709
+ function extractMcpPaymentRequired(toolResult) {
1710
+ if (toolResult.isError !== true) return void 0;
1711
+ if (isRecord(toolResult.structuredContent)) {
1712
+ const parsed = normalizePaymentRequired(toolResult.structuredContent);
1713
+ if (parsed) return parsed;
1714
+ }
1715
+ if (Array.isArray(toolResult.content)) {
1716
+ for (const item of toolResult.content) {
1717
+ if (!isRecord(item) || item.type !== "text" || typeof item.text !== "string") continue;
1718
+ let candidate;
1719
+ try {
1720
+ candidate = JSON.parse(item.text);
1721
+ } catch {
1722
+ continue;
1723
+ }
1724
+ const parsed = normalizePaymentRequired(candidate);
1725
+ if (parsed) return parsed;
1726
+ }
1727
+ }
1728
+ return void 0;
1729
+ }
1730
+ function mcpSettlementFromToolResult(toolResult) {
1731
+ const meta = toolResult._meta;
1732
+ if (!isRecord(meta)) return void 0;
1733
+ const settlement = meta[MCP_X402_PAYMENT_RESPONSE_META_KEY];
1734
+ if (!isRecord(settlement) || typeof settlement.success !== "boolean") return void 0;
1735
+ return {
1736
+ success: settlement.success,
1737
+ ...typeof settlement.transaction === "string" ? { transaction: settlement.transaction } : {},
1738
+ ...typeof settlement.network === "string" ? { network: settlement.network } : {},
1739
+ ...typeof settlement.payer === "string" ? { payer: settlement.payer } : {},
1740
+ ...typeof settlement.errorReason === "string" ? { errorReason: settlement.errorReason } : {}
1741
+ };
1742
+ }
1743
+ function withMcpPaymentMeta(init, paymentHeader) {
1744
+ const request = parseJsonRpcToolsCall(init.body);
1745
+ if (!request) return init;
1746
+ let envelope;
1747
+ try {
1748
+ envelope = decodeBase64Json(paymentHeader);
1749
+ } catch {
1750
+ return init;
1751
+ }
1752
+ if (!isRecord(envelope)) return init;
1753
+ const params = request.params;
1754
+ const existingMeta = isRecord(params._meta) ? params._meta : {};
1755
+ return {
1756
+ ...init,
1757
+ body: JSON.stringify({
1758
+ ...request,
1759
+ params: { ...params, _meta: { ...existingMeta, [MCP_X402_PAYMENT_META_KEY]: envelope } }
1760
+ })
1761
+ };
1762
+ }
1763
+ function encodeMcpSettlementReceipt(settlement) {
1764
+ return encodeBase64Json(settlement);
1765
+ }
1766
+ function parseJsonRpcToolsCall(body) {
1767
+ if (typeof body !== "string") return void 0;
1768
+ let request;
1769
+ try {
1770
+ request = JSON.parse(body);
1771
+ } catch {
1772
+ return void 0;
1773
+ }
1774
+ if (!isRecord(request) || request.method !== "tools/call" || !isRecord(request.params)) return void 0;
1775
+ return request;
1776
+ }
1777
+ function isJsonRpcToolsCallBody(body) {
1778
+ return parseJsonRpcToolsCall(body) !== void 0;
1779
+ }
1780
+ function isRecord(value) {
1781
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1782
+ }
1671
1783
  async function captureMerchantResponse(response) {
1672
1784
  const merchant_body = await response.text().catch(() => "");
1673
1785
  return {
@@ -2821,7 +2933,11 @@ var MerchantCompletion = class {
2821
2933
  initialInit,
2822
2934
  receipt.paymentHeader
2823
2935
  );
2824
- if (!retryResponse.ok) {
2936
+ const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
2937
+ const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
2938
+ const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
2939
+ const inBandRejection = inBandChallenge !== void 0 || metaSettlement?.success === false;
2940
+ if (!retryResponse.ok || inBandRejection) {
2825
2941
  const merchant = await captureMerchantResponse(retryResponse);
2826
2942
  await this.recordRetryRejected({
2827
2943
  rail: "x402",
@@ -2836,7 +2952,7 @@ var MerchantCompletion = class {
2836
2952
  });
2837
2953
  throw new HavenApiError(
2838
2954
  "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2839
- merchant.merchant_status,
2955
+ inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
2840
2956
  {
2841
2957
  marker: "x402_retry_rejected_after_funding",
2842
2958
  payment_id: receipt.paymentId,
@@ -2848,7 +2964,10 @@ var MerchantCompletion = class {
2848
2964
  }
2849
2965
  );
2850
2966
  }
2851
- const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
2967
+ const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2968
+ const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
2969
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2970
+ const merchantSettlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
2852
2971
  if (receipt.merchant && merchantSettlement.settlementTxHash) {
2853
2972
  receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2854
2973
  receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
@@ -2866,8 +2985,8 @@ var MerchantCompletion = class {
2866
2985
  selectedPayment: receipt.accepted,
2867
2986
  paymentProofHeaderName: x402PaymentHeaderNamesSent(receipt.paymentHeader),
2868
2987
  paymentProofHeader: receipt.paymentHeader,
2869
- protocolReceiptHeaderName: "PAYMENT-RESPONSE",
2870
- protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
2988
+ protocolReceiptHeaderName,
2989
+ protocolReceiptHeader
2871
2990
  });
2872
2991
  await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2873
2992
  return retryResponse;
@@ -3842,6 +3961,11 @@ var HavenClient = class {
3842
3961
  const request = snapshotX402Request(url, initialInit);
3843
3962
  const response = await this.merchantTransport.fetch(url, initialInit);
3844
3963
  if (response.status !== 402) {
3964
+ const toolResultChallenge = await this.merchantTransport.extractToolResultChallenge(response, initialInit);
3965
+ if (toolResultChallenge) {
3966
+ const mcpTransport2 = await this.merchantTransport.detect(url, toolResultChallenge, response);
3967
+ return buildX402Quote(toolResultChallenge, request, options.idempotencyKey, mcpTransport2);
3968
+ }
3845
3969
  let body;
3846
3970
  try {
3847
3971
  body = await response.clone().json();
@@ -4008,10 +4132,11 @@ var HavenClient = class {
4008
4132
  return this.merchantCompletion.retryRequest(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
4009
4133
  }
4010
4134
  /**
4011
- * Fetch wrapper that automatically handles HTTP 402 responses.
4135
+ * Fetch wrapper that automatically handles HTTP 402 responses — and, since
4136
+ * #3118, a native-MCP payment-required tool result answered under HTTP 200.
4012
4137
  *
4013
- * Works like the standard `fetch()` but intercepts 402 responses,
4014
- * pays via x402 through Haven, and retries the request.
4138
+ * Works like the standard `fetch()` but intercepts 402 responses (or that
4139
+ * tool-result challenge), pays via x402 through Haven, and retries the request.
4015
4140
  *
4016
4141
  * ```ts
4017
4142
  * const response = await haven.fetch('https://paid-api.com/data')
@@ -4019,8 +4144,9 @@ var HavenClient = class {
4019
4144
  * ```
4020
4145
  *
4021
4146
  * **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
4022
- * MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
4023
- * Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
4147
+ * MCP-shaped — the URL path ends in `/mcp`, or the 402 body (or, since
4148
+ * #3118, the tool-result challenge) carries a Coinbase Bazaar
4149
+ * `extensions.bazaar` block — the SDK runs the MCP
4024
4150
  * `initialize` handshake, threads the resulting `mcp-session-id`,
4025
4151
  * `Accept: application/json, text/event-stream`, and `x402-wallet` headers
4026
4152
  * through every request, and collapses SSE responses to the JSON-RPC
@@ -4038,16 +4164,23 @@ var HavenClient = class {
4038
4164
  let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
4039
4165
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
4040
4166
  const response = await this.merchantTransport.fetch(url, requestInit);
4041
- if (response.status !== 402) {
4042
- return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
4043
- }
4044
4167
  let paymentRequired;
4045
- try {
4046
- paymentRequired = await parsePaymentRequiredResponse(response);
4047
- } catch {
4048
- return response;
4168
+ let nativeChallenge = false;
4169
+ if (response.status !== 402) {
4170
+ const toolResultChallenge = await this.merchantTransport.extractToolResultChallenge(response, requestInit);
4171
+ if (!toolResultChallenge) {
4172
+ return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
4173
+ }
4174
+ paymentRequired = toolResultChallenge;
4175
+ nativeChallenge = true;
4176
+ } else {
4177
+ try {
4178
+ paymentRequired = await parsePaymentRequiredResponse(response);
4179
+ } catch {
4180
+ return response;
4181
+ }
4049
4182
  }
4050
- if (!mcpSessionId && await this.merchantTransport.hasBazaarExtension(response)) {
4183
+ if (!mcpSessionId && (paymentRequired.extensions?.bazaar != null || await this.merchantTransport.hasBazaarExtension(response))) {
4051
4184
  mcpSessionId = await this.merchantTransport.initialize(url, init);
4052
4185
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
4053
4186
  }
@@ -4069,7 +4202,7 @@ var HavenClient = class {
4069
4202
  throw err;
4070
4203
  }
4071
4204
  const retryResponse = await this.merchantCompletion.retryRequest(url, requestInit, paymentRequired, receipt);
4072
- return mcpSessionId ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
4205
+ return mcpSessionId || nativeChallenge ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
4073
4206
  }
4074
4207
  /**
4075
4208
  * Deliver an already-signed x402 payment header to the merchant and return
@@ -4130,9 +4263,7 @@ var HavenClient = class {
4130
4263
  let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
4131
4264
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
4132
4265
  const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
4133
- const surfaced = mcpSessionId ? await this.merchantTransport.surfaceResult(response) : response;
4134
- const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
4135
- const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
4266
+ const surfaced = await this.merchantTransport.surfaceResult(response);
4136
4267
  const text = await surfaced.text();
4137
4268
  let body;
4138
4269
  try {
@@ -4140,8 +4271,16 @@ var HavenClient = class {
4140
4271
  } catch {
4141
4272
  body = text;
4142
4273
  }
4274
+ const toolResult = mcpToolResultOf(body);
4275
+ const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
4276
+ const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
4277
+ const headerReceipt = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
4278
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
4279
+ const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
4280
+ const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
4281
+ const merchantAccepted = surfaced.ok && inBandChallenge === void 0 && metaSettlement?.success !== false;
4143
4282
  let evidenceOutcome;
4144
- if (!surfaced.ok) {
4283
+ if (!merchantAccepted) {
4145
4284
  if (!input.noFundingLeg && fundingTxHash) {
4146
4285
  await this.merchantCompletion.recordRetryRejected({
4147
4286
  rail: "x402",
@@ -4171,7 +4310,7 @@ var HavenClient = class {
4171
4310
  merchantStatus: surfaced.status,
4172
4311
  paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
4173
4312
  paymentProofHeader: input.paymentHeader,
4174
- protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
4313
+ protocolReceiptHeaderName,
4175
4314
  protocolReceiptHeader
4176
4315
  });
4177
4316
  }
@@ -4179,9 +4318,10 @@ var HavenClient = class {
4179
4318
  }
4180
4319
  return {
4181
4320
  status: surfaced.status,
4182
- ok: surfaced.ok,
4321
+ ok: merchantAccepted,
4183
4322
  body,
4184
- settlementTxHash: settlement.settlementTxHash ?? void 0,
4323
+ // #3155 review S3: a transaction beside a rejection is not a settlement.
4324
+ settlementTxHash: merchantAccepted ? settlement.settlementTxHash ?? void 0 : void 0,
4185
4325
  evidenceOutcome
4186
4326
  };
4187
4327
  }
@@ -4371,12 +4511,12 @@ var toolDescriptions = {
4371
4511
  payX402OneShot: {
4372
4512
  summary: "Fetch an x402 paid HTTP resource in a single call. Handles the full probe -> pay -> retry round trip and returns the merchant response.",
4373
4513
  selectionGuidance: "Prefer this over the quote+pay split when the agent just wants the paid resource and does not need to inspect the price first. If you already have a quote from haven_quote_x402, use haven_pay_x402_quote instead. Do not use for read-only allowance, budget, spend-limit, remaining-amount, reset-period, or what-can-I-spend questions; use the allowance lookup tool instead.",
4374
- behavior: "Calls the URL, parses any HTTP 402 x402 challenge, signs the payment locally, then retries the original request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only) and returns the merchant response. Settlement is either direct account-to-merchant with no funding leg, or a bridge that first redeems the agent's budget delegation to fund the delegate wallet for an EIP-3009 authorization. A payment outside the on-chain budget is declined before any money moves; nothing is queued for a human to approve later. If the resource returns a non-402 status, returns it unchanged without contacting Haven.",
4514
+ behavior: "Calls the URL, parses any HTTP 402 x402 challenge (or, since #3118, a native-MCP payment-required tool result under HTTP 200), signs the payment locally, then retries the original request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only) and returns the merchant response. Settlement is either direct account-to-merchant with no funding leg, or a bridge that first redeems the agent's budget delegation to fund the delegate wallet for an EIP-3009 authorization. A payment outside the on-chain budget is declined before any money moves; nothing is queued for a human to approve later. If the resource returns a non-402 status with no payment-required tool result, returns it unchanged without contacting Haven.",
4375
4515
  nextActionGuidance: "Preserve the returned resume_state or paymentId \u2014 either identifies this payment if you need to ask about it later. This tool performs the merchant retry itself, so do not wait on a signal while the call is in flight. If the process crashes after this call and a later haven_get_payment_status reports nextAction=retry_original_x402_request, Haven's funding leg confirmed but no merchant response was ever recorded \u2014 call the resume tool with the preserved resume_state or payment_id instead of paying again. If the response carries phase=insufficient_funds and nextAction=fund_account_or_raise_allowance, the payment cannot be retried until the account is funded or the agent budget raised \u2014 stop and tell the user the shortfall reported on the response."
4376
4516
  },
4377
4517
  resumeX402: {
4378
4518
  summary: "Resume an x402 payment whose Haven-side authorization already succeeded but whose merchant retry did not complete.",
4379
- behavior: "Accepts either resume_state or payment_id, validates the original x402 details against the authorized Haven funding, and retries the merchant request with the signed payment header (sent under PAYMENT-SIGNATURE, plus the legacy X-PAYMENT on the EIP-3009 path only). No new Haven payment is created.",
4519
+ behavior: "Accepts either resume_state or payment_id, validates the x402 details against the authorized funding, and retries the merchant request with the signed payment header (PAYMENT-SIGNATURE, plus legacy X-PAYMENT on EIP-3009 only; also params._meta on an MCP tools/call body). No new Haven payment is created.",
4380
4520
  nextActionGuidance: "Only call this after haven_get_payment_status reports nextAction=retry_original_x402_request \u2014 that means Haven's funding leg confirmed but no merchant response was ever recorded, most often because the process crashed between funding and the merchant retry. Any other nextAction reports a conflict instead of retrying, so do not call this speculatively. Do not start a new merchant session and do not pay again \u2014 that would pay twice for one resource."
4381
4521
  },
4382
4522
  // #1328: quoteMpp / payMpp / resumeMpp (the mpp_demo challenge/quote/resume