@haven_ai/mcp 0.10.0-alpha.0 → 0.11.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/index.d.cts CHANGED
@@ -98,6 +98,14 @@ declare const toolSchemas: {
98
98
  readonly headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
99
99
  readonly body: z.ZodOptional<z.ZodString>;
100
100
  readonly idempotency_key: z.ZodOptional<z.ZodString>;
101
+ /**
102
+ * #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
103
+ * compared against the amount of the option pay will actually select. A
104
+ * mismatch stops the agent — the same stop signals the hosted quote
105
+ * carries. Local parity: a hosted-only input would split the two tools'
106
+ * contracts. Same shape and name as the hosted surface's.
107
+ */
108
+ readonly expected_amount: z.ZodOptional<z.ZodString>;
101
109
  /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
102
110
  readonly idempotencyKey: z.ZodOptional<z.ZodString>;
103
111
  };
@@ -415,7 +423,7 @@ declare function createHavenMcpServer(options?: HavenMcpServerOptions): Promise<
415
423
  * `agent_tool_invocations` rows are always attributed to the right tool.
416
424
  */
417
425
  declare const MCP_NAME = "@haven_ai/mcp";
418
- declare const MCP_VERSION = "0.10.0-alpha.0";
426
+ declare const MCP_VERSION = "0.11.0-alpha.0";
419
427
  /**
420
428
  * Refuse to start on an unsupported Node (#1161).
421
429
  *
package/dist/index.d.ts CHANGED
@@ -98,6 +98,14 @@ declare const toolSchemas: {
98
98
  readonly headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
99
99
  readonly body: z.ZodOptional<z.ZodString>;
100
100
  readonly idempotency_key: z.ZodOptional<z.ZodString>;
101
+ /**
102
+ * #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
103
+ * compared against the amount of the option pay will actually select. A
104
+ * mismatch stops the agent — the same stop signals the hosted quote
105
+ * carries. Local parity: a hosted-only input would split the two tools'
106
+ * contracts. Same shape and name as the hosted surface's.
107
+ */
108
+ readonly expected_amount: z.ZodOptional<z.ZodString>;
101
109
  /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
102
110
  readonly idempotencyKey: z.ZodOptional<z.ZodString>;
103
111
  };
@@ -415,7 +423,7 @@ declare function createHavenMcpServer(options?: HavenMcpServerOptions): Promise<
415
423
  * `agent_tool_invocations` rows are always attributed to the right tool.
416
424
  */
417
425
  declare const MCP_NAME = "@haven_ai/mcp";
