@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/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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
989
|
+
accountAddress: credentials.accountAddress,
|
|
939
990
|
delegateAddress: credentials.delegateAddress,
|
|
940
991
|
chainId: credentials.chainId,
|
|
941
992
|
allowanceSummary: credentials.allowanceSummary
|