@haven_ai/mcp 0.5.0-alpha.1 → 0.7.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/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, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, verifyPaymentReceipt, discoverMerchantMcpUrl, sameUrl } from '@haven_ai/sdk';
4
+ import { composeDescription, toolDescriptions as toolDescriptions$1, HavenClient, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, resolveTokenBySymbol, formatTokenAmount, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, verifyPaymentReceipt, discoverMerchantMcpUrl, sameUrl, resolveTokenFromAddress, TASK_SIGN_CONTEXT_VERSION, connectorUpgradeCommand } 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';
@@ -236,6 +236,17 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
236
236
  );
237
237
  }
238
238
  }
239
+ var TASK_BUDGET_MIN_SIGNER_VERSION = "0.6.0-alpha.0";
240
+ var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
241
+ function taskSignerCompatibilityNotice() {
242
+ return {
243
+ task_sign_context_version: TASK_SIGN_CONTEXT_VERSION,
244
+ min_signer_version: TASK_BUDGET_MIN_SIGNER_VERSION,
245
+ signer_capability: SIGNER_CAPABILITY_KEY,
246
+ check: `Call next_tool with next_arguments EXACTLY as given \u2014 the signer fetches the exact bytes by task_budget_id. If haven_sign answers SIGNING_ERROR with a message starting "Pass payment_id (preferred for delegation-rail x402", the signer predates task budgets and signed nothing: first close this budget with haven_close_task_budget { task_budget_id } to release it, then update the connector by running \`${connectorUpgradeCommand()}\` and the repair line it prints, then open the budget again.`,
247
+ fallback: `haven_sign answered SIGNING_ERROR with a message starting "Pass payment_id (preferred for delegation-rail x402" \u2014 the signer predates task budgets and signed nothing: call haven_close_task_budget { task_budget_id } to release the pending budget, then update the connector by running \`${connectorUpgradeCommand()}\` and the repair line it prints, then open the budget again. There is no relay fallback for this signing context.`
248
+ };
249
+ }
239
250
  var headersSchema = z.record(z.string(), z.string()).optional();