418
- declare const MCP_VERSION = "0.10.0-alpha.0";
426
+ declare const MCP_VERSION = "0.11.0-alpha.0";
419
427
  /**
420
428
  * Refuse to start on an unsupported Node (#1161).
421
429
  *
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { composeDescription, toolDescriptions as toolDescriptions$1, TASK_SIGN_CONTEXT_VERSION, connectorUpgradeCommand, HavenPaymentStateError, HavenSigningError, HavenApiError, AgentPaymentNextAction, HavenError, resolveTokenBySymbol, formatTokenAmount, HavenClient, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, verifyPaymentReceipt, catalogFundingLegExpected, discoverMerchantMcpUrl, sameUrl, AgentPaymentFailureCode, resolveTokenFromAddress } from '@haven_ai/sdk';
3
+ import { composeDescription, toolDescriptions as toolDescriptions$1, TASK_SIGN_CONTEXT_VERSION, connectorUpgradeCommand, x402AuthorizationAmount, AgentPaymentNextAction, HavenPaymentStateError, HavenSigningError, MerchantEgressRefusedError, HavenApiError, HavenError, resolveTokenBySymbol, formatTokenAmount, HavenClient, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, verifyPaymentReceipt, catalogFundingLegExpected, discoverMerchantMcpUrl, sameUrl, selectErc7710PaymentOption, selectStandardPaymentOption, AgentPaymentFailureCode, resolveTokenFromAddress } from '@haven_ai/sdk';
4
4
  import { readFile, stat, mkdir, writeFile } from 'fs/promises';
5
5
  import { z } from 'zod/v3';
6
6
  import { createHash } from 'crypto';
@@ -236,6 +236,18 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
236
236
  );
237
237
  }
238
238
  }
239
+ function chainRpcsFromEnv(env) {
240
+ const chainRpcs = {};
241
+ for (const [name, value] of Object.entries(env)) {
242
+ const match = /^HAVEN_RPC_URL_(\d+)$/.exec(name);
243
+ if (!match) continue;
244
+ const chainId = Number.parseInt(match[1], 10);
245
+ if (!Number.isSafeInteger(chainId) || chainId <= 0) continue;
246
+ const url = value?.trim();
247
+ if (url) chainRpcs[chainId] = url;
248
+ }
249
+ return chainRpcs;
250
+ }
239
251
  var TASK_BUDGET_MIN_SIGNER_VERSION = "0.6.0-alpha.0";
240
252
  var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
241
253
  function taskSignerCompatibilityNotice() {
@@ -248,6 +260,20 @@ function taskSignerCompatibilityNotice() {
248
260
  };
249
261
  }
250
262
  var headersSchema = z.record(z.string(), z.string()).optional();
263
+ function selectQuoteOption(accepts, agent) {
264
+ if (agent.executionRail === "delegation") {
265
+ const erc7710 = selectErc7710PaymentOption(accepts);
266
+ if (erc7710) return erc7710;
267
+ }
268
+ return selectStandardPaymentOption(accepts);
269
+ }
270
+ function humanDecimalToAtomic(human, decimals) {
271
+ const match = /^([0-9]+)(?:\.([0-9]+))?$/.exec(human);
272
+ if (!match) return null;
273
+ const fraction = match[2] ?? "";
274
+ if (fraction.length > decimals) return null;
275
+ return BigInt(match[1] + fraction.padEnd(decimals, "0"));
276
+ }
251
277
  var toolSchemas = {
252
278
  haven_send: {
253
279
  asset: z.enum(["ETH", "USDC"]),
@@ -279,6 +305,17 @@ var toolSchemas = {
279
305
  headers: headersSchema,
280
306
  body: z.string().optional(),
281
307
  idempotency_key: z.string().optional(),
308
+ /**
309
+ * #3967: the price the agent EXPECTS, in whole tokens ("0.10" = 0.1 USDC),
310
+ * compared against the amount of the option pay will actually select. A
311
+ * mismatch stops the agent — the same stop signals the hosted quote
312
+ * carries. Local parity: a hosted-only input would split the two tools'
313
+ * contracts. Same shape and name as the hosted surface's.
314
+ */
315
+ expected_amount: z.string().regex(
316
+ /^[0-9]+(\.[0-9]+)?$/,
317
+ 'expected_amount must be a plain decimal amount in whole tokens, e.g. "0.10"'
318
+ ).optional(),
282
319
  /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
283
320
  idempotencyKey: z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
284
321
  },
@@ -625,8 +662,39 @@ function createToolHandlers(haven) {
625
662
  const pf = preflight("haven_quote_x402", input);
626
663
  if ("success" in pf) return pf;
627
664
  const { args, warnings } = pf;
665
+ const agentPromise = args.expected_amount !== void 0 ? haven.getAgent().then(
666
+ (a) => a,
667
+ () => void 0
668
+ ) : null;
628
669
  try {
629
670
  const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.resolvedIdempotencyKey });
