@haven_ai/mcp 0.9.0-alpha.0 → 0.11.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 +38 -0
- package/dist/cli.cjs +120 -8
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +121 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +120 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +121 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -238,6 +238,18 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
238
238
|
);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
function chainRpcsFromEnv(env) {
|
|
242
|
+
const chainRpcs = {};
|
|
243
|
+
for (const [name, value] of Object.entries(env)) {
|
|
244
|
+
const match = /^HAVEN_RPC_URL_(\d+)$/.exec(name);
|
|
245
|
+
if (!match) continue;
|
|
246
|
+
const chainId = Number.parseInt(match[1], 10);
|
|
247
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) continue;
|
|
248
|
+
const url = value?.trim();
|
|
249
|
+
if (url) chainRpcs[chainId] = url;
|
|
250
|
+
}
|
|
251
|
+
return chainRpcs;
|
|
252
|
+
}
|
|
241
253
|
var TASK_BUDGET_MIN_SIGNER_VERSION = "0.6.0-alpha.0";
|
|
242
254
|
var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
|
|
243
255
|
function taskSignerCompatibilityNotice() {
|
|
@@ -250,6 +262,20 @@ function taskSignerCompatibilityNotice() {
|
|
|
250
262
|
};
|
|
251
263
|
}
|
|
252
264
|
var headersSchema = v3.z.record(v3.z.string(), v3.z.string()).optional();
|
|
265
|
+
function selectQuoteOption(accepts, agent) {
|
|
266
|
+
if (agent.executionRail === "delegation") {
|
|
267
|
+
const erc7710 = sdk.selectErc7710PaymentOption(accepts);
|
|
268
|
+
if (erc7710) return erc7710;
|
|
269
|
+
}
|
|
270
|
+
return sdk.selectStandardPaymentOption(accepts);
|
|
271
|
+
}
|
|
272
|
+
function humanDecimalToAtomic(human, decimals) {
|
|
273
|
+
const match = /^([0-9]+)(?:\.([0-9]+))?$/.exec(human);
|
|
274
|
+
if (!match) return null;
|
|
275
|
+
const fraction = match[2] ?? "";
|
|
276
|
+
if (fraction.length > decimals) return null;
|
|
277
|
+
return BigInt(match[1] + fraction.padEnd(decimals, "0"));
|
|
278
|
+
}
|
|
253
279
|
var toolSchemas = {
|
|
254
280
|
haven_send: {
|
|
255
281
|
asset: v3.z.enum(["ETH", "USDC"]),
|
|
@@ -281,6 +307,17 @@ var toolSchemas = {
|
|
|
281
307
|
headers: headersSchema,
|
|
282
308
|
body: v3.z.string().optional(),
|
|
283
309
|
idempotency_key: v3.z.string().optional(),
|
|
310
|
+
/**
|
|
311
|
+
* #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
|
|
312
|
+
* compared against the amount of the option pay will actually select. A
|
|
313
|
+
* mismatch stops the agent — the same stop signals the hosted quote
|
|
314
|
+
* carries. Local parity: a hosted-only input would split the two tools'
|
|
315
|
+
* contracts. Same shape and name as the hosted surface's.
|
|
316
|
+
*/
|
|
317
|
+
expected_amount: v3.z.string().regex(
|
|
318
|
+
/^[0-9]+(\.[0-9]+)?$/,
|
|
319
|
+
'expected_amount must be a plain decimal amount in whole tokens, e.g. "0.10"'
|
|
320
|
+
).optional(),
|
|
284
321
|
/** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
|
|
285
322
|
idempotencyKey: v3.z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
|
|
286
323
|
},
|
|
@@ -627,8 +664,39 @@ function createToolHandlers(haven) {
|
|
|
627
664
|
const pf = preflight("haven_quote_x402", input);
|
|
628
665
|
if ("success" in pf) return pf;
|
|
629
666
|
const { args, warnings } = pf;
|
|
667
|
+
const agentPromise = args.expected_amount !== void 0 ? haven.getAgent().then(
|
|
668
|
+
(a) => a,
|
|
669
|
+
() => void 0
|
|
670
|
+
) : null;
|
|
630
671
|
try {
|
|
631
672
|
const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.resolvedIdempotencyKey });
|
|
673
|
+
if (args.expected_amount !== void 0) {
|
|
674
|
+
const agent = agentPromise ? await agentPromise : void 0;
|
|
675
|
+
const option = agent ? selectQuoteOption(data.paymentRequired.accepts, agent) : null;
|
|
676
|
+
if (option && data.decimals !== null) {
|
|
677
|
+
const expectedAtomic = humanDecimalToAtomic(args.expected_amount, data.decimals);
|
|
678
|
+
let optionAmount = null;
|
|
679
|
+
try {
|
|
680
|
+
optionAmount = BigInt(sdk.x402AuthorizationAmount(option));
|
|
681
|
+
} catch {
|
|
682
|
+
optionAmount = null;
|
|
683
|
+
}
|
|
684
|
+
if (optionAmount !== null && expectedAtomic !== null) {
|
|
685
|
+
const priceDiffers = optionAmount !== expectedAtomic;
|
|
686
|
+
const payload = {
|
|
687
|
+
...data,
|
|
688
|
+
price_differs_from_expected: priceDiffers,
|
|
689
|
+
...priceDiffers ? {
|
|
690
|
+
safe_to_continue: false,
|
|
691
|
+
next_action: sdk.AgentPaymentNextAction.StopAndTellUser,
|
|
692
|
+
next_tool_omitted_reason: "no Haven tool follows: the quoted price does not match the stated expectation \u2014 the user decides before any payment",
|
|
693
|
+
reason: `The merchant's quoted price (${data.amount} ${data.asset}) does not match the expected price (${args.expected_amount}) stated with this quote. Stop and tell the user before any payment; do not re-quote, re-pay, or poll. If the x402 challenge itself is wrong and nothing has been paid via x402 on this invoice, the invoice's own deposit address MAY be an alternative \u2014 but only with the user's explicit go-ahead, never automatically, and never once anything has been paid via x402.`
|
|
694
|
+
} : {}
|
|
695
|
+
};
|
|
696
|
+
return warnings.length > 0 ? { success: true, data: payload, warnings } : { success: true, data: payload };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
632
700
|
return warnings.length > 0 ? { success: true, data, warnings } : { success: true, data };
|
|
633
701
|
} catch (err) {
|
|
634
702
|
return normalizeError(err);
|
|
@@ -727,6 +795,10 @@ function createToolHandlers(haven) {
|
|
|
727
795
|
price_atomic: entry.priceAtomic,
|
|
728
796
|
asset: entry.asset,
|
|
729
797
|
network: entry.network,
|
|
798
|
+
// #3839: same hint the hosted surface emits — a catalog hint, the
|
|
799
|
+
// live quote stays authoritative; "unknown" when nothing is recorded.
|
|
800
|
+
asset_transfer_methods: entry.assetTransferMethods ?? null,
|
|
801
|
+
funding_leg_expected: sdk.catalogFundingLegExpected(entry.assetTransferMethods),
|
|
730
802
|
status: entry.status,
|
|
731
803
|
verified_at: entry.verifiedAt,
|
|
732
804
|
source: entry.source,
|
|
@@ -992,17 +1064,26 @@ function parseMaybeJson(text) {
|
|
|
992
1064
|
return text;
|
|
993
1065
|
}
|
|
994
1066
|
}
|
|
1067
|
+
var MERCHANT_OUT_OF_GAS_REASON = "settlement_wallet_out_of_gas";
|
|
1068
|
+
function merchantOutOfGasMessage(settlementsRemaining, failFloor) {
|
|
1069
|
+
const remaining = typeof settlementsRemaining === "number" ? settlementsRemaining : null;
|
|
1070
|
+
const floor = typeof failFloor === "number" ? failFloor : null;
|
|
1071
|
+
const figures = remaining !== null && floor !== null ? ` It has gas for ${remaining} more settlement${remaining === 1 ? "" : "s"} and refuses new payments below ${floor} (settlements_remaining: ${remaining}, fail_floor: ${floor}).` : remaining !== null ? ` settlements_remaining: ${remaining}.` : "";
|
|
1072
|
+
return `The merchant refused this call: its settlement wallet is out of gas (reason_code: ${MERCHANT_OUT_OF_GAS_REASON}).` + figures + " No payment was created. The merchant's operator must top up its settlement wallet; until then every retry is refused again. Tell the user, and re-quote once the merchant has been topped up.";
|
|
1073
|
+
}
|
|
995
1074
|
var MerchantNotReadyError = class extends Error {
|
|
1075
|
+
constructor(message, nextToolOmittedReason = "the merchant needs to recover first; re-quote after the retry_after_s in the message") {
|
|
1076
|
+
super(message);
|
|
1077
|
+
this.nextToolOmittedReason = nextToolOmittedReason;
|
|
1078
|
+
this.name = "MerchantNotReadyError";
|
|
1079
|
+
}
|
|
1080
|
+
nextToolOmittedReason;
|
|
996
1081
|
code = sdk.AgentPaymentFailureCode.MerchantNotReady;
|
|
997
1082
|
statusCode = 503;
|
|
998
1083
|
nextAction = sdk.AgentPaymentNextAction.StopAndTellUser;
|
|
999
1084
|
// Genuinely retryable — unlike a rejection, nothing about THIS call was
|
|
1000
1085
|
// wrong; the merchant's own wallet needs to recover first.
|
|
1001
1086
|
retryWithNewQuote = true;
|
|
1002
|
-
constructor(message) {
|
|
1003
|
-
super(message);
|
|
1004
|
-
this.name = "MerchantNotReadyError";
|
|
1005
|
-
}
|
|
1006
1087
|
};
|
|
1007
1088
|
async function merchantNotReadyErrorFor(response) {
|
|
1008
1089
|
if (response.status !== 503) return null;
|
|
@@ -1015,7 +1096,14 @@ async function merchantNotReadyErrorFor(response) {
|
|
|
1015
1096
|
if (!body || typeof body !== "object" || body.error !== "merchant_not_ready") {
|
|
1016
1097
|
return null;
|
|
1017
1098
|
}
|
|
1018
|
-
const { reason_code, settlements_remaining, retry_after_s } = body;
|
|
1099
|
+
const { reason_code, settlements_remaining, fail_floor, retry_after_s } = body;
|
|
1100
|
+
if (reason_code === MERCHANT_OUT_OF_GAS_REASON) {
|
|
1101
|
+
return new MerchantNotReadyError(
|
|
1102
|
+
merchantOutOfGasMessage(settlements_remaining, fail_floor),
|
|
1103
|
+
// Same reason the hosted runtime returns (support/mcp-context.ts).
|
|
1104
|
+
"the merchant's operator must top up its settlement wallet first; re-quote after that"
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1019
1107
|
return new MerchantNotReadyError(
|
|
1020
1108
|
"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.")
|
|
1021
1109
|
);
|
|
@@ -1036,8 +1124,10 @@ function normalizeError(err) {
|
|
|
1036
1124
|
next_action: err.nextAction,
|
|
1037
1125
|
retry_with_new_quote: err.retryWithNewQuote,
|
|
1038
1126
|
// #3103: the local runtime's one decision site — no tool can act until
|
|
1039
|
-
// the merchant recovers
|
|
1040
|
-
|
|
1127
|
+
// the merchant recovers. The reason rides the error (#3834): the
|
|
1128
|
+
// generic one points at the message's retry_after_s, the out-of-gas
|
|
1129
|
+
// one at the operator top-up.
|
|
1130
|
+
next_tool_omitted_reason: err.nextToolOmittedReason
|
|
1041
1131
|
};
|
|
1042
1132
|
}
|
|
1043
1133
|
if (err instanceof sdk.HavenPaymentStateError) {
|
|
@@ -1062,6 +1152,24 @@ function normalizeError(err) {
|
|
|
1062
1152
|
message: err.message
|
|
1063
1153
|
};
|
|
1064
1154
|
}
|
|
1155
|
+
if (err instanceof sdk.MerchantEgressRefusedError && !err.beforeRequest && err.paymentId) {
|
|
1156
|
+
const noFundingLeg = err.settlementScheme === "erc7710";
|
|
1157
|
+
const nextAction = noFundingLeg ? sdk.AgentPaymentNextAction.CheckStatusLater : sdk.AgentPaymentNextAction.SweepStrandedFunds;
|
|
1158
|
+
return {
|
|
1159
|
+
success: false,
|
|
1160
|
+
code: err.code,
|
|
1161
|
+
message: err.message,
|
|
1162
|
+
statusCode: err.statusCode,
|
|
1163
|
+
paymentId: err.paymentId,
|
|
1164
|
+
status: "merchant_unresponsive_after_funding",
|
|
1165
|
+
phase: noFundingLeg ? "not_delivered" : "funded_but_unsettled",
|
|
1166
|
+
nextAction,
|
|
1167
|
+
next_action: nextAction,
|
|
1168
|
+
next_tool: "haven_get_payment_status",
|
|
1169
|
+
next_arguments: { payment_id: err.paymentId },
|
|
1170
|
+
suggested_tool: "haven_get_payment_status"
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1065
1173
|
if (err instanceof sdk.HavenApiError) {
|
|
1066
1174
|
const body = err.body;
|
|
1067
1175
|
return {
|
|
@@ -1308,10 +1416,14 @@ async function resolveHavenClient(options = {}) {
|
|
|
1308
1416
|
signerPath: options.signerPath
|
|
1309
1417
|
} : void 0;
|
|
1310
1418
|
const credentials = options.credentials ?? await loadCredentials(credentialSource);
|
|
1419
|
+
const chainRpcs = chainRpcsFromEnv(process.env);
|
|
1311
1420
|
const client = new sdk.HavenClient({
|
|
1312
1421
|
apiKey: credentials.apiKey,
|
|
1313
1422
|
delegateKey: credentials.delegateKey,
|
|
1314
1423
|
baseUrl: credentials.apiUrl,
|
|
1424
|
+
// Only added when non-empty so the unset-variable construction is byte-for-byte
|
|
1425
|
+
// today's: the SDK defaults `chainRpcs` to `{}` either way.
|
|
1426
|
+
...Object.keys(chainRpcs).length > 0 ? { chainRpcs } : {},
|
|
1315
1427
|
// #3303: name this package, not the SDK inside it, so the backend's
|
|
1316
1428
|
// update hint and refusal speak about what the user actually installed.
|
|
1317
1429
|
clientIdentity: sdk.havenClientIdentity(MCP_NAME, MCP_VERSION)
|
|
@@ -1323,7 +1435,7 @@ async function createHavenMcpServer(options = {}) {
|
|
|
1323
1435
|
return buildMcpServer(haven);
|
|
1324
1436
|
}
|
|
1325
1437
|
var MCP_NAME = "@haven_ai/mcp";
|
|
1326
|
-
var MCP_VERSION = "0.
|
|
1438
|
+
var MCP_VERSION = "0.11.0-alpha.0";
|
|
1327
1439
|
var MCP_INSTRUCTIONS = [
|
|
1328
1440
|
"Haven local MCP server: signs in-process with the delegate key it holds on",
|
|
1329
1441
|
"this machine \u2014 the key never leaves this process. Call haven_get_agent",
|