@haven_ai/mcp 0.6.0-alpha.0 → 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, resolveTokenFromAddress } 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,18 +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"),
248
259
  /** #3329: spend against an open task budget instead of the agent's period budget. */
249
- task_budget_id: z.string().min(1).optional()
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()
250
267
  },
251
268
  haven_pay_mcp_tool: {
252
269
  merchant_url: z.string().url(),
253
270
  tool_name: z.string().min(1),
254
271
  arguments: z.record(z.string(), z.unknown()).optional(),
255
272
  idempotency_key: z.string().optional(),
256
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
257
- 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")
258
275
  },
259
276
  haven_quote_x402: {
260
277
  url: z.string().url(),
@@ -262,16 +279,18 @@ var toolSchemas = {
262
279
  headers: headersSchema,
263
280
  body: z.string().optional(),
264
281
  idempotency_key: z.string().optional(),
265
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
266
- 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")
267
284
  },
268
285
  haven_pay_x402_quote: {
269
286
  quote: z.unknown(),
270
287
  idempotency_key: z.string().optional(),
271
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
272
- 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"),
273
290
  /** #3329: spend against an open task budget instead of the agent's period budget. */
274
- task_budget_id: z.string().min(1).optional()
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()
275
294
  },
276
295
  haven_pay_x402: {
277
296
  url: z.string().url(),
@@ -279,10 +298,12 @@ var toolSchemas = {
279
298
  headers: headersSchema,
280
299
  body: z.string().optional(),
281
300
  idempotency_key: z.string().optional(),
282
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
283
- 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"),
284
303
  /** #3329: spend against an open task budget instead of the agent's period budget. */
285
- task_budget_id: z.string().min(1).optional()
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()
286
307
  },
287
308
  haven_resume_x402_payment: {
288
309
  payment_id: z.string().optional(),
@@ -310,7 +331,9 @@ var toolSchemas = {
310
331
  haven_list_receipts: {
311
332
  limit: z.number().int().min(1).max(100).optional(),
312
333
  /** #3128: the previous page's next_cursor (a receipt id). */
313
- 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.")
314
337
  },
315
338
  haven_verify_receipt: {
316
339
  receipt: z.unknown()
@@ -399,12 +422,15 @@ function createToolHandlers(haven) {
399
422
  token: args.asset,
400
423
  amount: args.amount,
401
424
  to: args.recipient,
402
- // #1207: was accepted by the schema but silently dropped — now
403
- // carried to the backend's replay contract.
404
- 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,
405
428
  // #3329: spend against an open task budget instead of the
406
429
  // agent's period budget, when the caller names one.
407
- ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
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 } : {}
408
434
  });
409
435
  return {
410
436
  payment_id: result.paymentId,
@@ -442,7 +468,7 @@ function createToolHandlers(haven) {
442
468
  body: JSON.stringify(envelope)
443
469
  };
444
470
  let merchantUrl = args.merchant_url;
445
- const idempotencyKey = args.idempotencyKey;
471
+ const idempotencyKey = args.resolvedIdempotencyKey;
446
472
  const attempt = () => haven.fetch(merchantUrl, init, { idempotencyKey });
447
473
  let response = await attempt();
448
474
  if (!response.ok) {
@@ -476,7 +502,7 @@ function createToolHandlers(haven) {
476
502
  if ("success" in pf) return pf;
477
503
  const { args, warnings } = pf;
478
504
  try {
479
- 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 });
480
506
  return warnings.length > 0 ? { success: true, data, warnings } : { success: true, data };
481
507
  } catch (err) {
482
508
  return normalizeError(err);
@@ -503,8 +529,9 @@ function createToolHandlers(haven) {
503
529
  }
504
530
  return runTool(async () => {
505
531
  const response = await haven.payX402Quote(args.quote, {
506
- idempotencyKey: args.idempotencyKey,
507
- ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {}
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 } : {}
508
535
  });
509
536
  return responsePayload(response);
510
537
  }, warnings);
@@ -515,8 +542,9 @@ function createToolHandlers(haven) {
515
542
  const { args, warnings } = pf;
516
543
  return runTool(async () => {
517
544
  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 } : {}
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 } : {}
520
548
  });
521
549
  return responsePayload(response);
522
550
  }, warnings);
@@ -613,7 +641,9 @@ function createToolHandlers(haven) {
613
641
  },
614
642
  haven_list_receipts: async (input) => {
615
643
  const args = objectInput("haven_list_receipts", input);
616
- 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
+ );
617
647
  },