240
251
  var toolSchemas = {
241
252
  haven_send: {
@@ -243,16 +254,24 @@ var toolSchemas = {
243
254
  recipient: z.string().min(1),
244
255
  amount: z.string().min(1),
245
256
  idempotency_key: z.string().optional(),
246
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
247
- idempotencyKey: z.string().optional()
257
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
258
+ idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key"),
259
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
260
+ task_budget_id: z.string().min(1).optional(),
261
+ /**
262
+ * #3330: spend against an open sub-budget this agent HOLDS (it is the
263
+ * sub-agent B) instead of the agent's own budget. Mutually exclusive
264
+ * with task_budget_id — the backend refuses a body naming both.
265
+ */
266
+ sub_budget_id: z.string().min(1).optional()
248
267
  },
249
268
  haven_pay_mcp_tool: {
250
269
  merchant_url: z.string().url(),
251
270
  tool_name: z.string().min(1),
252
271
  arguments: z.record(z.string(), z.unknown()).optional(),
253
272
  idempotency_key: z.string().optional(),
254
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
255
- idempotencyKey: z.string().optional()
273
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
274
+ idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
256
275
  },
257
276
  haven_quote_x402: {
258
277
  url: z.string().url(),
@@ -260,14 +279,18 @@ var toolSchemas = {
260
279
  headers: headersSchema,
261
280
  body: z.string().optional(),
262
281
  idempotency_key: z.string().optional(),
263
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
264
- idempotencyKey: z.string().optional()
282
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
283
+ idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
265
284
  },
266
285
  haven_pay_x402_quote: {
267
286
  quote: z.unknown(),
268
287
  idempotency_key: z.string().optional(),
269
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
270
- idempotencyKey: z.string().optional()
288
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
289
+ idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key"),
290
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
291
+ task_budget_id: z.string().min(1).optional(),
292
+ /** #3330: spend against an open sub-budget this agent holds (it is sub-agent B). Mutually exclusive with task_budget_id. */
293
+ sub_budget_id: z.string().min(1).optional()
271
294
  },
272
295
  haven_pay_x402: {
273
296
  url: z.string().url(),
@@ -275,8 +298,12 @@ var toolSchemas = {
275
298
  headers: headersSchema,
276
299
  body: z.string().optional(),
277
300
  idempotency_key: z.string().optional(),
278
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
279
- idempotencyKey: z.string().optional()
301
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
302
+ idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key"),
303
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
304
+ task_budget_id: z.string().min(1).optional(),
305
+ /** #3330: spend against an open sub-budget this agent holds (it is sub-agent B). Mutually exclusive with task_budget_id. */
306
+ sub_budget_id: z.string().min(1).optional()
280
307
  },
281
308
  haven_resume_x402_payment: {
282
309
  payment_id: z.string().optional(),
@@ -304,13 +331,65 @@ var toolSchemas = {
304
331
  haven_list_receipts: {
305
332
  limit: z.number().int().min(1).max(100).optional(),
306
333
  /** #3128: the previous page's next_cursor (a receipt id). */
307
- cursor: z.string().min(1).optional()
334
+ cursor: z.string().min(1).optional(),
335
+ /** #3423: drop each row's payload echoes; the default shape is unchanged. */
336
+ compact: z.boolean().optional().describe("true drops challengePayload, selectedPayment and protocolReceiptPayload from each row; omit for the full row.")
308
337
  },
309
338
  haven_verify_receipt: {
310
339
  receipt: z.unknown()
340
+ },
341
+ // #3329: a budget for one task that ends by itself — a short-lived child of
342
+ // the agent's own budget, capped and time-boxed independently of the period
343
+ // reset.
344
+ haven_open_task_budget: {
345
+ max_amount_human: z.string().min(1),
346
+ ttl_minutes: z.number().int().min(1).max(1440),
347
+ recipient: z.string().optional(),
348
+ label: z.string().max(120).optional(),
349
+ token: z.string().optional()
350
+ },
351
+ haven_close_task_budget: {
352
+ task_budget_id: z.string().min(1)
353
+ },
354
+ // #3329: relays a signature from the local signer — either the open/close
355
+ // signature for a task budget, or (schema parity with the hosted surface)
356
+ // a direct-payment signature by payment_id. Exactly one of task_budget_id /
357
+ // payment_id, never both or neither.
358
+ haven_submit: {
359
+ task_budget_id: z.string().min(1).optional(),
360
+ payment_id: z.string().min(1).optional(),
361
+ signature: z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string")
311
362
  }
312
363
  // #3101: keys survive on the type (see the hosted server's contracts.ts).
313
364
  };
365
+ var OPEN_TASK_BUDGET_DESCRIPTION = [
366
+ "Open a budget for one task that ends by itself: a spending cap, good for at most ttl_minutes,",
367
+ "reserved out of the agent's own budget and separate from its period reset.",
368
+ 'Pass max_amount_human (whole tokens, e.g. "5" for 5 USDC), ttl_minutes (1-1440), and optionally',
369
+ "recipient (pins every payment against this task budget to one address), label, and token",
370
+ '(defaults to USDC). Returns { task_budget, next_action: "sign", next_tool, next_arguments } \u2014',
371
+ "call next_tool with next_arguments EXACTLY as given to get a signature, then relay it with",
372
+ "haven_submit. Spending anything above the reserved amount, past the deadline, or to a",
373
+ "different recipient than the one pinned here is declined on the spot \u2014 nothing is queued.",
374
+ "A recipient pin is checked against where each payment first goes: haven_pay_x402 and",
375
+ "haven_pay_x402_quote first move funds into the agent's own wallet, so a budget pinned to a",
376
+ "merchant is declined there \u2014 pin only budgets meant for haven_send."
377
+ ].join(" ");
378
+ var CLOSE_TASK_BUDGET_DESCRIPTION = [
379
+ "End a task budget early, before its deadline, releasing whatever of its cap was unspent back",
380
+ "to the agent's own budget. Pass task_budget_id. If the budget was never signed (still pending),",
381
+ 'this ends it immediately with { task_budget, status: "closed" } and nothing was ever reserved',
382
+ 'on-chain. Otherwise it returns a signature request: { task_budget, next_action: "sign",',
383
+ "next_tool, next_arguments } \u2014 call next_tool with next_arguments EXACTLY as given, then relay",
384
+ "the signature with haven_submit. A task budget past its own deadline closes immediately, the",
385
+ "same as the pending case."
386
+ ].join(" ");
387
+ var SUBMIT_DESCRIPTION = [
388
+ "Relay a signature from the local signer. Pass exactly one of task_budget_id (from",
389
+ "haven_open_task_budget or haven_close_task_budget) or payment_id \u2014 never both, never neither \u2014",
390
+ "plus signature. For a task budget this opens or closes it on-chain and returns { task_budget,",
391
+ 'status }; a close in progress may return { task_budget, status: "closed", close_tx_hash }.'
392
+ ].join(" ");
314
393
  var toolDescriptions = {
315
394
  haven_send: composeDescription(toolDescriptions$1.send),
316
395
  haven_pay_mcp_tool: composeDescription(toolDescriptions$1.payMcpTool),
@@ -326,7 +405,10 @@ var toolDescriptions = {
326
405
  haven_discover_tools: composeDescription(toolDescriptions$1.discoverTools),
327
406
  haven_submit_catalog_entry: composeDescription(toolDescriptions$1.submitCatalogEntry),
328
407
  haven_list_receipts: composeDescription(toolDescriptions$1.listReceipts),
329
- haven_verify_receipt: composeDescription(toolDescriptions$1.verifyReceipt)
408
+ haven_verify_receipt: composeDescription(toolDescriptions$1.verifyReceipt),
409
+ haven_open_task_budget: OPEN_TASK_BUDGET_DESCRIPTION,
410
+ haven_close_task_budget: CLOSE_TASK_BUDGET_DESCRIPTION,
411
+ haven_submit: SUBMIT_DESCRIPTION
330
412
  };
331
413
  function createToolHandlers(haven) {
332
414
  return {
@@ -340,9 +422,15 @@ function createToolHandlers(haven) {
340
422
  token: args.asset,
341
423
  amount: args.amount,
342
424
  to: args.recipient,
343
- // #1207: was accepted by the schema but silently dropped — now
344
- // carried to the backend's replay contract.
345
- idempotencyKey: args.idempotencyKey
425
+ // #1207/#3411: was accepted by the schema but silently dropped — now
426
+ // carried to the backend's replay contract via the resolved carrier.
427
+ idempotencyKey: args.resolvedIdempotencyKey,
428
+ // #3329: spend against an open task budget instead of the
429
+ // agent's period budget, when the caller names one.
430
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {},
431
+ // #3330: spend through a sub-budget this agent holds (it is
432
+ // sub-agent B). Exactly one of the two ids, never both.
433
+ ...typeof args.sub_budget_id === "string" ? { subBudgetId: args.sub_budget_id } : {}
346
434
  });
347
435
  return {
348
436
  payment_id: result.paymentId,
@@ -380,7 +468,7 @@ function createToolHandlers(haven) {
380
468
  body: JSON.stringify(envelope)
381
469
  };
382
470
  let merchantUrl = args.merchant_url;
383
- const idempotencyKey = args.idempotencyKey;
471
+ const idempotencyKey = args.resolvedIdempotencyKey;
384
472
  const attempt = () => haven.fetch(merchantUrl, init, { idempotencyKey });
385
473
  let response = await attempt();
386
474
  if (!response.ok) {
@@ -414,7 +502,7 @@ function createToolHandlers(haven) {
414
502
  if ("success" in pf) return pf;
415
503
  const { args, warnings } = pf;
416
504
  try {
417
- const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
505
+ const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.resolvedIdempotencyKey });
418
506
  return warnings.length > 0 ? { success: true, data, warnings } : { success: true, data };
419
507
  } catch (err) {
420
508
  return normalizeError(err);
@@ -440,7 +528,11 @@ function createToolHandlers(haven) {
440
528
  );
441
529
  }
442
530
  return runTool(async () => {
443
- const response = await haven.payX402Quote(args.quote, { idempotencyKey: args.idempotencyKey });
531
+ const response = await haven.payX402Quote(args.quote, {
532
+ idempotencyKey: args.resolvedIdempotencyKey,
533
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {},
534
+ ...typeof args.sub_budget_id === "string" ? { subBudgetId: args.sub_budget_id } : {}
535
+ });
444
536
  return responsePayload(response);
445
537
  }, warnings);
446
538
  },
@@ -449,7 +541,11 @@ function createToolHandlers(haven) {
449
541
  if ("success" in pf) return pf;
450
542
  const { args, warnings } = pf;
451
543
  return runTool(async () => {
452
- const response = await haven.fetch(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
544
+ const response = await haven.fetch(args.url, requestInit(args), {
545
+ idempotencyKey: args.resolvedIdempotencyKey,
546
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {},
547
+ ...typeof args.sub_budget_id === "string" ? { subBudgetId: args.sub_budget_id } : {}
548
+ });
453
549
  return responsePayload(response);
454
550
  }, warnings);
455
551
  },
@@ -545,11 +641,82 @@ function createToolHandlers(haven) {
545
641
  },
546
642
  haven_list_receipts: async (input) => {
547
643
  const args = objectInput("haven_list_receipts", input);
548
- return runTool(async () => haven.listReceiptsPage({ limit: args.limit, cursor: args.cursor }));
644
+ return runTool(
645
+ async () => haven.listReceiptsPage({ limit: args.limit, cursor: args.cursor, compact: args.compact === true })
646
+ );
549
647
  },
550
648
  haven_verify_receipt: async (input) => {
551
649
  const args = objectInput("haven_verify_receipt", input);
552
650
  return runTool(async () => verifyPaymentReceipt(args.receipt));
651
+ },
652
+ haven_open_task_budget: async (input) => {
653
+ const args = objectInput("haven_open_task_budget", input);
654
+ return runTool(async () => {
655
+ const token = await resolveTaskBudgetToken(haven, typeof args.token === "string" ? args.token : void 0);
656
+ const maxAmountAtomic = humanToAtomicOrNull(args.max_amount_human, token.decimals);
657
+ if (maxAmountAtomic === null) {
658
+ throw new HavenApiError(
659
+ `max_amount_human ("${args.max_amount_human}") is not a valid decimal amount for ${token.symbol} (${token.decimals} decimal places). Nothing was reserved.`,
660
+ 400
661
+ );
662
+ }
663
+ const result = await haven.openTaskBudget({
664
+ tokenAddress: token.address,
665
+ maxAmountAtomic,
666
+ ttlSeconds: Number(args.ttl_minutes) * 60,
667
+ recipientAddress: typeof args.recipient === "string" ? args.recipient : void 0,
668
+ label: typeof args.label === "string" ? args.label : void 0
669
+ });
670
+ return {
671
+ task_budget: result.taskBudget,
672
+ next_action: "sign",
673
+ next_tool: "mcp__haven-signer__haven_sign",
674
+ next_arguments: { task_budget_id: result.taskBudget.id },
675
+ signer_compatibility: taskSignerCompatibilityNotice()
676
+ };
677
+ });
678
+ },
679
+ haven_close_task_budget: async (input) => {
680
+ const args = objectInput("haven_close_task_budget", input);
681
+ return runTool(async () => {
682
+ const result = await haven.closeTaskBudget(args.task_budget_id);
683
+ if (result.status === "closed") {
684
+ return { task_budget: result.taskBudget, status: "closed" };
685
+ }
686
+ return {
687
+ task_budget: result.taskBudget,
688
+ next_action: "sign",
689
+ next_tool: "mcp__haven-signer__haven_sign",
690
+ next_arguments: { task_budget_id: args.task_budget_id },
691
+ signer_compatibility: taskSignerCompatibilityNotice()
692
+ };
693
+ });
694
+ },
695
+ haven_submit: async (input) => {
696
+ const args = objectInput("haven_submit", input);
697
+ const hasTaskBudget = typeof args.task_budget_id === "string" && args.task_budget_id.length > 0;
698
+ const hasPayment = typeof args.payment_id === "string" && args.payment_id.length > 0;
699
+ if (hasTaskBudget === hasPayment) {
700
+ return {
701
+ success: false,
702
+ code: "INVALID_INPUT",
703
+ message: "haven_submit takes exactly one of task_budget_id or payment_id, never both or neither. Nothing was relayed."
704
+ };
705
+ }
706
+ return runTool(async () => {
707
+ if (hasTaskBudget) {
708
+ const result = await haven.submitTaskBudget(args.task_budget_id, args.signature);
709
+ return {
710
+ task_budget: result.taskBudget,
711
+ status: result.status,
712
+ ...result.closeTxHash !== void 0 ? { close_tx_hash: result.closeTxHash } : {}
713
+ };
714
+ }
715
+ throw new HavenApiError(
716
+ "haven_submit with payment_id is not supported on the local MCP surface: haven_send signs and submits a direct payment in one call, so there is no separate relay step. Use haven_submit with task_budget_id to relay a task-budget open/close signature.",
717
+ 400
718
+ );
719
+ });
553
720
  }
554
721
  };
555
722
  async function resumeState(args, rail) {
@@ -578,28 +745,20 @@ function preflight(name, input) {
578
745
  }
579
746
  const idem = resolveIdempotencyKey(args);
580
747
  if ("success" in idem) return idem;
581
- if (idem.key !== void 0) args = { ...args, idempotencyKey: idem.key };
748
+ if (idem.key !== void 0) args = { ...args, resolvedIdempotencyKey: idem.key };
582
749
  return { args, warnings: idem.warnings };
583
750
  }
584
751
  function resolveIdempotencyKey(args) {
585
752
  const modern = typeof args.idempotency_key === "string" ? args.idempotency_key : void 0;
586
753
  const legacy = typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0;
587
- if (modern !== void 0 && legacy !== void 0 && modern !== legacy) {
754
+ if (legacy !== void 0) {
588
755
  return {
589
756
  success: false,
590
- code: "AMBIGUOUS_IDEMPOTENCY_KEY",
591
- message: "Both idempotency_key and idempotencyKey were sent with different values. Nothing was contacted or spent. Send exactly one \u2014 idempotency_key is the current spelling; idempotencyKey is deprecated and will be removed."
757
+ code: "IDEMPOTENCY_KEY_RENAMED",
758
+ message: "idempotencyKey has been renamed to idempotency_key and is no longer accepted. Send idempotency_key instead. Nothing was contacted or spent."
592
759
  };
593
760
  }
594
761
  if (modern !== void 0) return { key: modern, warnings: [] };
595
- if (legacy !== void 0) {
596
- return {
597
- key: legacy,
598
- warnings: [
599
- "idempotencyKey is deprecated and will be removed in a future release of @haven_ai/mcp. Send idempotency_key instead \u2014 it is the spelling the hosted Haven MCP surface and every other Haven wire contract use."
600
- ]
601
- };
602
- }
603
762
  return { warnings: [] };
604
763
  }
605
764
  function buildMcpToolsCallEnvelope(toolName, args) {
@@ -641,6 +800,28 @@ async function responsePayload(response) {
641
800
  body: parseMaybeJson(text)
642
801
  };
643
802
  }
803
+ async function resolveTaskBudgetToken(haven, symbolOrAddress) {
804
+ const wanted = (symbolOrAddress ?? "USDC").toLowerCase();
805
+ const { allowances } = await haven.getAllowances();
806
+ const isAddress = /^0x[0-9a-fA-F]{40}$/.test(wanted);
807
+ const match = isAddress ? allowances.find((a) => a.tokenAddress.toLowerCase() === wanted) : allowances.find((a) => (a.tokenSymbol ?? "").toLowerCase() === wanted);
808
+ if (!match) {
809
+ const known = allowances.map((a) => `${a.tokenSymbol} (${a.tokenAddress})`);
810
+ throw new HavenApiError(
811
+ `token "${symbolOrAddress ?? "USDC"}" is not the symbol or address of any allowance this agent holds${known.length > 0 ? ` (it holds: ${known.join(", ")})` : " (it holds none)"}. Nothing was reserved.`,
812
+ 400
813
+ );
814
+ }
815
+ const resolved = resolveTokenFromAddress(match.tokenAddress);
816
+ return { address: match.tokenAddress, symbol: match.tokenSymbol, decimals: resolved?.decimals ?? 6 };
817
+ }
818
+ function humanToAtomicOrNull(human, decimals) {
819
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(human)) return null;
820
+ const [whole, frac = ""] = human.split(".");
821
+ if (frac.length > decimals) return null;
822
+ const atomic = BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(frac.padEnd(decimals, "0") || "0");
823
+ return atomic.toString();
824
+ }
644
825
  function parseMaybeJson(text) {
645
826
  if (!text) return null;
646
827
  try {
@@ -808,7 +989,7 @@ function renderConsentBlock(input, hash) {
808
989
  lines.push("signed delegation, not by Haven):");
809
990
  for (const a of input.allowanceSummary) {
810
991
  const reset = a.resetMinutes ? ` per ${a.resetMinutes} min` : " (no reset)";
811
- lines.push(` \u2022 up to ${a.amount} ${a.token}${reset}`);
992
+ lines.push(` \u2022 up to ${describeBudgetAmount(a, input.chainId)}${reset}`);
812
993
  }
813
994
  }
814
995
  lines.push("");
@@ -905,7 +1086,14 @@ async function consentInputFromClient(haven, seed, toolNames) {
905
1086
  }
906
1087
  const liveAllowanceSummary = list.map((a) => ({
907
1088
  token: a.tokenSymbol ?? "UNKNOWN",
908
- amount: a.onchain?.amount ?? a.configuredAmount ?? "0",
1089
+ // #3410: `onchain.amount` is the atomic budget (`budget_atomic` on the
1090
+ // backend). The old `a.configuredAmount` fallback here was unreachable
1091
+ // — `onchain` is required in the SDK types and dereferenced
1092
+ // unconditionally, so a response without it throws into the seed path —
1093
+ // and it was the one HUMAN-decimal value in this chain; a human number
1094
+ // must never reach an atomic field. '0' keeps the (defensive) shape for
1095
+ // a malformed onchain object without inventing a unit.
1096
+ amount: a.onchain?.amount ?? "0",
909
1097
  resetMinutes: typeof a.onchain?.resetTimeMin === "number" ? a.onchain.resetTimeMin : typeof a.resetPeriodMin === "number" ? a.resetPeriodMin : null
910
1098
  }));
911
1099
  allowanceSummary = liveAllowanceSummary;
@@ -928,6 +1116,11 @@ function isAllowanceSummary(value) {
928
1116
  function derivePrefix(apiKey) {
929
1117
  return apiKey.slice(0, 12);
930
1118
  }
1119
+ function describeBudgetAmount(a, chainId) {
1120
+ const token = chainId === void 0 ? null : resolveTokenBySymbol(chainId, a.token);
1121
+ if (!token) return `${a.amount} ${a.token} (atomic units)`;
1122
+ return `${formatTokenAmount(a.amount, token.decimals)} ${a.token}`;
1123
+ }
931
1124
  function registeredToolNames() {
932
1125
  return Object.keys(toolSchemas);
933
1126
  }
@@ -952,7 +1145,7 @@ async function resolveHavenClient(options = {}) {
952
1145
  return { client, credentials };
953
1146
  }
954
1147
  var MCP_NAME = "@haven_ai/mcp";
955
- var MCP_VERSION = "0.5.0-alpha.1";
1148
+ var MCP_VERSION = "0.7.0-alpha.0";
956
1149
  var MCP_INSTRUCTIONS = [
957
1150
  "Haven local MCP server: signs in-process with the delegate key it holds on",
958
1151
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",