@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.d.cts
CHANGED
|
@@ -98,6 +98,14 @@ declare const toolSchemas: {
|
|
|
98
98
|
readonly headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
99
99
|
readonly body: z.ZodOptional<z.ZodString>;
|
|
100
100
|
readonly idempotency_key: z.ZodOptional<z.ZodString>;
|
|
101
|
+
/**
|
|
102
|
+
* #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
|
|
103
|
+
* compared against the amount of the option pay will actually select. A
|
|
104
|
+
* mismatch stops the agent — the same stop signals the hosted quote
|
|
105
|
+
* carries. Local parity: a hosted-only input would split the two tools'
|
|
106
|
+
* contracts. Same shape and name as the hosted surface's.
|
|
107
|
+
*/
|
|
108
|
+
readonly expected_amount: z.ZodOptional<z.ZodString>;
|
|
101
109
|
/** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
|
|
102
110
|
readonly idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
103
111
|
};
|
|
@@ -415,7 +423,7 @@ declare function createHavenMcpServer(options?: HavenMcpServerOptions): Promise<
|
|
|
415
423
|
* `agent_tool_invocations` rows are always attributed to the right tool.
|
|
416
424
|
*/
|
|
417
425
|
declare const MCP_NAME = "@haven_ai/mcp";
|
|
418
|
-
declare const MCP_VERSION = "0.
|
|
426
|
+
declare const MCP_VERSION = "0.11.0-alpha.0";
|
|
419
427
|
/**
|
|
420
428
|
* Refuse to start on an unsupported Node (#1161).
|
|
421
429
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -98,6 +98,14 @@ declare const toolSchemas: {
|
|
|
98
98
|
readonly headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
99
99
|
readonly body: z.ZodOptional<z.ZodString>;
|
|
100
100
|
readonly idempotency_key: z.ZodOptional<z.ZodString>;
|
|
101
|
+
/**
|
|
102
|
+
* #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
|
|
103
|
+
* compared against the amount of the option pay will actually select. A
|
|
104
|
+
* mismatch stops the agent — the same stop signals the hosted quote
|
|
105
|
+
* carries. Local parity: a hosted-only input would split the two tools'
|
|
106
|
+
* contracts. Same shape and name as the hosted surface's.
|
|
107
|
+
*/
|
|
108
|
+
readonly expected_amount: z.ZodOptional<z.ZodString>;
|
|
101
109
|
/** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
|
|
102
110
|
readonly idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
103
111
|
};
|
|
@@ -415,7 +423,7 @@ declare function createHavenMcpServer(options?: HavenMcpServerOptions): Promise<
|
|
|
415
423
|
* `agent_tool_invocations` rows are always attributed to the right tool.
|
|
416
424
|
*/
|
|
417
425
|
declare const MCP_NAME = "@haven_ai/mcp";
|
|
418
|
-
declare const MCP_VERSION = "0.
|
|
426
|
+
declare const MCP_VERSION = "0.11.0-alpha.0";
|
|
419
427
|
/**
|
|
420
428
|
* Refuse to start on an unsupported Node (#1161).
|
|
421
429
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { composeDescription, toolDescriptions as toolDescriptions$1, TASK_SIGN_CONTEXT_VERSION, connectorUpgradeCommand, HavenPaymentStateError, HavenSigningError,
|
|
3
|
+
import { composeDescription, toolDescriptions as toolDescriptions$1, TASK_SIGN_CONTEXT_VERSION, connectorUpgradeCommand, x402AuthorizationAmount, AgentPaymentNextAction, HavenPaymentStateError, HavenSigningError, MerchantEgressRefusedError, HavenApiError, HavenError, resolveTokenBySymbol, formatTokenAmount, HavenClient, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, verifyPaymentReceipt, catalogFundingLegExpected, discoverMerchantMcpUrl, sameUrl, selectErc7710PaymentOption, selectStandardPaymentOption, AgentPaymentFailureCode, resolveTokenFromAddress } from '@haven_ai/sdk';
|
|
4
4
|
import { readFile, stat, mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import { z } from 'zod/v3';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
@@ -236,6 +236,18 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
function chainRpcsFromEnv(env) {
|
|
240
|
+
const chainRpcs = {};
|
|
241
|
+
for (const [name, value] of Object.entries(env)) {
|
|
242
|
+
const match = /^HAVEN_RPC_URL_(\d+)$/.exec(name);
|
|
243
|
+
if (!match) continue;
|
|
244
|
+
const chainId = Number.parseInt(match[1], 10);
|
|
245
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) continue;
|
|
246
|
+
const url = value?.trim();
|
|
247
|
+
if (url) chainRpcs[chainId] = url;
|
|
248
|
+
}
|
|
249
|
+
return chainRpcs;
|
|
250
|
+
}
|
|
239
251
|
var TASK_BUDGET_MIN_SIGNER_VERSION = "0.6.0-alpha.0";
|
|
240
252
|
var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
|
|
241
253
|
function taskSignerCompatibilityNotice() {
|
|
@@ -248,6 +260,20 @@ function taskSignerCompatibilityNotice() {
|
|
|
248
260
|
};
|
|
249
261
|
}
|
|
250
262
|
var headersSchema = z.record(z.string(), z.string()).optional();
|
|
263
|
+
function selectQuoteOption(accepts, agent) {
|
|
264
|
+
if (agent.executionRail === "delegation") {
|
|
265
|
+
const erc7710 = selectErc7710PaymentOption(accepts);
|
|
266
|
+
if (erc7710) return erc7710;
|
|
267
|
+
}
|
|
268
|
+
return selectStandardPaymentOption(accepts);
|
|
269
|
+
}
|
|
270
|
+
function humanDecimalToAtomic(human, decimals) {
|
|
271
|
+
const match = /^([0-9]+)(?:\.([0-9]+))?$/.exec(human);
|
|
272
|
+
if (!match) return null;
|
|
273
|
+
const fraction = match[2] ?? "";
|
|
274
|
+
if (fraction.length > decimals) return null;
|
|
275
|
+
return BigInt(match[1] + fraction.padEnd(decimals, "0"));
|
|
276
|
+
}
|
|
251
277
|
var toolSchemas = {
|
|
252
278
|
haven_send: {
|
|
253
279
|
asset: z.enum(["ETH", "USDC"]),
|
|
@@ -279,6 +305,17 @@ var toolSchemas = {
|
|
|
279
305
|
headers: headersSchema,
|
|
280
306
|
body: z.string().optional(),
|
|
281
307
|
idempotency_key: z.string().optional(),
|
|
308
|
+
/**
|
|
309
|
+
* #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
|
|
310
|
+
* compared against the amount of the option pay will actually select. A
|
|
311
|
+
* mismatch stops the agent — the same stop signals the hosted quote
|
|
312
|
+
* carries. Local parity: a hosted-only input would split the two tools'
|
|
313
|
+
* contracts. Same shape and name as the hosted surface's.
|
|
314
|
+
*/
|
|
315
|
+
expected_amount: z.string().regex(
|
|
316
|
+
/^[0-9]+(\.[0-9]+)?$/,
|
|
317
|
+
'expected_amount must be a plain decimal amount in whole tokens, e.g. "0.10"'
|
|
318
|
+
).optional(),
|
|
282
319
|
/** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
|
|
283
320
|
idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
|
|
284
321
|
},
|
|
@@ -625,8 +662,39 @@ function createToolHandlers(haven) {
|
|
|
625
662
|
const pf = preflight("haven_quote_x402", input);
|
|
626
663
|
if ("success" in pf) return pf;
|
|
627
664
|
const { args, warnings } = pf;
|
|
665
|
+
const agentPromise = args.expected_amount !== void 0 ? haven.getAgent().then(
|
|
666
|
+
(a) => a,
|
|
667
|
+
() => void 0
|
|
668
|
+
) : null;
|
|
628
669
|
try {
|
|
629
670
|
const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.resolvedIdempotencyKey });
|
|
671
|
+
if (args.expected_amount !== void 0) {
|
|
672
|
+
const agent = agentPromise ? await agentPromise : void 0;
|
|
673
|
+
const option = agent ? selectQuoteOption(data.paymentRequired.accepts, agent) : null;
|
|
674
|
+
if (option && data.decimals !== null) {
|
|
675
|
+
const expectedAtomic = humanDecimalToAtomic(args.expected_amount, data.decimals);
|
|
676
|
+
let optionAmount = null;
|
|
677
|
+
try {
|
|
678
|
+
optionAmount = BigInt(x402AuthorizationAmount(option));
|
|
679
|
+
} catch {
|
|
680
|
+
optionAmount = null;
|
|
681
|
+
}
|
|
682
|
+
if (optionAmount !== null && expectedAtomic !== null) {
|
|
683
|
+
const priceDiffers = optionAmount !== expectedAtomic;
|
|
684
|
+
const payload = {
|
|
685
|
+
...data,
|
|
686
|
+
price_differs_from_expected: priceDiffers,
|
|
687
|
+
...priceDiffers ? {
|
|
688
|
+
safe_to_continue: false,
|
|
689
|
+
next_action: AgentPaymentNextAction.StopAndTellUser,
|
|
690
|
+
next_tool_omitted_reason: "no Haven tool follows: the quoted price does not match the stated expectation \u2014 the user decides before any payment",
|
|
691
|
+
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.`
|
|
692
|
+
} : {}
|
|
693
|
+
};
|
|
694
|
+
return warnings.length > 0 ? { success: true, data: payload, warnings } : { success: true, data: payload };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
630
698
|
return warnings.length > 0 ? { success: true, data, warnings } : { success: true, data };
|
|
631
699
|
} catch (err) {
|
|
632
700
|
return normalizeError(err);
|
|
@@ -725,6 +793,10 @@ function createToolHandlers(haven) {
|
|
|
725
793
|
price_atomic: entry.priceAtomic,
|
|
726
794
|
asset: entry.asset,
|
|
727
795
|
network: entry.network,
|
|
796
|
+
// #3839: same hint the hosted surface emits — a catalog hint, the
|
|
797
|
+
// live quote stays authoritative; "unknown" when nothing is recorded.
|
|
798
|
+
asset_transfer_methods: entry.assetTransferMethods ?? null,
|
|
799
|
+
funding_leg_expected: catalogFundingLegExpected(entry.assetTransferMethods),
|
|
728
800
|
status: entry.status,
|
|
729
801
|
verified_at: entry.verifiedAt,
|
|
730
802
|
source: entry.source,
|
|
@@ -990,17 +1062,26 @@ function parseMaybeJson(text) {
|
|
|
990
1062
|
return text;
|
|
991
1063
|
}
|
|
992
1064
|
}
|
|
1065
|
+
var MERCHANT_OUT_OF_GAS_REASON = "settlement_wallet_out_of_gas";
|
|
1066
|
+
function merchantOutOfGasMessage(settlementsRemaining, failFloor) {
|
|
1067
|
+
const remaining = typeof settlementsRemaining === "number" ? settlementsRemaining : null;
|
|
1068
|
+
const floor = typeof failFloor === "number" ? failFloor : null;
|
|
1069
|
+
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}.` : "";
|
|
1070
|
+
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.";
|
|
1071
|
+
}
|
|
993
1072
|
var MerchantNotReadyError = class extends Error {
|
|
1073
|
+
constructor(message, nextToolOmittedReason = "the merchant needs to recover first; re-quote after the retry_after_s in the message") {
|
|
1074
|
+
super(message);
|
|
1075
|
+
this.nextToolOmittedReason = nextToolOmittedReason;
|
|
1076
|
+
this.name = "MerchantNotReadyError";
|
|
1077
|
+
}
|
|
1078
|
+
nextToolOmittedReason;
|
|
994
1079
|
code = AgentPaymentFailureCode.MerchantNotReady;
|
|
995
1080
|
statusCode = 503;
|
|
996
1081
|
nextAction = AgentPaymentNextAction.StopAndTellUser;
|
|
997
1082
|
// Genuinely retryable — unlike a rejection, nothing about THIS call was
|
|
998
1083
|
// wrong; the merchant's own wallet needs to recover first.
|
|
999
1084
|
retryWithNewQuote = true;
|
|
1000
|
-
constructor(message) {
|
|
1001
|
-
super(message);
|
|
1002
|
-
this.name = "MerchantNotReadyError";
|
|
1003
|
-
}
|
|
1004
1085
|
};
|
|
1005
1086
|
async function merchantNotReadyErrorFor(response) {
|
|
1006
1087
|
if (response.status !== 503) return null;
|
|
@@ -1013,7 +1094,14 @@ async function merchantNotReadyErrorFor(response) {
|
|
|
1013
1094
|
if (!body || typeof body !== "object" || body.error !== "merchant_not_ready") {
|
|
1014
1095
|
return null;
|
|
1015
1096
|
}
|
|
1016
|
-
const { reason_code, settlements_remaining, retry_after_s } = body;
|
|
1097
|
+
const { reason_code, settlements_remaining, fail_floor, retry_after_s } = body;
|
|
1098
|
+
if (reason_code === MERCHANT_OUT_OF_GAS_REASON) {
|
|
1099
|
+
return new MerchantNotReadyError(
|
|
1100
|
+
merchantOutOfGasMessage(settlements_remaining, fail_floor),
|
|
1101
|
+
// Same reason the hosted runtime returns (support/mcp-context.ts).
|
|
1102
|
+
"the merchant's operator must top up its settlement wallet first; re-quote after that"
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1017
1105
|
return new MerchantNotReadyError(
|
|
1018
1106
|
"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.")
|
|
1019
1107
|
);
|
|
@@ -1034,8 +1122,10 @@ function normalizeError(err) {
|
|
|
1034
1122
|
next_action: err.nextAction,
|
|
1035
1123
|
retry_with_new_quote: err.retryWithNewQuote,
|
|
1036
1124
|
// #3103: the local runtime's one decision site — no tool can act until
|
|
1037
|
-
// the merchant recovers
|
|
1038
|
-
|
|
1125
|
+
// the merchant recovers. The reason rides the error (#3834): the
|
|
1126
|
+
// generic one points at the message's retry_after_s, the out-of-gas
|
|
1127
|
+
// one at the operator top-up.
|
|
1128
|
+
next_tool_omitted_reason: err.nextToolOmittedReason
|
|
1039
1129
|
};
|
|
1040
1130
|
}
|
|
1041
1131
|
if (err instanceof HavenPaymentStateError) {
|
|
@@ -1060,6 +1150,24 @@ function normalizeError(err) {
|
|
|
1060
1150
|
message: err.message
|
|
1061
1151
|
};
|
|
1062
1152
|
}
|
|
1153
|
+
if (err instanceof MerchantEgressRefusedError && !err.beforeRequest && err.paymentId) {
|
|
1154
|
+
const noFundingLeg = err.settlementScheme === "erc7710";
|
|
1155
|
+
const nextAction = noFundingLeg ? AgentPaymentNextAction.CheckStatusLater : AgentPaymentNextAction.SweepStrandedFunds;
|
|
1156
|
+
return {
|
|
1157
|
+
success: false,
|
|
1158
|
+
code: err.code,
|
|
1159
|
+
message: err.message,
|
|
1160
|
+
statusCode: err.statusCode,
|
|
1161
|
+
paymentId: err.paymentId,
|
|
1162
|
+
status: "merchant_unresponsive_after_funding",
|
|
1163
|
+
phase: noFundingLeg ? "not_delivered" : "funded_but_unsettled",
|
|
1164
|
+
nextAction,
|
|
1165
|
+
next_action: nextAction,
|
|
1166
|
+
next_tool: "haven_get_payment_status",
|
|
1167
|
+
next_arguments: { payment_id: err.paymentId },
|
|
1168
|
+
suggested_tool: "haven_get_payment_status"
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1063
1171
|
if (err instanceof HavenApiError) {
|
|
1064
1172
|
const body = err.body;
|
|
1065
1173
|
return {
|
|
@@ -1306,10 +1414,14 @@ async function resolveHavenClient(options = {}) {
|
|
|
1306
1414
|
signerPath: options.signerPath
|
|
1307
1415
|
} : void 0;
|
|
1308
1416
|
const credentials = options.credentials ?? await loadCredentials(credentialSource);
|
|
1417
|
+
const chainRpcs = chainRpcsFromEnv(process.env);
|
|
1309
1418
|
const client = new HavenClient({
|
|
1310
1419
|
apiKey: credentials.apiKey,
|
|
1311
1420
|
delegateKey: credentials.delegateKey,
|
|
1312
1421
|
baseUrl: credentials.apiUrl,
|
|
1422
|
+
// Only added when non-empty so the unset-variable construction is byte-for-byte
|
|
1423
|
+
// today's: the SDK defaults `chainRpcs` to `{}` either way.
|
|
1424
|
+
...Object.keys(chainRpcs).length > 0 ? { chainRpcs } : {},
|
|
1313
1425
|
// #3303: name this package, not the SDK inside it, so the backend's
|
|
1314
1426
|
// update hint and refusal speak about what the user actually installed.
|
|
1315
1427
|
clientIdentity: havenClientIdentity(MCP_NAME, MCP_VERSION)
|
|
@@ -1321,7 +1433,7 @@ async function createHavenMcpServer(options = {}) {
|
|
|
1321
1433
|
return buildMcpServer(haven);
|
|
1322
1434
|
}
|
|
1323
1435
|
var MCP_NAME = "@haven_ai/mcp";
|
|
1324
|
-
var MCP_VERSION = "0.
|
|
1436
|
+
var MCP_VERSION = "0.11.0-alpha.0";
|
|
1325
1437
|
var MCP_INSTRUCTIONS = [
|
|
1326
1438
|
"Haven local MCP server: signs in-process with the delegate key it holds on",
|
|
1327
1439
|
"this machine \u2014 the key never leaves this process. Call haven_get_agent",
|