@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 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/<agent-id>/identity.json --signer ~/.haven/agents/<agent-id>/signer.json
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.10.0-alpha.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",