@haven_ai/mcp 0.10.0-alpha.0 → 0.12.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 +43 -1
- package/dist/cli.cjs +91 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +92 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +91 -1
- 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 +92 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -54,9 +54,13 @@ the whole difference between the two paragraphs.
|
|
|
54
54
|
The Haven connector may also write split credentials:
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
|
-
npx @haven_ai/mcp@alpha --identity ~/.haven/agents/<
|
|
57
|
+
npx @haven_ai/mcp@alpha --identity ~/.haven/agents/<slug>/identity.json --signer ~/.haven/agents/<slug>/signer.json
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
A default setup names that directory from the wiring slug (the `haven-<slug>`
|
|
61
|
+
pair, #3737); only a `--bare` run, or a `--replace` without `--name`, names it
|
|
62
|
+
by the agent id instead.
|
|
63
|
+
|
|
60
64
|
`identity.json` holds the local API key and setup metadata. `signer.json` holds
|
|
61
65
|
the delegate key. Both files stay on the user's machine.
|
|
62
66
|
|
|
@@ -105,6 +109,44 @@ Environment variable form:
|
|
|
105
109
|
}
|
|
106
110
|
```
|
|
107
111
|
|
|
112
|
+
## Per-chain RPC endpoints
|
|
113
|
+
|
|
114
|
+
When an x402 payment hits an idempotency collision, the server can tell a
|
|
115
|
+
finished payment from a stuck one only by reading the agent delegate's balance
|
|
116
|
+
on chain. Without an RPC endpoint it cannot, so every collision is refused as
|
|
117
|
+
`unverifiable`, including the recoverable case where the delegate is still
|
|
118
|
+
funded and the payment could resume.
|
|
119
|
+
|
|
120
|
+
Give the server a read-only RPC endpoint per chain and it can check:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"haven": {
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": ["@haven_ai/mcp"],
|
|
128
|
+
"env": {
|
|
129
|
+
"HAVEN_CREDENTIALS": "/absolute/path/to/haven-agent.json",
|
|
130
|
+
"HAVEN_RPC_URL_8453": "https://your-rpc.example/v3/key",
|
|
131
|
+
"HAVEN_RPC_URL_84532": "https://your-rpc.example/sepolia/v3/key"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
One variable per chain: the suffix is the chain id (`8453` is Base mainnet,
|
|
139
|
+
`84532` is Base Sepolia). An unset variable changes nothing: collisions are
|
|
140
|
+
refused exactly as before. A value whose suffix is not a positive integer is
|
|
141
|
+
ignored, and a malformed or unreachable URL degrades to the same refusal.
|
|
142
|
+
Nothing is written to stdout in any of these cases, because stdout is the
|
|
143
|
+
protocol channel.
|
|
144
|
+
|
|
145
|
+
With an endpoint wired for the payment's chain, a collision where the delegate
|
|
146
|
+
is still funded resumes: the balance says the merchant never pulled the
|
|
147
|
+
payment, so the server mints a fresh authorization. A drained delegate still
|
|
148
|
+
refuses, with a settled basis, because the payment already completed.
|
|
149
|
+
|
|
108
150
|
## Tools
|
|
109
151
|
|
|
110
152
|
- `haven_quote_x402`
|
package/dist/cli.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);
|
|
@@ -1084,6 +1152,24 @@ function normalizeError(err) {
|
|
|
1084
1152
|
message: err.message
|
|
1085
1153
|
};
|
|
1086
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
|
+
}
|
|
1087
1173
|
if (err instanceof sdk.HavenApiError) {
|
|
1088
1174
|
const body = err.body;
|
|
1089
1175
|
return {
|
|
@@ -1326,10 +1412,14 @@ async function resolveHavenClient(options = {}) {
|
|
|
1326
1412
|
signerPath: options.signerPath
|
|
1327
1413
|
} : void 0;
|
|
1328
1414
|
const credentials = options.credentials ?? await loadCredentials(credentialSource);
|
|
1415
|
+
const chainRpcs = chainRpcsFromEnv(process.env);
|
|
1329
1416
|
const client = new sdk.HavenClient({
|
|
1330
1417
|
apiKey: credentials.apiKey,
|
|
1331
1418
|
delegateKey: credentials.delegateKey,
|
|
1332
1419
|
baseUrl: credentials.apiUrl,
|
|
1420
|
+
// Only added when non-empty so the unset-variable construction is byte-for-byte
|
|
1421
|
+
// today's: the SDK defaults `chainRpcs` to `{}` either way.
|
|
1422
|
+
...Object.keys(chainRpcs).length > 0 ? { chainRpcs } : {},
|
|
1333
1423
|
// #3303: name this package, not the SDK inside it, so the backend's
|
|
1334
1424
|
// update hint and refusal speak about what the user actually installed.
|
|
1335
1425
|
clientIdentity: sdk.havenClientIdentity(MCP_NAME, MCP_VERSION)
|
|
@@ -1337,7 +1427,7 @@ async function resolveHavenClient(options = {}) {
|
|
|
1337
1427
|
return { client, credentials };
|
|
1338
1428
|
}
|
|
1339
1429
|
var MCP_NAME = "@haven_ai/mcp";
|
|
1340
|
-
var MCP_VERSION = "0.
|
|
1430
|
+
var MCP_VERSION = "0.12.0-alpha.0";
|
|
1341
1431
|
var MCP_INSTRUCTIONS = [
|
|
1342
1432
|
"Haven local MCP server: signs in-process with the delegate key it holds on",
|
|
1343
1433
|
"this machine \u2014 the key never leaves this process. Call haven_get_agent",
|