@haven_ai/mcp 0.2.0-alpha.0 → 0.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { composeDescription, toolDescriptions as toolDescriptions$1, HavenClient, isSupportedNodeVersion, unsupportedNodeVersionMessage, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, verifyPaymentReceipt, discoverMerchantMcpUrl, sameUrl } from '@haven_ai/sdk';
4
+ import { composeDescription, toolDescriptions as toolDescriptions$1, HavenClient, isSupportedNodeVersion, unsupportedNodeVersionMessage, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, verifyPaymentReceipt, discoverMerchantMcpUrl, sameUrl } from '@haven_ai/sdk';
5
5
  import { readFile, mkdir, writeFile, stat } from 'fs/promises';
6
6
  import { z } from 'zod/v3';
7
7
  import { createHash } from 'crypto';
@@ -54,7 +54,6 @@ async function loadCredentialsFromFile(path) {
54
54
  delegateKey,
55
55
  agentId: stringField(raw.agent_id ?? raw.agentId),
56
56
  accountAddress,
57
- safeAddress: accountAddress,
58
57
  delegateAddress: stringField(raw.delegate_address ?? raw.delegateAddress),
59
58
  chainId: numberField(raw.chain_id ?? raw.chainId),
60
59
  network: stringField(raw.network),
@@ -84,13 +83,11 @@ async function loadCredentialsFromSplitFiles(identityPath, signerPath) {
84
83
  identity.agent_id ?? identity.agentId,
85
84
  signer.agent_id ?? signer.agentId
86
85
  ),
87
- ...accountAddressTwins(
88
- matchingStringField(
89
- "account_address",
90
- readAccountAddressField(identity),
91
- readAccountAddressField(signer),
92
- { caseInsensitive: true }
93
- )
86
+ accountAddress: matchingStringField(
87
+ "account_address",
88
+ readAccountAddressField(identity),
89
+ readAccountAddressField(signer),
90
+ { caseInsensitive: true }
94
91
  ),
95
92
  delegateAddress: matchingStringField(
96
93
  "delegate_address",
@@ -145,7 +142,7 @@ function loadCredentialsFromEnv() {
145
142
  apiKey,
146
143
  delegateKey,
147
144
  agentId: stringField(process.env.HAVEN_AGENT_ID),
148
- ...accountAddressTwins(readAccountAddressEnv(process.env)),
145
+ accountAddress: readAccountAddressEnv(process.env),
149
146
  chainId: numberField(process.env.HAVEN_CHAIN_ID),
150
147
  network: stringField(process.env.HAVEN_NETWORK),
151
148
  apiUrl: stringField(process.env.HAVEN_API_URL)
@@ -155,10 +152,22 @@ function readAccountAddressField(raw) {
155
152
  return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
156
153
  }
157
154
  function readAccountAddressEnv(env) {
158
- return stringField(env.HAVEN_ACCOUNT_ADDRESS ?? env.HAVEN_WALLET_ADDRESS ?? env.HAVEN_SAFE_ADDRESS);
159
- }
160
- function accountAddressTwins(address) {
161
- return { accountAddress: address, safeAddress: address };
155
+ const current = stringField(env.HAVEN_ACCOUNT_ADDRESS);
156
+ for (const name of ["HAVEN_WALLET_ADDRESS", "HAVEN_SAFE_ADDRESS"]) {
157
+ const retired = stringField(env[name]);
158
+ if (!retired) continue;
159
+ if (!current) {
160
+ throw new Error(
161
+ `${name} is retired (#2906) \u2014 Haven accounts are addressed as accounts, not Safes. Set HAVEN_ACCOUNT_ADDRESS to the same value. Refusing rather than ignoring it, because an unset account address silently skips the sweep-destination check.`
162
+ );
163
+ }
164
+ if (retired.toLowerCase() !== current.toLowerCase()) {
165
+ throw new Error(
166
+ `${name} and HAVEN_ACCOUNT_ADDRESS were both set to different addresses. Remove the retired name, or make them match \u2014 picking one silently would hide the mismatch.`
167
+ );
168
+ }
169
+ }
170
+ return current;
162
171
  }
163
172
  function stringField(value) {
164
173
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -372,6 +381,8 @@ function createToolHandlers(haven) {
372
381
  const attempt = () => haven.fetch(merchantUrl, init, { idempotencyKey });
373
382
  let response = await attempt();
374
383
  if (!response.ok) {
384
+ const notReady = await merchantNotReadyErrorFor(response);
385
+ if (notReady) throw notReady;
375
386
  const discovered = await discoverMerchantMcpUrl(merchantUrl);
376
387
  if (!discovered || sameUrl(discovered, merchantUrl)) {
377
388
  throw discoveryMissError(response, merchantUrl, discovered);
@@ -380,6 +391,8 @@ function createToolHandlers(haven) {
380
391
  merchantUrl = discovered;
381
392
  const retryResponse = await attempt();
382
393
  if (!retryResponse.ok) {
394
+ const notReadyAtDiscovered = await merchantNotReadyErrorFor(retryResponse);
395
+ if (notReadyAtDiscovered) throw notReadyAtDiscovered;
383
396
  throw discoveryMissError(retryResponse, merchantUrl, discovered, inputUrl);
384
397
  }
385
398
  response = retryResponse;
@@ -613,12 +626,50 @@ function parseMaybeJson(text) {
613
626
  return text;
614
627
  }
615
628
  }
629
+ var MerchantNotReadyError = class extends Error {
630
+ code = AgentPaymentFailureCode.MerchantNotReady;
631
+ statusCode = 503;
632
+ nextAction = AgentPaymentNextAction.StopAndTellUser;
633
+ // Genuinely retryable — unlike a rejection, nothing about THIS call was
634
+ // wrong; the merchant's own wallet needs to recover first.
635
+ retryWithNewQuote = true;
636
+ constructor(message) {
637
+ super(message);
638
+ this.name = "MerchantNotReadyError";
639
+ }
640
+ };
641
+ async function merchantNotReadyErrorFor(response) {
642
+ if (response.status !== 503) return null;
643
+ let body;
644
+ try {
645
+ body = await response.clone().json();
646
+ } catch {
647
+ return null;
648
+ }
649
+ if (!body || typeof body !== "object" || body.error !== "merchant_not_ready") {
650
+ return null;
651
+ }
652
+ const { reason_code, settlements_remaining, retry_after_s } = body;
653
+ return new MerchantNotReadyError(
654
+ "The merchant refused this call: it cannot settle a payment right now" + (typeof reason_code === "string" ? ` (reason_code: ${reason_code})` : "") + (typeof settlements_remaining === "number" ? `, settlements_remaining: ${settlements_remaining}` : "") + ". No payment was created." + (typeof retry_after_s === "number" ? ` Retry after approximately ${retry_after_s}s.` : " This is often transient; retry later.")
655
+ );
656
+ }
616
657
  function discoveryMissError(response, merchantUrl, discovered, discoveredFromUrl) {
617
658
  const base = discoveredFromUrl ? `Merchant call to ${merchantUrl} failed with HTTP ${response.status} (at the DISCOVERED endpoint ${merchantUrl}, resolved from ${discoveredFromUrl} via the merchant discovery document).` : `Merchant call to ${merchantUrl} failed with HTTP ${response.status}.`;
618
659
  const guidance = discoveredFromUrl ? "" : discovered ? ` Same-origin discovery resolved the same URL (${discovered}), which still did not answer successfully.` : ` No same-origin discovery document was found at /.well-known/haven-demo-merchant or /. If ${merchantUrl} is a base merchant URL, pass the exact MCP endpoint instead (often <origin>/mcp).`;
619
660
  return new HavenApiError(`${base}${guidance}`, response.status || 400);
620
661
  }
621
662
  function normalizeError(err) {
663
+ if (err instanceof MerchantNotReadyError) {
664
+ return {
665
+ success: false,
666
+ code: err.code,
667
+ message: err.message,
668
+ statusCode: err.statusCode,
669
+ nextAction: err.nextAction,
670
+ retry_with_new_quote: err.retryWithNewQuote
671
+ };
672
+ }
622
673
  if (err instanceof HavenPaymentStateError) {
623
674
  return {
624
675
  success: false,
@@ -679,7 +730,7 @@ function computeConsentHash(input) {
679
730
  input.apiKeyPrefix,
680
731
  input.apiUrl ?? "",
681
732
  input.agentId ?? "",
682
- (input.safeAddress ?? "").toLowerCase(),
733
+ (input.accountAddress ?? "").toLowerCase(),
683
734
  (input.delegateAddress ?? "").toLowerCase(),
684
735
  input.chainId ?? ""
685
736
  ].join("|");
@@ -698,7 +749,7 @@ function renderConsentBlock(input, hash) {
698
749
  ];
699
750
  if (input.apiUrl) lines.push(`Haven API: ${input.apiUrl}`);
700
751
  if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
701
- if (input.safeAddress) lines.push(`Haven wallet (Safe): ${input.safeAddress}`);
752
+ if (input.accountAddress) lines.push(`Haven wallet (Safe): ${input.accountAddress}`);
702
753
  if (input.delegateAddress) lines.push(`Delegate (local signer): ${input.delegateAddress}`);
703
754
  if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
704
755
  lines.push("");
@@ -805,14 +856,14 @@ async function writeAckFile(path, hash) {
805
856
  }
806
857
  async function consentInputFromClient(haven, seed, toolNames) {
807
858
  let allowanceSummary = seed.allowanceSummary ?? [];
808
- let safeAddress = seed.safeAddress;
859
+ let accountAddress = seed.accountAddress;
809
860
  let delegateAddress = seed.delegateAddress;
810
861
  let chainId = seed.chainId;
811
862
  try {
812
863
  const summary = await haven.getAllowances();
813
864
  const list = isAllowanceSummary(summary) ? summary.allowances : Array.isArray(summary) ? summary : [];
814
865
  if (isAllowanceSummary(summary)) {
815
- safeAddress = summary.accountAddress ?? summary.safeAddress ?? safeAddress;
866
+ accountAddress = summary.accountAddress ?? accountAddress;
816
867
  delegateAddress = summary.delegateAddress;
817
868
  chainId = typeof summary.chainId === "number" ? summary.chainId : chainId;
818
869
  }
@@ -828,7 +879,7 @@ async function consentInputFromClient(haven, seed, toolNames) {
828
879
  apiKeyPrefix: derivePrefix(seed.apiKey),
829
880
  apiUrl: seed.apiUrl,
830
881
  agentId: seed.agentId,
831
- safeAddress,
882
+ accountAddress,
832
883
  delegateAddress,
833
884
  chainId,
834
885
  toolNames,
@@ -862,7 +913,7 @@ async function resolveHavenClient(options = {}) {
862
913
  return { client, credentials };
863
914
  }
864
915
  var MCP_NAME = "@haven_ai/mcp";
865
- var MCP_VERSION = "0.2.0-alpha.0";
916
+ var MCP_VERSION = "0.3.0-alpha.0";
866
917
  var MCP_INSTRUCTIONS = [
867
918
  "Haven local MCP server: signs in-process with the delegate key it holds on",
868
919
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",
@@ -935,7 +986,7 @@ async function runConsentGate(haven, credentials, options) {
935
986
  apiKey: credentials.apiKey,
936
987
  apiUrl: credentials.apiUrl,
937
988
  agentId: credentials.agentId,
938
- safeAddress: credentials.accountAddress ?? credentials.safeAddress,
989
+ accountAddress: credentials.accountAddress,
939
990
  delegateAddress: credentials.delegateAddress,
940
991
  chainId: credentials.chainId,
941
992
  allowanceSummary: credentials.allowanceSummary