@haven_ai/mcp 0.5.0-alpha.1 → 0.6.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, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, verifyPaymentReceipt, discoverMerchantMcpUrl, sameUrl, resolveTokenFromAddress } 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';
@@ -244,7 +244,9 @@ var toolSchemas = {
244
244
  amount: z.string().min(1),
245
245
  idempotency_key: z.string().optional(),
246
246
  /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
247
- idempotencyKey: z.string().optional()
247
+ idempotencyKey: z.string().optional(),
248
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
249
+ task_budget_id: z.string().min(1).optional()
248
250
  },
249
251
  haven_pay_mcp_tool: {
250
252
  merchant_url: z.string().url(),
@@ -267,7 +269,9 @@ var toolSchemas = {
267
269
  quote: z.unknown(),
268
270
  idempotency_key: z.string().optional(),
269
271
  /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
270
- idempotencyKey: z.string().optional()
272
+ idempotencyKey: z.string().optional(),
273
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
274
+ task_budget_id: z.string().min(1).optional()
271
275
  },
272
276
  haven_pay_x402: {
273
277
  url: z.string().url(),
@@ -276,7 +280,9 @@ var toolSchemas = {
276
280
  body: z.string().optional(),
277
281
  idempotency_key: z.string().optional(),
278
282
  /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
279
- idempotencyKey: z.string().optional()
283
+ idempotencyKey: z.string().optional(),
284
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
285
+ task_budget_id: z.string().min(1).optional()
280
286
  },
281
287
  haven_resume_x402_payment: {
282
288
  payment_id: z.string().optional(),
@@ -308,9 +314,59 @@ var toolSchemas = {
308
314
  },
309
315
  haven_verify_receipt: {
310
316
  receipt: z.unknown()
317
+ },
318
+ // #3329: a budget for one task that ends by itself — a short-lived child of
319
+ // the agent's own budget, capped and time-boxed independently of the period
320
+ // reset.
321
+ haven_open_task_budget: {
322
+ max_amount_human: z.string().min(1),
323
+ ttl_minutes: z.number().int().min(1).max(1440),
324
+ recipient: z.string().optional(),
325
+ label: z.string().max(120).optional(),
326
+ token: z.string().optional()
327
+ },
328
+ haven_close_task_budget: {
329
+ task_budget_id: z.string().min(1)
330
+ },
331
+ // #3329: relays a signature from the local signer — either the open/close
332
+ // signature for a task budget, or (schema parity with the hosted surface)
333
+ // a direct-payment signature by payment_id. Exactly one of task_budget_id /
334
+ // payment_id, never both or neither.
335
+ haven_submit: {
336
+ task_budget_id: z.string().min(1).optional(),
337
+ payment_id: z.string().min(1).optional(),
338
+ signature: z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string")
311
339
  }
312
340
  // #3101: keys survive on the type (see the hosted server's contracts.ts).
313
341
  };
342
+ var OPEN_TASK_BUDGET_DESCRIPTION = [
343
+ "Open a budget for one task that ends by itself: a spending cap, good for at most ttl_minutes,",
344
+ "reserved out of the agent's own budget and separate from its period reset.",
345
+ 'Pass max_amount_human (whole tokens, e.g. "5" for 5 USDC), ttl_minutes (1-1440), and optionally',
346
+ "recipient (pins every payment against this task budget to one address), label, and token",
347
+ '(defaults to USDC). Returns { task_budget, next_action: "sign", next_tool, next_arguments } \u2014',
348
+ "call next_tool with next_arguments EXACTLY as given to get a signature, then relay it with",
349
+ "haven_submit. Spending anything above the reserved amount, past the deadline, or to a",
350
+ "different recipient than the one pinned here is declined on the spot \u2014 nothing is queued.",
351
+ "A recipient pin is checked against where each payment first goes: haven_pay_x402 and",
352
+ "haven_pay_x402_quote first move funds into the agent's own wallet, so a budget pinned to a",
353
+ "merchant is declined there \u2014 pin only budgets meant for haven_send."
354
+ ].join(" ");
355
+ var CLOSE_TASK_BUDGET_DESCRIPTION = [
356
+ "End a task budget early, before its deadline, releasing whatever of its cap was unspent back",
357
+ "to the agent's own budget. Pass task_budget_id. If the budget was never signed (still pending),",
358
+ 'this ends it immediately with { task_budget, status: "closed" } and nothing was ever reserved',
359
+ 'on-chain. Otherwise it returns a signature request: { task_budget, next_action: "sign",',
360
+ "next_tool, next_arguments } \u2014 call next_tool with next_arguments EXACTLY as given, then relay",
361
+ "the signature with haven_submit. A task budget past its own deadline closes immediately, the",
362
+ "same as the pending case."
363
+ ].join(" ");
364
+ var SUBMIT_DESCRIPTION = [
365
+ "Relay a signature from the local signer. Pass exactly one of task_budget_id (from",
366
+ "haven_open_task_budget or haven_close_task_budget) or payment_id \u2014 never both, never neither \u2014",
367
+ "plus signature. For a task budget this opens or closes it on-chain and returns { task_budget,",
368
+ 'status }; a close in progress may return { task_budget, status: "closed", close_tx_hash }.'
369
+ ].join(" ");
314
370
  var toolDescriptions = {
315
371
  haven_send: composeDescription(toolDescriptions$1.send),
316
372
  haven_pay_mcp_tool: composeDescription(toolDescriptions$1.payMcpTool),
@@ -326,7 +382,10 @@ var toolDescriptions = {
326
382
  haven_discover_tools: composeDescription(toolDescriptions$1.discoverTools),
327
383
  haven_submit_catalog_entry: composeDescription(toolDescriptions$1.submitCatalogEntry),
328
384
  haven_list_receipts: composeDescription(toolDescriptions$1.listReceipts),
329
- haven_verify_receipt: composeDescription(toolDescriptions$1.verifyReceipt)
385
+ haven_verify_receipt: composeDescription(toolDescriptions$1.verifyReceipt),
386
+ haven_open_task_budget: OPEN_TASK_BUDGET_DESCRIPTION,
387
+ haven_close_task_budget: CLOSE_TASK_BUDGET_DESCRIPTION,
388
+ haven_submit: SUBMIT_DESCRIPTION
330
389
  };
331
390
  function createToolHandlers(haven) {
332
391
  return {
@@ -342,7 +401,10 @@ function createToolHandlers(haven) {
342
401
  to: args.recipient,
343
402
  // #1207: was accepted by the schema but silently dropped — now
344
403
  // carried to the backend's replay contract.
345
- idempotencyKey: args.idempotencyKey
404
+ idempotencyKey: args.idempotencyKey,
405
+ // #3329: spend against an open task budget instead of the
406
+ // agent's period budget, when the caller names one.
407
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
346
408
  });
347
409
  return {
348
410
  payment_id: result.paymentId,
@@ -440,7 +502,10 @@ function createToolHandlers(haven) {
440
502
  );
441
503
  }
442
504
  return runTool(async () => {
443
- const response = await haven.payX402Quote(args.quote, { idempotencyKey: args.idempotencyKey });
505
+ const response = await haven.payX402Quote(args.quote, {
506
+ idempotencyKey: args.idempotencyKey,
507
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
508
+ });
444
509
  return responsePayload(response);
445
510
  }, warnings);
446
511
  },
@@ -449,7 +514,10 @@ function createToolHandlers(haven) {
449
514
  if ("success" in pf) return pf;
450
515
  const { args, warnings } = pf;
451
516
  return runTool(async () => {
452
- const response = await haven.fetch(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
517
+ const response = await haven.fetch(args.url, requestInit(args), {
518
+ idempotencyKey: args.idempotencyKey,
519
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
520
+ });
453
521
  return responsePayload(response);
454
522
  }, warnings);
455
523
  },
@@ -550,6 +618,73 @@ function createToolHandlers(haven) {
550
618
  haven_verify_receipt: async (input) => {
551
619
  const args = objectInput("haven_verify_receipt", input);
552
620
  return runTool(async () => verifyPaymentReceipt(args.receipt));
621
+ },
622
+ haven_open_task_budget: async (input) => {
623
+ const args = objectInput("haven_open_task_budget", input);
624
+ return runTool(async () => {
625
+ const token = await resolveTaskBudgetToken(haven, typeof args.token === "string" ? args.token : void 0);
626
+ const maxAmountAtomic = humanToAtomicOrNull(args.max_amount_human, token.decimals);
627
+ if (maxAmountAtomic === null) {
628
+ throw new HavenApiError(
629
+ `max_amount_human ("${args.max_amount_human}") is not a valid decimal amount for ${token.symbol} (${token.decimals} decimal places). Nothing was reserved.`,
630
+ 400
631
+ );
632
+ }
633
+ const result = await haven.openTaskBudget({
634
+ tokenAddress: token.address,
635
+ maxAmountAtomic,
636
+ ttlSeconds: Number(args.ttl_minutes) * 60,
637
+ recipientAddress: typeof args.recipient === "string" ? args.recipient : void 0,
638
+ label: typeof args.label === "string" ? args.label : void 0
639
+ });
640
+ return {
641
+ task_budget: result.taskBudget,
642
+ next_action: "sign",
643
+ next_tool: "mcp__haven-signer__haven_sign",
644
+ next_arguments: { task_budget_id: result.taskBudget.id }
645
+ };
646
+ });
647
+ },
648
+ haven_close_task_budget: async (input) => {
649
+ const args = objectInput("haven_close_task_budget", input);
650
+ return runTool(async () => {
651
+ const result = await haven.closeTaskBudget(args.task_budget_id);
652
+ if (result.status === "closed") {
653
+ return { task_budget: result.taskBudget, status: "closed" };
654
+ }
655
+ return {
656
+ task_budget: result.taskBudget,
657
+ next_action: "sign",
658
+ next_tool: "mcp__haven-signer__haven_sign",
659
+ next_arguments: { task_budget_id: args.task_budget_id }
660
+ };
661
+ });
662
+ },
663
+ haven_submit: async (input) => {
664
+ const args = objectInput("haven_submit", input);
665
+ const hasTaskBudget = typeof args.task_budget_id === "string" && args.task_budget_id.length > 0;
666
+ const hasPayment = typeof args.payment_id === "string" && args.payment_id.length > 0;
667
+ if (hasTaskBudget === hasPayment) {
668
+ return {
669
+ success: false,
670
+ code: "INVALID_INPUT",
671
+ message: "haven_submit takes exactly one of task_budget_id or payment_id, never both or neither. Nothing was relayed."
672
+ };
673
+ }
674
+ return runTool(async () => {
675
+ if (hasTaskBudget) {
676
+ const result = await haven.submitTaskBudget(args.task_budget_id, args.signature);
677
+ return {
678
+ task_budget: result.taskBudget,
679
+ status: result.status,
680
+ ...result.closeTxHash !== void 0 ? { close_tx_hash: result.closeTxHash } : {}
681
+ };
682
+ }
683
+ throw new HavenApiError(
684
+ "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.",
685
+ 400
686
+ );
687
+ });
553
688
  }
554
689
  };
555
690
  async function resumeState(args, rail) {
@@ -641,6 +776,28 @@ async function responsePayload(response) {
641
776
  body: parseMaybeJson(text)
642
777
  };
643
778
  }
779
+ async function resolveTaskBudgetToken(haven, symbolOrAddress) {
780
+ const wanted = (symbolOrAddress ?? "USDC").toLowerCase();
781
+ const { allowances } = await haven.getAllowances();
782
+ const isAddress = /^0x[0-9a-fA-F]{40}$/.test(wanted);
783
+ const match = isAddress ? allowances.find((a) => a.tokenAddress.toLowerCase() === wanted) : allowances.find((a) => (a.tokenSymbol ?? "").toLowerCase() === wanted);
784
+ if (!match) {
785
+ const known = allowances.map((a) => `${a.tokenSymbol} (${a.tokenAddress})`);
786
+ throw new HavenApiError(
787
+ `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.`,
788
+ 400
789
+ );
790
+ }
791
+ const resolved = resolveTokenFromAddress(match.tokenAddress);
792
+ return { address: match.tokenAddress, symbol: match.tokenSymbol, decimals: resolved?.decimals ?? 6 };
793
+ }
794
+ function humanToAtomicOrNull(human, decimals) {
795
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(human)) return null;
796
+ const [whole, frac = ""] = human.split(".");
797
+ if (frac.length > decimals) return null;
798
+ const atomic = BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(frac.padEnd(decimals, "0") || "0");
799
+ return atomic.toString();
800
+ }
644
801
  function parseMaybeJson(text) {
645
802
  if (!text) return null;
646
803
  try {
@@ -952,7 +1109,7 @@ async function resolveHavenClient(options = {}) {
952
1109
  return { client, credentials };
953
1110
  }
954
1111
  var MCP_NAME = "@haven_ai/mcp";
955
- var MCP_VERSION = "0.5.0-alpha.1";
1112
+ var MCP_VERSION = "0.6.0-alpha.0";
956
1113
  var MCP_INSTRUCTIONS = [
957
1114
  "Haven local MCP server: signs in-process with the delegate key it holds on",
958
1115
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",