618
648
  haven_verify_receipt: async (input) => {
619
649
  const args = objectInput("haven_verify_receipt", input);
@@ -641,7 +671,8 @@ function createToolHandlers(haven) {
641
671
  task_budget: result.taskBudget,
642
672
  next_action: "sign",
643
673
  next_tool: "mcp__haven-signer__haven_sign",
644
- next_arguments: { task_budget_id: result.taskBudget.id }
674
+ next_arguments: { task_budget_id: result.taskBudget.id },
675
+ signer_compatibility: taskSignerCompatibilityNotice()
645
676
  };
646
677
  });
647
678
  },
@@ -656,7 +687,8 @@ function createToolHandlers(haven) {
656
687
  task_budget: result.taskBudget,
657
688
  next_action: "sign",
658
689
  next_tool: "mcp__haven-signer__haven_sign",
659
- next_arguments: { task_budget_id: args.task_budget_id }
690
+ next_arguments: { task_budget_id: args.task_budget_id },
691
+ signer_compatibility: taskSignerCompatibilityNotice()
660
692
  };
661
693
  });
662
694
  },
@@ -713,28 +745,20 @@ function preflight(name, input) {
713
745
  }
714
746
  const idem = resolveIdempotencyKey(args);
715
747
  if ("success" in idem) return idem;
716
- if (idem.key !== void 0) args = { ...args, idempotencyKey: idem.key };
748
+ if (idem.key !== void 0) args = { ...args, resolvedIdempotencyKey: idem.key };
717
749
  return { args, warnings: idem.warnings };
718
750
  }
719
751
  function resolveIdempotencyKey(args) {
720
752
  const modern = typeof args.idempotency_key === "string" ? args.idempotency_key : void 0;
721
753
  const legacy = typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0;
722
- if (modern !== void 0 && legacy !== void 0 && modern !== legacy) {
754
+ if (legacy !== void 0) {
723
755
  return {
724
756
  success: false,
725
- code: "AMBIGUOUS_IDEMPOTENCY_KEY",
726
- 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."
727
759
  };
728
760
  }
729
761
  if (modern !== void 0) return { key: modern, warnings: [] };
730
- if (legacy !== void 0) {
731
- return {
732
- key: legacy,
733
- warnings: [
734
- "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."
735
- ]
736
- };
737
- }
738
762
  return { warnings: [] };
739
763
  }
740
764
  function buildMcpToolsCallEnvelope(toolName, args) {
@@ -965,7 +989,7 @@ function renderConsentBlock(input, hash) {
965
989
  lines.push("signed delegation, not by Haven):");
966
990
  for (const a of input.allowanceSummary) {
967
991
  const reset = a.resetMinutes ? ` per ${a.resetMinutes} min` : " (no reset)";
968
- lines.push(` \u2022 up to ${a.amount} ${a.token}${reset}`);
992
+ lines.push(` \u2022 up to ${describeBudgetAmount(a, input.chainId)}${reset}`);
969
993
  }
970
994
  }
971
995
  lines.push("");
@@ -1062,7 +1086,14 @@ async function consentInputFromClient(haven, seed, toolNames) {
1062
1086
  }
1063
1087
  const liveAllowanceSummary = list.map((a) => ({
1064
1088
  token: a.tokenSymbol ?? "UNKNOWN",
1065
- 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",
1066
1097
  resetMinutes: typeof a.onchain?.resetTimeMin === "number" ? a.onchain.resetTimeMin : typeof a.resetPeriodMin === "number" ? a.resetPeriodMin : null
1067
1098
  }));
1068
1099
  allowanceSummary = liveAllowanceSummary;
@@ -1085,6 +1116,11 @@ function isAllowanceSummary(value) {
1085
1116
  function derivePrefix(apiKey) {
1086
1117
  return apiKey.slice(0, 12);
1087
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
+ }
1088
1124
  function registeredToolNames() {
1089
1125
  return Object.keys(toolSchemas);
1090
1126
  }
@@ -1109,7 +1145,7 @@ async function resolveHavenClient(options = {}) {
1109
1145
  return { client, credentials };
1110
1146
  }
1111
1147
  var MCP_NAME = "@haven_ai/mcp";
1112
- var MCP_VERSION = "0.6.0-alpha.0";
1148
+ var MCP_VERSION = "0.7.0-alpha.0";
1113
1149
  var MCP_INSTRUCTIONS = [
1114
1150
  "Haven local MCP server: signs in-process with the delegate key it holds on",
1115
1151
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",