@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 +6 -3
- package/dist/cli.cjs +71 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +72 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +71 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -20
- package/dist/index.d.ts +15 -20
- package/dist/index.js +72 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
991
|
+
accountAddress: credentials.accountAddress,
|
|
941
992
|
delegateAddress: credentials.delegateAddress,
|
|
942
993
|
chainId: credentials.chainId,
|
|
943
994
|
allowanceSummary: credentials.allowanceSummary
|