@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/README.md CHANGED
@@ -44,9 +44,12 @@ Create a private JSON file from the values in the Haven agent handoff:
44
44
  `delegate_key` is required. Without it the MCP server cannot sign locally.
45
45
  `account_address` is the name the connector writes since #2908; a file that
46
46
  still says `safe_address` (or `safeAddress`) is read the same way, permanently.
47
- The same holds for the environment: `HAVEN_ACCOUNT_ADDRESS` is read first, and
48
- the older `HAVEN_WALLET_ADDRESS` / `HAVEN_SAFE_ADDRESS` are accepted for one
49
- release (#2914 removes them).
47
+ The environment is **not** the same: only `HAVEN_ACCOUNT_ADDRESS` is read.
48
+ `HAVEN_WALLET_ADDRESS` and `HAVEN_SAFE_ADDRESS` were accepted for one release
49
+ and #2914 removed them, so a machine still configured through either resolves
50
+ no account address at all. The credential-FILE fallback above is permanent —
51
+ a file on disk never rewrites itself — and the environment is not, which is
52
+ the whole difference between the two paragraphs.
50
53
 
51
54
  The Haven connector may also write split credentials:
52
55
 
package/dist/cli.cjs CHANGED
@@ -56,7 +56,6 @@ async function loadCredentialsFromFile(path) {
56
56
  delegateKey,
57
57
  agentId: stringField(raw.agent_id ?? raw.agentId),
58
58
  accountAddress,
59
- safeAddress: accountAddress,
60
59
  delegateAddress: stringField(raw.delegate_address ?? raw.delegateAddress),
61
60
  chainId: numberField(raw.chain_id ?? raw.chainId),
62
61
  network: stringField(raw.network),
@@ -86,13 +85,11 @@ async function loadCredentialsFromSplitFiles(identityPath, signerPath) {
86
85
  identity.agent_id ?? identity.agentId,
87
86
  signer.agent_id ?? signer.agentId
88
87
  ),
89
- ...accountAddressTwins(
90
- matchingStringField(
91
- "account_address",
92
- readAccountAddressField(identity),
93
- readAccountAddressField(signer),
94
- { caseInsensitive: true }
95
- )
88
+ accountAddress: matchingStringField(
89
+ "account_address",
90
+ readAccountAddressField(identity),
91
+ readAccountAddressField(signer),
92
+ { caseInsensitive: true }
96
93
  ),
97
94
  delegateAddress: matchingStringField(
98
95
  "delegate_address",
@@ -147,7 +144,7 @@ function loadCredentialsFromEnv() {
147
144
  apiKey,
148
145
  delegateKey,
149
146
  agentId: stringField(process.env.HAVEN_AGENT_ID),
150
- ...accountAddressTwins(readAccountAddressEnv(process.env)),
147
+ accountAddress: readAccountAddressEnv(process.env),
151
148
  chainId: numberField(process.env.HAVEN_CHAIN_ID),
152
149
  network: stringField(process.env.HAVEN_NETWORK),
153
150
  apiUrl: stringField(process.env.HAVEN_API_URL)
@@ -157,10 +154,22 @@ function readAccountAddressField(raw) {
157
154
  return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
158
155
  }
159
156
  function readAccountAddressEnv(env) {
160
- return stringField(env.HAVEN_ACCOUNT_ADDRESS ?? env.HAVEN_WALLET_ADDRESS ?? env.HAVEN_SAFE_ADDRESS);
161
- }
162
- function accountAddressTwins(address) {
163
- return { accountAddress: address, safeAddress: address };
157
+ const current = stringField(env.HAVEN_ACCOUNT_ADDRESS);
158
+ for (const name of ["HAVEN_WALLET_ADDRESS", "HAVEN_SAFE_ADDRESS"]) {
159
+ const retired = stringField(env[name]);
160
+ if (!retired) continue;
161
+ if (!current) {
162
+ throw new Error(
163
+ `${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.`
164
+ );
165
+ }
166
+ if (retired.toLowerCase() !== current.toLowerCase()) {
167
+ throw new Error(
168
+ `${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.`
169
+ );
170
+ }
171
+ }
172
+ return current;
164
173
  }
165
174
  function stringField(value) {
166
175
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -374,6 +383,8 @@ function createToolHandlers(haven) {
374
383
  const attempt = () => haven.fetch(merchantUrl, init, { idempotencyKey });
375
384
  let response = await attempt();
376
385
  if (!response.ok) {
386
+ const notReady = await merchantNotReadyErrorFor(response);
387
+ if (notReady) throw notReady;
377
388
  const discovered = await sdk.discoverMerchantMcpUrl(merchantUrl);
378
389
  if (!discovered || sdk.sameUrl(discovered, merchantUrl)) {
379
390
  throw discoveryMissError(response, merchantUrl, discovered);
@@ -382,6 +393,8 @@ function createToolHandlers(haven) {
382
393
  merchantUrl = discovered;
383
394
  const retryResponse = await attempt();
384
395
  if (!retryResponse.ok) {
396
+ const notReadyAtDiscovered = await merchantNotReadyErrorFor(retryResponse);
397
+ if (notReadyAtDiscovered) throw notReadyAtDiscovered;
385
398
  throw discoveryMissError(retryResponse, merchantUrl, discovered, inputUrl);
386
399
  }
387
400
  response = retryResponse;
@@ -615,12 +628,50 @@ function parseMaybeJson(text) {
615
628
  return text;
616
629
  }
617
630
  }
631
+ var MerchantNotReadyError = class extends Error {
632
+ code = sdk.AgentPaymentFailureCode.MerchantNotReady;
633
+ statusCode = 503;
634
+ nextAction = sdk.AgentPaymentNextAction.StopAndTellUser;
635
+ // Genuinely retryable — unlike a rejection, nothing about THIS call was
636
+ // wrong; the merchant's own wallet needs to recover first.
637
+ retryWithNewQuote = true;
638
+ constructor(message) {
639
+ super(message);
640
+ this.name = "MerchantNotReadyError";
641
+ }
642
+ };
643
+ async function merchantNotReadyErrorFor(response) {
644
+ if (response.status !== 503) return null;
645
+ let body;
646
+ try {
647
+ body = await response.clone().json();
648
+ } catch {
649
+ return null;
650
+ }
651
+ if (!body || typeof body !== "object" || body.error !== "merchant_not_ready") {
652
+ return null;
653
+ }
654
+ const { reason_code, settlements_remaining, retry_after_s } = body;
655
+ return new MerchantNotReadyError(
656
+ "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.")
657
+ );
658
+ }
618
659
  function discoveryMissError(response, merchantUrl, discovered, discoveredFromUrl) {
619
660
  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}.`;
620
661
  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).`;
621
662
  return new sdk.HavenApiError(`${base}${guidance}`, response.status || 400);
622
663
  }
623
664
  function normalizeError(err) {
665
+ if (err instanceof MerchantNotReadyError) {
666
+ return {
667
+ success: false,
668
+ code: err.code,
669
+ message: err.message,
670
+ statusCode: err.statusCode,
671
+ nextAction: err.nextAction,
672
+ retry_with_new_quote: err.retryWithNewQuote
673
+ };
674
+ }
624
675
  if (err instanceof sdk.HavenPaymentStateError) {
625
676
  return {
626
677
  success: false,
@@ -681,7 +732,7 @@ function computeConsentHash(input) {
681
732
  input.apiKeyPrefix,
682
733
  input.apiUrl ?? "",
683
734
  input.agentId ?? "",
684
- (input.safeAddress ?? "").toLowerCase(),
735
+ (input.accountAddress ?? "").toLowerCase(),
685
736
  (input.delegateAddress ?? "").toLowerCase(),
686
737
  input.chainId ?? ""
687
738
  ].join("|");
@@ -700,7 +751,7 @@ function renderConsentBlock(input, hash) {
700
751
  ];
701
752
  if (input.apiUrl) lines.push(`Haven API: ${input.apiUrl}`);
702
753
  if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
703
- if (input.safeAddress) lines.push(`Haven wallet (Safe): ${input.safeAddress}`);
754
+ if (input.accountAddress) lines.push(`Haven wallet (Safe): ${input.accountAddress}`);
704
755
  if (input.delegateAddress) lines.push(`Delegate (local signer): ${input.delegateAddress}`);
705
756
  if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
706
757
  lines.push("");
@@ -807,14 +858,14 @@ async function writeAckFile(path$1, hash) {
807
858
  }
808
859
  async function consentInputFromClient(haven, seed, toolNames) {
809
860
  let allowanceSummary = seed.allowanceSummary ?? [];
810
- let safeAddress = seed.safeAddress;
861
+ let accountAddress = seed.accountAddress;
811
862
  let delegateAddress = seed.delegateAddress;
812
863
  let chainId = seed.chainId;
813
864
  try {
814
865
  const summary = await haven.getAllowances();
815
866
  const list = isAllowanceSummary(summary) ? summary.allowances : Array.isArray(summary) ? summary : [];
816
867
  if (isAllowanceSummary(summary)) {
817
- safeAddress = summary.accountAddress ?? summary.safeAddress ?? safeAddress;
868
+ accountAddress = summary.accountAddress ?? accountAddress;
818
869
  delegateAddress = summary.delegateAddress;
819
870
  chainId = typeof summary.chainId === "number" ? summary.chainId : chainId;
820
871
  }
@@ -830,7 +881,7 @@ async function consentInputFromClient(haven, seed, toolNames) {
830
881
  apiKeyPrefix: derivePrefix(seed.apiKey),
831
882
  apiUrl: seed.apiUrl,
832
883
  agentId: seed.agentId,
833
- safeAddress,
884
+ accountAddress,
834
885
  delegateAddress,
835
886
  chainId,
836
887
  toolNames,
@@ -864,7 +915,7 @@ async function resolveHavenClient(options = {}) {
864
915
  return { client, credentials };
865
916
  }
866
917
  var MCP_NAME = "@haven_ai/mcp";
867
- var MCP_VERSION = "0.2.0-alpha.0";
918
+ var MCP_VERSION = "0.3.0-alpha.0";
868
919
  var MCP_INSTRUCTIONS = [
869
920
  "Haven local MCP server: signs in-process with the delegate key it holds on",
870
921
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",
@@ -937,7 +988,7 @@ async function runConsentGate(haven, credentials, options) {
937
988
  apiKey: credentials.apiKey,
938
989
  apiUrl: credentials.apiUrl,
939
990
  agentId: credentials.agentId,
940
- safeAddress: credentials.accountAddress ?? credentials.safeAddress,
991
+ accountAddress: credentials.accountAddress,
941
992
  delegateAddress: credentials.delegateAddress,
942
993
  chainId: credentials.chainId,
943
994
  allowanceSummary: credentials.allowanceSummary