671
+ if (args.expected_amount !== void 0) {
672
+ const agent = agentPromise ? await agentPromise : void 0;
673
+ const option = agent ? selectQuoteOption(data.paymentRequired.accepts, agent) : null;
674
+ if (option && data.decimals !== null) {
675
+ const expectedAtomic = humanDecimalToAtomic(args.expected_amount, data.decimals);
676
+ let optionAmount = null;
677
+ try {
678
+ optionAmount = BigInt(x402AuthorizationAmount(option));
679
+ } catch {
680
+ optionAmount = null;
681
+ }
682
+ if (optionAmount !== null && expectedAtomic !== null) {
683
+ const priceDiffers = optionAmount !== expectedAtomic;
684
+ const payload = {
685
+ ...data,
686
+ price_differs_from_expected: priceDiffers,
687
+ ...priceDiffers ? {
688
+ safe_to_continue: false,
689
+ next_action: AgentPaymentNextAction.StopAndTellUser,
690
+ next_tool_omitted_reason: "no Haven tool follows: the quoted price does not match the stated expectation \u2014 the user decides before any payment",
691
+ 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.`
692
+ } : {}
693
+ };
694
+ return warnings.length > 0 ? { success: true, data: payload, warnings } : { success: true, data: payload };
695
+ }
696
+ }
697
+ }
630
698
  return warnings.length > 0 ? { success: true, data, warnings } : { success: true, data };
631
699
  } catch (err) {
632
700
  return normalizeError(err);
@@ -1082,6 +1150,24 @@ function normalizeError(err) {
1082
1150
  message: err.message
1083
1151
  };
1084
1152
  }
1153
+ if (err instanceof MerchantEgressRefusedError && !err.beforeRequest && err.paymentId) {
1154
+ const noFundingLeg = err.settlementScheme === "erc7710";
1155
+ const nextAction = noFundingLeg ? AgentPaymentNextAction.CheckStatusLater : AgentPaymentNextAction.SweepStrandedFunds;
1156
+ return {
1157
+ success: false,
1158
+ code: err.code,
1159
+ message: err.message,
1160
+ statusCode: err.statusCode,
1161
+ paymentId: err.paymentId,
1162
+ status: "merchant_unresponsive_after_funding",
1163
+ phase: noFundingLeg ? "not_delivered" : "funded_but_unsettled",
1164
+ nextAction,
1165
+ next_action: nextAction,
1166
+ next_tool: "haven_get_payment_status",
1167
+ next_arguments: { payment_id: err.paymentId },
1168
+ suggested_tool: "haven_get_payment_status"
1169
+ };
1170
+ }
1085
1171
  if (err instanceof HavenApiError) {
1086
1172
  const body = err.body;
1087
1173
  return {
@@ -1328,10 +1414,14 @@ async function resolveHavenClient(options = {}) {
1328
1414
  signerPath: options.signerPath
1329
1415
  } : void 0;
1330
1416
  const credentials = options.credentials ?? await loadCredentials(credentialSource);
1417
+ const chainRpcs = chainRpcsFromEnv(process.env);
1331
1418
  const client = new HavenClient({
1332
1419
  apiKey: credentials.apiKey,
1333
1420
  delegateKey: credentials.delegateKey,
1334
1421
  baseUrl: credentials.apiUrl,
1422
+ // Only added when non-empty so the unset-variable construction is byte-for-byte
1423
+ // today's: the SDK defaults `chainRpcs` to `{}` either way.
1424
+ ...Object.keys(chainRpcs).length > 0 ? { chainRpcs } : {},
1335
1425
  // #3303: name this package, not the SDK inside it, so the backend's
1336
1426
  // update hint and refusal speak about what the user actually installed.
1337
1427
  clientIdentity: havenClientIdentity(MCP_NAME, MCP_VERSION)
@@ -1343,7 +1433,7 @@ async function createHavenMcpServer(options = {}) {
1343
1433
  return buildMcpServer(haven);
1344
1434
  }
1345
1435
  var MCP_NAME = "@haven_ai/mcp";
1346
- var MCP_VERSION = "0.10.0-alpha.0";
1436
+ var MCP_VERSION = "0.11.0-alpha.0";
1347
1437
  var MCP_INSTRUCTIONS = [
1348
1438
  "Haven local MCP server: signs in-process with the delegate key it holds on",
1349
1439
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",