@haven_ai/mcp 0.4.0-alpha.0 → 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/README.md CHANGED
@@ -117,6 +117,9 @@ Environment variable form:
117
117
  - `haven_discover_tools`
118
118
  - `haven_submit_catalog_entry`
119
119
  - `haven_list_receipts`
120
+ - `haven_open_task_budget` (#3329)
121
+ - `haven_close_task_budget` (#3329)
122
+ - `haven_submit` (#3329 — relays a local signer signature by `task_budget_id`; a `payment_id` is refused on this runtime, which signs and submits payments inline)
120
123
 
121
124
  ### `idempotencyKey` is deprecated — send `idempotency_key`
122
125
 
package/dist/cli.cjs CHANGED
@@ -246,7 +246,9 @@ var toolSchemas = {
246
246
  amount: v3.z.string().min(1),
247
247
  idempotency_key: v3.z.string().optional(),
248
248
  /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
249
- idempotencyKey: v3.z.string().optional()
249
+ idempotencyKey: v3.z.string().optional(),
250
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
251
+ task_budget_id: v3.z.string().min(1).optional()
250
252
  },
251
253
  haven_pay_mcp_tool: {
252
254
  merchant_url: v3.z.string().url(),
@@ -269,7 +271,9 @@ var toolSchemas = {
269
271
  quote: v3.z.unknown(),
270
272
  idempotency_key: v3.z.string().optional(),
271
273
  /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
272
- idempotencyKey: v3.z.string().optional()
274
+ idempotencyKey: v3.z.string().optional(),
275
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
276
+ task_budget_id: v3.z.string().min(1).optional()
273
277
  },
274
278
  haven_pay_x402: {
275
279
  url: v3.z.string().url(),
@@ -278,7 +282,9 @@ var toolSchemas = {
278
282
  body: v3.z.string().optional(),
279
283
  idempotency_key: v3.z.string().optional(),
280
284
  /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
281
- idempotencyKey: v3.z.string().optional()
285
+ idempotencyKey: v3.z.string().optional(),
286
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
287
+ task_budget_id: v3.z.string().min(1).optional()
282
288
  },
283
289
  haven_resume_x402_payment: {
284
290
  payment_id: v3.z.string().optional(),
@@ -310,9 +316,59 @@ var toolSchemas = {
310
316
  },
311
317
  haven_verify_receipt: {
312
318
  receipt: v3.z.unknown()
319
+ },
320
+ // #3329: a budget for one task that ends by itself — a short-lived child of
321
+ // the agent's own budget, capped and time-boxed independently of the period
322
+ // reset.
323
+ haven_open_task_budget: {
324
+ max_amount_human: v3.z.string().min(1),
325
+ ttl_minutes: v3.z.number().int().min(1).max(1440),
326
+ recipient: v3.z.string().optional(),
327
+ label: v3.z.string().max(120).optional(),
328
+ token: v3.z.string().optional()
329
+ },
330
+ haven_close_task_budget: {
331
+ task_budget_id: v3.z.string().min(1)
332
+ },
333
+ // #3329: relays a signature from the local signer — either the open/close
334
+ // signature for a task budget, or (schema parity with the hosted surface)
335
+ // a direct-payment signature by payment_id. Exactly one of task_budget_id /
336
+ // payment_id, never both or neither.
337
+ haven_submit: {
338
+ task_budget_id: v3.z.string().min(1).optional(),
339
+ payment_id: v3.z.string().min(1).optional(),
340
+ signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string")
313
341
  }
314
342
  // #3101: keys survive on the type (see the hosted server's contracts.ts).
315
343
  };
344
+ var OPEN_TASK_BUDGET_DESCRIPTION = [
345
+ "Open a budget for one task that ends by itself: a spending cap, good for at most ttl_minutes,",
346
+ "reserved out of the agent's own budget and separate from its period reset.",
347
+ 'Pass max_amount_human (whole tokens, e.g. "5" for 5 USDC), ttl_minutes (1-1440), and optionally',
348
+ "recipient (pins every payment against this task budget to one address), label, and token",
349
+ '(defaults to USDC). Returns { task_budget, next_action: "sign", next_tool, next_arguments } \u2014',
350
+ "call next_tool with next_arguments EXACTLY as given to get a signature, then relay it with",
351
+ "haven_submit. Spending anything above the reserved amount, past the deadline, or to a",
352
+ "different recipient than the one pinned here is declined on the spot \u2014 nothing is queued.",
353
+ "A recipient pin is checked against where each payment first goes: haven_pay_x402 and",
354
+ "haven_pay_x402_quote first move funds into the agent's own wallet, so a budget pinned to a",
355
+ "merchant is declined there \u2014 pin only budgets meant for haven_send."
356
+ ].join(" ");
357
+ var CLOSE_TASK_BUDGET_DESCRIPTION = [
358
+ "End a task budget early, before its deadline, releasing whatever of its cap was unspent back",
359
+ "to the agent's own budget. Pass task_budget_id. If the budget was never signed (still pending),",
360
+ 'this ends it immediately with { task_budget, status: "closed" } and nothing was ever reserved',
361
+ 'on-chain. Otherwise it returns a signature request: { task_budget, next_action: "sign",',
362
+ "next_tool, next_arguments } \u2014 call next_tool with next_arguments EXACTLY as given, then relay",
363
+ "the signature with haven_submit. A task budget past its own deadline closes immediately, the",
364
+ "same as the pending case."
365
+ ].join(" ");
366
+ var SUBMIT_DESCRIPTION = [
367
+ "Relay a signature from the local signer. Pass exactly one of task_budget_id (from",
368
+ "haven_open_task_budget or haven_close_task_budget) or payment_id \u2014 never both, never neither \u2014",
369
+ "plus signature. For a task budget this opens or closes it on-chain and returns { task_budget,",
370
+ 'status }; a close in progress may return { task_budget, status: "closed", close_tx_hash }.'
371
+ ].join(" ");
316
372
  var toolDescriptions = {
317
373
  haven_send: sdk.composeDescription(sdk.toolDescriptions.send),
318
374
  haven_pay_mcp_tool: sdk.composeDescription(sdk.toolDescriptions.payMcpTool),
@@ -328,7 +384,10 @@ var toolDescriptions = {
328
384
  haven_discover_tools: sdk.composeDescription(sdk.toolDescriptions.discoverTools),
329
385
  haven_submit_catalog_entry: sdk.composeDescription(sdk.toolDescriptions.submitCatalogEntry),
330
386
  haven_list_receipts: sdk.composeDescription(sdk.toolDescriptions.listReceipts),
331
- haven_verify_receipt: sdk.composeDescription(sdk.toolDescriptions.verifyReceipt)
387
+ haven_verify_receipt: sdk.composeDescription(sdk.toolDescriptions.verifyReceipt),
388
+ haven_open_task_budget: OPEN_TASK_BUDGET_DESCRIPTION,
389
+ haven_close_task_budget: CLOSE_TASK_BUDGET_DESCRIPTION,
390
+ haven_submit: SUBMIT_DESCRIPTION
332
391
  };
333
392
  function createToolHandlers(haven) {
334
393
  return {
@@ -344,7 +403,10 @@ function createToolHandlers(haven) {
344
403
  to: args.recipient,
345
404
  // #1207: was accepted by the schema but silently dropped — now
346
405
  // carried to the backend's replay contract.
347
- idempotencyKey: args.idempotencyKey
406
+ idempotencyKey: args.idempotencyKey,
407
+ // #3329: spend against an open task budget instead of the
408
+ // agent's period budget, when the caller names one.
409
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
348
410
  });
349
411
  return {
350
412
  payment_id: result.paymentId,
@@ -442,7 +504,10 @@ function createToolHandlers(haven) {
442
504
  );
443
505
  }
444
506
  return runTool(async () => {
445
- const response = await haven.payX402Quote(args.quote, { idempotencyKey: args.idempotencyKey });
507
+ const response = await haven.payX402Quote(args.quote, {
508
+ idempotencyKey: args.idempotencyKey,
509
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
510
+ });
446
511
  return responsePayload(response);
447
512
  }, warnings);
448
513
  },
@@ -451,7 +516,10 @@ function createToolHandlers(haven) {
451
516
  if ("success" in pf) return pf;
452
517
  const { args, warnings } = pf;
453
518
  return runTool(async () => {
454
- const response = await haven.fetch(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
519
+ const response = await haven.fetch(args.url, requestInit(args), {
520
+ idempotencyKey: args.idempotencyKey,
521
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
522
+ });
455
523
  return responsePayload(response);
456
524
  }, warnings);
457
525
  },
@@ -552,6 +620,73 @@ function createToolHandlers(haven) {
552
620
  haven_verify_receipt: async (input) => {
553
621
  const args = objectInput("haven_verify_receipt", input);
554
622
  return runTool(async () => sdk.verifyPaymentReceipt(args.receipt));
623
+ },
624
+ haven_open_task_budget: async (input) => {
625
+ const args = objectInput("haven_open_task_budget", input);
626
+ return runTool(async () => {
627
+ const token = await resolveTaskBudgetToken(haven, typeof args.token === "string" ? args.token : void 0);
628
+ const maxAmountAtomic = humanToAtomicOrNull(args.max_amount_human, token.decimals);
629
+ if (maxAmountAtomic === null) {
630
+ throw new sdk.HavenApiError(
631
+ `max_amount_human ("${args.max_amount_human}") is not a valid decimal amount for ${token.symbol} (${token.decimals} decimal places). Nothing was reserved.`,
632
+ 400
633
+ );
634
+ }
635
+ const result = await haven.openTaskBudget({
636
+ tokenAddress: token.address,
637
+ maxAmountAtomic,
638
+ ttlSeconds: Number(args.ttl_minutes) * 60,
639
+ recipientAddress: typeof args.recipient === "string" ? args.recipient : void 0,
640
+ label: typeof args.label === "string" ? args.label : void 0
641
+ });
642
+ return {
643
+ task_budget: result.taskBudget,
644
+ next_action: "sign",
645
+ next_tool: "mcp__haven-signer__haven_sign",
646
+ next_arguments: { task_budget_id: result.taskBudget.id }
647
+ };
648
+ });
649
+ },
650
+ haven_close_task_budget: async (input) => {
651
+ const args = objectInput("haven_close_task_budget", input);
652
+ return runTool(async () => {
653
+ const result = await haven.closeTaskBudget(args.task_budget_id);
654
+ if (result.status === "closed") {
655
+ return { task_budget: result.taskBudget, status: "closed" };
656
+ }
657
+ return {
658
+ task_budget: result.taskBudget,
659
+ next_action: "sign",
660
+ next_tool: "mcp__haven-signer__haven_sign",
661
+ next_arguments: { task_budget_id: args.task_budget_id }
662
+ };
663
+ });
664
+ },
665
+ haven_submit: async (input) => {
666
+ const args = objectInput("haven_submit", input);
667
+ const hasTaskBudget = typeof args.task_budget_id === "string" && args.task_budget_id.length > 0;
668
+ const hasPayment = typeof args.payment_id === "string" && args.payment_id.length > 0;
669
+ if (hasTaskBudget === hasPayment) {
670
+ return {
671
+ success: false,
672
+ code: "INVALID_INPUT",
673
+ message: "haven_submit takes exactly one of task_budget_id or payment_id, never both or neither. Nothing was relayed."
674
+ };
675
+ }
676
+ return runTool(async () => {
677
+ if (hasTaskBudget) {
678
+ const result = await haven.submitTaskBudget(args.task_budget_id, args.signature);
679
+ return {
680
+ task_budget: result.taskBudget,
681
+ status: result.status,
682
+ ...result.closeTxHash !== void 0 ? { close_tx_hash: result.closeTxHash } : {}
683
+ };
684
+ }
685
+ throw new sdk.HavenApiError(
686
+ "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.",
687
+ 400
688
+ );
689
+ });
555
690
  }
556
691
  };
557
692
  async function resumeState(args, rail) {
@@ -643,6 +778,28 @@ async function responsePayload(response) {
643
778
  body: parseMaybeJson(text)
644
779
  };
645
780
  }
781
+ async function resolveTaskBudgetToken(haven, symbolOrAddress) {
782
+ const wanted = (symbolOrAddress ?? "USDC").toLowerCase();
783
+ const { allowances } = await haven.getAllowances();
784
+ const isAddress = /^0x[0-9a-fA-F]{40}$/.test(wanted);
785
+ const match = isAddress ? allowances.find((a) => a.tokenAddress.toLowerCase() === wanted) : allowances.find((a) => (a.tokenSymbol ?? "").toLowerCase() === wanted);
786
+ if (!match) {
787
+ const known = allowances.map((a) => `${a.tokenSymbol} (${a.tokenAddress})`);
788
+ throw new sdk.HavenApiError(
789
+ `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.`,
790
+ 400
791
+ );
792
+ }
793
+ const resolved = sdk.resolveTokenFromAddress(match.tokenAddress);
794
+ return { address: match.tokenAddress, symbol: match.tokenSymbol, decimals: resolved?.decimals ?? 6 };
795
+ }
796
+ function humanToAtomicOrNull(human, decimals) {
797
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(human)) return null;
798
+ const [whole, frac = ""] = human.split(".");
799
+ if (frac.length > decimals) return null;
800
+ const atomic = BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(frac.padEnd(decimals, "0") || "0");
801
+ return atomic.toString();
802
+ }
646
803
  function parseMaybeJson(text) {
647
804
  if (!text) return null;
648
805
  try {
@@ -732,6 +889,9 @@ function normalizeError(err) {
732
889
  phase: stringOrUndefined(body?.phase),
733
890
  nextAction: stringOrUndefined(body?.nextAction) ?? stringOrUndefined(body?.next_action) ?? sdk.AgentPaymentNextAction.StopAndTellUser,
734
891
  next_action: stringOrUndefined(body?.nextAction) ?? stringOrUndefined(body?.next_action) ?? sdk.AgentPaymentNextAction.StopAndTellUser,
892
+ // #3303: a backend refusal that already names why no tool follows (the
893
+ // 426 `client_outdated` does) keeps that reason at the top level.
894
+ ...typeof body?.next_tool_omitted_reason === "string" ? { next_tool_omitted_reason: body.next_tool_omitted_reason } : {},
735
895
  body: err.body
736
896
  };
737
897
  }
@@ -784,7 +944,7 @@ function renderConsentBlock(input, hash) {
784
944
  ];
785
945
  if (input.apiUrl) lines.push(`Haven API: ${input.apiUrl}`);
786
946
  if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
787
- if (input.accountAddress) lines.push(`Haven wallet (Safe): ${input.accountAddress}`);
947
+ if (input.accountAddress) lines.push(`Haven wallet: ${input.accountAddress}`);
788
948
  if (input.delegateAddress) lines.push(`Delegate (local signer): ${input.delegateAddress}`);
789
949
  if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
790
950
  lines.push("");
@@ -943,12 +1103,15 @@ async function resolveHavenClient(options = {}) {
943
1103
  const client = new sdk.HavenClient({
944
1104
  apiKey: credentials.apiKey,
945
1105
  delegateKey: credentials.delegateKey,
946
- baseUrl: credentials.apiUrl
1106
+ baseUrl: credentials.apiUrl,
1107
+ // #3303: name this package, not the SDK inside it, so the backend's
1108
+ // update hint and refusal speak about what the user actually installed.
1109
+ clientIdentity: sdk.havenClientIdentity(MCP_NAME, MCP_VERSION)
947
1110
  });
948
1111
  return { client, credentials };
949
1112
  }
950
1113
  var MCP_NAME = "@haven_ai/mcp";
951
- var MCP_VERSION = "0.4.0-alpha.0";
1114
+ var MCP_VERSION = "0.6.0-alpha.0";
952
1115
  var MCP_INSTRUCTIONS = [
953
1116
  "Haven local MCP server: signs in-process with the delegate key it holds on",
954
1117
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",
@@ -985,7 +1148,7 @@ function buildMcpServer(haven) {
985
1148
  toolSchemas[name],
986
1149
  async (args) => haven.withRequestContext(
987
1150
  { "X-Haven-MCP-Tool": name },
988
- async () => toMcpResult(await handlers[name](args))
1151
+ async () => toMcpResult(withClientUpdate(await handlers[name](args), haven.clientUpdate()))
989
1152
  )
990
1153
  );
991
1154
  }
@@ -1035,6 +1198,10 @@ async function runConsentGate(haven, credentials, options) {
1035
1198
  writeAck: options.writeAck
1036
1199
  });
1037
1200
  }
1201
+ function withClientUpdate(payload, hint) {
1202
+ if (!hint || payload.client_update) return payload;
1203
+ return { ...payload, client_update: hint };
1204
+ }
1038
1205
  function toMcpResult(payload) {
1039
1206
  return {
1040
1207
  isError: !payload.success,