@muhaven/mcp 0.2.2 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.4] — 2026-05-23
11
+
12
+ ### Fixed
13
+
14
+ - **Paymaster RPC method + param shape**:
15
+ `pm_sponsorUserOperation` → `zd_sponsorUserOperation`. ZeroDev v3
16
+ endpoints route paymaster RPCs through Alchemy infrastructure, which
17
+ exposes only ERC-7677 (`pm_getPaymasterStubData` / `pm_getPaymasterData`)
18
+ and ZeroDev-prefixed (`zd_sponsorUserOperation`) — the legacy
19
+ `pm_sponsorUserOperation` returns `"Unsupported method"` from
20
+ Alchemy. The MCP server's `attemptPathD` failed at
21
+ `pathDFallbackReason: paymaster_rejected` on every Path D autonomous
22
+ buy. Fix: switch method name + change request shape from positional
23
+ `[userOp, entryPoint]` to wrapped
24
+ `[{chainId, userOp, entryPointAddress, shouldOverrideFee, shouldConsume}]`.
25
+ Matches `@zerodev/sdk@5.5.10`'s `paymaster/sponsorUserOperation.js`
26
+ byte-for-byte. Verified 2026-05-23 via direct curl reproduction
27
+ against the prod bundler URL (bare → "Unsupported method"; correct
28
+ shape → simulation result with AA23 from synthetic UserOp, proving
29
+ the method works).
30
+
31
+ `sponsorUserOp` now requires `expectedChainId` in `BundlerClientOptions`
32
+ (throws `BundlerClientError(config)` when missing) — the chainId is
33
+ part of the request envelope. Defaults conservative placeholder gas
34
+ limits on the userOp when the caller omits them (ZeroDev's Zod
35
+ validator requires the gas fields in the request even though
36
+ simulation recomputes them).
37
+
38
+ Added 3 regression tests pinning the new method name, the wrapped
39
+ envelope, the gas-default behaviour + a `config`-error case when
40
+ expectedChainId is missing.
41
+
42
+ ## [0.2.3] — 2026-05-23
43
+
44
+ ### Fixed
45
+
46
+ - **`BundlerClient` now sends an `Origin` header on every RPC.** ZeroDev
47
+ bundler URLs gate access via an IP+domain allowlist; browser requests
48
+ from `https://muhaven.app` pass because the project's allowlist
49
+ accepts that domain, but Node `fetch` (the MCP server's transport)
50
+ sends no `Origin` by default and so hit a `403 "Neither IP nor domain
51
+ is on the allowlist"` on every `eth_call` / `eth_gasPrice`. The MCP
52
+ server then surfaced this as `pathDFallbackReason:
53
+ bundler_setup_failed` and degraded to Path C. Fix: stamp `Origin:
54
+ <MUHAVEN_DASHBOARD_URL>` on every bundler RPC (defaults to
55
+ `https://muhaven.app`, threaded through from
56
+ `buildMcpServer({ dashboardBaseUrl })`). Mirrors how ethers.js + viem
57
+ stamp default Origins against EVM RPC providers; surfaces a single
58
+ knob (`MUHAVEN_DASHBOARD_URL`) for operators on a custom domain.
59
+ Diagnosed 2026-05-23 via direct curl reproduction (bare → 403; with
60
+ `Origin: https://muhaven.app` → `result: 0x66eee`).
61
+
62
+ Added 3 regression tests pinning the contract (Origin sent when set;
63
+ omitted when undefined; omitted when explicitly empty for test
64
+ injection).
65
+
10
66
  ## [0.2.2] — 2026-05-23
11
67
 
12
68
  ### Fixed
package/dist/broker.cjs CHANGED
@@ -2783,7 +2783,7 @@ function printUsage() {
2783
2783
  }
2784
2784
  function getBrokerPackageVersion() {
2785
2785
  {
2786
- return "0.2.2";
2786
+ return "0.2.4";
2787
2787
  }
2788
2788
  }
2789
2789
  function printVersion() {
package/dist/broker.js CHANGED
@@ -2785,7 +2785,7 @@ function printUsage() {
2785
2785
  }
2786
2786
  function getBrokerPackageVersion() {
2787
2787
  {
2788
- return "0.2.2";
2788
+ return "0.2.4";
2789
2789
  }
2790
2790
  }
2791
2791
  function printVersion() {
package/dist/index.cjs CHANGED
@@ -1071,7 +1071,28 @@ var BundlerClient = class {
1071
1071
  * `estimateUserOpGas` round-trip on the happy path).
1072
1072
  */
1073
1073
  async sponsorUserOp(userOp, entryPoint) {
1074
- const result = await this.rpc("pm_sponsorUserOperation", [userOp, entryPoint]);
1074
+ if (this.options.expectedChainId === void 0) {
1075
+ throw new BundlerClientError(
1076
+ "config",
1077
+ "sponsorUserOp requires expectedChainId in BundlerClientOptions \u2014 ZeroDev paymaster needs it in the request envelope"
1078
+ );
1079
+ }
1080
+ const userOpWithGas = {
1081
+ ...userOp,
1082
+ callGasLimit: userOp.callGasLimit ?? "0x100000",
1083
+ // 1_048_576
1084
+ verificationGasLimit: userOp.verificationGasLimit ?? "0x100000",
1085
+ preVerificationGas: userOp.preVerificationGas ?? "0x100000"
1086
+ };
1087
+ const result = await this.rpc("zd_sponsorUserOperation", [
1088
+ {
1089
+ chainId: this.options.expectedChainId,
1090
+ userOp: userOpWithGas,
1091
+ entryPointAddress: entryPoint,
1092
+ shouldOverrideFee: false,
1093
+ shouldConsume: true
1094
+ }
1095
+ ]);
1075
1096
  return parseSponsoredFields(result);
1076
1097
  }
1077
1098
  /**
@@ -1194,9 +1215,16 @@ var BundlerClient = class {
1194
1215
  const timer = setTimeout(() => ctrl.abort(), this.options.requestTimeoutMs);
1195
1216
  let res;
1196
1217
  try {
1218
+ const headers = {
1219
+ "content-type": "application/json",
1220
+ accept: "application/json"
1221
+ };
1222
+ if (this.options.originHeader) {
1223
+ headers["origin"] = this.options.originHeader;
1224
+ }
1197
1225
  res = await this.fetchImpl(this.options.endpoint, {
1198
1226
  method: "POST",
1199
- headers: { "content-type": "application/json", accept: "application/json" },
1227
+ headers,
1200
1228
  body,
1201
1229
  signal: ctrl.signal
1202
1230
  });
@@ -3025,7 +3053,7 @@ var SERVER_NAME = "@muhaven/mcp";
3025
3053
  var SERVER_VERSION = resolveServerVersion();
3026
3054
  function resolveServerVersion() {
3027
3055
  {
3028
- return "0.2.2";
3056
+ return "0.2.4";
3029
3057
  }
3030
3058
  }
3031
3059
  function toJsonInputSchema(schema) {
@@ -3166,7 +3194,13 @@ async function runMcpStdioCli(opts = {}) {
3166
3194
  const bundler = config.bundlerUrl ? new BundlerClient({
3167
3195
  endpoint: config.bundlerUrl,
3168
3196
  requestTimeoutMs: config.bundlerTimeoutMs,
3169
- expectedChainId: config.chainId
3197
+ expectedChainId: config.chainId,
3198
+ // Wave 5 Path D 0.2.3 — Origin defaults to the dashboard URL
3199
+ // so ZeroDev's domain-allowlist accepts the MCP server's RPC
3200
+ // traffic. The dashboard URL is the natural match because it's
3201
+ // also the SIWE / passkey origin the project already trusts
3202
+ // for browser-side traffic.
3203
+ originHeader: config.dashboardBaseUrl
3170
3204
  }) : void 0;
3171
3205
  const baseRegistry = selectRegistry(config.readOnly);
3172
3206
  const registry = opts.filterRegistry ? opts.filterRegistry(baseRegistry) : baseRegistry;
package/dist/index.d.cts CHANGED
@@ -708,6 +708,17 @@ interface PartialUserOpForSponsorship {
708
708
  readonly maxPriorityFeePerGas: `0x${string}`;
709
709
  /** Worst-case placeholder so paymaster simulates realistic gas. */
710
710
  readonly signature: `0x${string}`;
711
+ /**
712
+ * Optional pre-estimated gas fields. ZeroDev's `zd_sponsorUserOperation`
713
+ * does its own simulation + recomputes these in the response, so
714
+ * passing placeholders is OK — but the upstream Zod validator
715
+ * REQUIRES the fields to be present. `sponsorUserOp` defaults them
716
+ * to safe placeholders when omitted (the response carries the real
717
+ * values for the caller to use).
718
+ */
719
+ readonly callGasLimit?: `0x${string}`;
720
+ readonly verificationGasLimit?: `0x${string}`;
721
+ readonly preVerificationGas?: `0x${string}`;
711
722
  }
712
723
  interface SponsoredUserOpFields {
713
724
  readonly paymaster: `0x${string}`;
@@ -753,6 +764,24 @@ interface BundlerClientOptions {
753
764
  /** Expected chain id (Arb Sepolia = 421614). When set, `assertChainId()`
754
765
  * refuses to proceed if the bundler reports a different chain. */
755
766
  readonly expectedChainId?: number;
767
+ /**
768
+ * Wave 5 Path D 0.2.3 — `Origin` header sent on every bundler RPC.
769
+ *
770
+ * Why: ZeroDev's bundler URLs gate access via an IP+domain allowlist.
771
+ * Browser requests from `https://muhaven.app` pass because the
772
+ * project's allowlist includes that domain; Node `fetch` (the MCP
773
+ * server's transport) sends no `Origin` header by default and so
774
+ * hits a 403 "Neither IP nor domain is on the allowlist". Sending
775
+ * an `Origin` matching the project's allowlisted domain unblocks
776
+ * the MCP server without requiring an operator-side ZeroDev
777
+ * dashboard edit. Mirrors how ethers.js + viem's HTTP transports
778
+ * stamp a default `Origin` against EVM RPC providers.
779
+ *
780
+ * Defaults to `https://muhaven.app` at the call site
781
+ * (`server.ts::buildMcpServer`) — operators on a custom dashboard
782
+ * URL override via `MUHAVEN_DASHBOARD_URL`.
783
+ */
784
+ readonly originHeader?: string;
756
785
  /** Inject for tests. */
757
786
  readonly fetchImpl?: typeof fetch;
758
787
  }
package/dist/index.d.ts CHANGED
@@ -708,6 +708,17 @@ interface PartialUserOpForSponsorship {
708
708
  readonly maxPriorityFeePerGas: `0x${string}`;
709
709
  /** Worst-case placeholder so paymaster simulates realistic gas. */
710
710
  readonly signature: `0x${string}`;
711
+ /**
712
+ * Optional pre-estimated gas fields. ZeroDev's `zd_sponsorUserOperation`
713
+ * does its own simulation + recomputes these in the response, so
714
+ * passing placeholders is OK — but the upstream Zod validator
715
+ * REQUIRES the fields to be present. `sponsorUserOp` defaults them
716
+ * to safe placeholders when omitted (the response carries the real
717
+ * values for the caller to use).
718
+ */
719
+ readonly callGasLimit?: `0x${string}`;
720
+ readonly verificationGasLimit?: `0x${string}`;
721
+ readonly preVerificationGas?: `0x${string}`;
711
722
  }
712
723
  interface SponsoredUserOpFields {
713
724
  readonly paymaster: `0x${string}`;
@@ -753,6 +764,24 @@ interface BundlerClientOptions {
753
764
  /** Expected chain id (Arb Sepolia = 421614). When set, `assertChainId()`
754
765
  * refuses to proceed if the bundler reports a different chain. */
755
766
  readonly expectedChainId?: number;
767
+ /**
768
+ * Wave 5 Path D 0.2.3 — `Origin` header sent on every bundler RPC.
769
+ *
770
+ * Why: ZeroDev's bundler URLs gate access via an IP+domain allowlist.
771
+ * Browser requests from `https://muhaven.app` pass because the
772
+ * project's allowlist includes that domain; Node `fetch` (the MCP
773
+ * server's transport) sends no `Origin` header by default and so
774
+ * hits a 403 "Neither IP nor domain is on the allowlist". Sending
775
+ * an `Origin` matching the project's allowlisted domain unblocks
776
+ * the MCP server without requiring an operator-side ZeroDev
777
+ * dashboard edit. Mirrors how ethers.js + viem's HTTP transports
778
+ * stamp a default `Origin` against EVM RPC providers.
779
+ *
780
+ * Defaults to `https://muhaven.app` at the call site
781
+ * (`server.ts::buildMcpServer`) — operators on a custom dashboard
782
+ * URL override via `MUHAVEN_DASHBOARD_URL`.
783
+ */
784
+ readonly originHeader?: string;
756
785
  /** Inject for tests. */
757
786
  readonly fetchImpl?: typeof fetch;
758
787
  }
package/dist/index.js CHANGED
@@ -1067,7 +1067,28 @@ var BundlerClient = class {
1067
1067
  * `estimateUserOpGas` round-trip on the happy path).
1068
1068
  */
1069
1069
  async sponsorUserOp(userOp, entryPoint) {
1070
- const result = await this.rpc("pm_sponsorUserOperation", [userOp, entryPoint]);
1070
+ if (this.options.expectedChainId === void 0) {
1071
+ throw new BundlerClientError(
1072
+ "config",
1073
+ "sponsorUserOp requires expectedChainId in BundlerClientOptions \u2014 ZeroDev paymaster needs it in the request envelope"
1074
+ );
1075
+ }
1076
+ const userOpWithGas = {
1077
+ ...userOp,
1078
+ callGasLimit: userOp.callGasLimit ?? "0x100000",
1079
+ // 1_048_576
1080
+ verificationGasLimit: userOp.verificationGasLimit ?? "0x100000",
1081
+ preVerificationGas: userOp.preVerificationGas ?? "0x100000"
1082
+ };
1083
+ const result = await this.rpc("zd_sponsorUserOperation", [
1084
+ {
1085
+ chainId: this.options.expectedChainId,
1086
+ userOp: userOpWithGas,
1087
+ entryPointAddress: entryPoint,
1088
+ shouldOverrideFee: false,
1089
+ shouldConsume: true
1090
+ }
1091
+ ]);
1071
1092
  return parseSponsoredFields(result);
1072
1093
  }
1073
1094
  /**
@@ -1190,9 +1211,16 @@ var BundlerClient = class {
1190
1211
  const timer = setTimeout(() => ctrl.abort(), this.options.requestTimeoutMs);
1191
1212
  let res;
1192
1213
  try {
1214
+ const headers = {
1215
+ "content-type": "application/json",
1216
+ accept: "application/json"
1217
+ };
1218
+ if (this.options.originHeader) {
1219
+ headers["origin"] = this.options.originHeader;
1220
+ }
1193
1221
  res = await this.fetchImpl(this.options.endpoint, {
1194
1222
  method: "POST",
1195
- headers: { "content-type": "application/json", accept: "application/json" },
1223
+ headers,
1196
1224
  body,
1197
1225
  signal: ctrl.signal
1198
1226
  });
@@ -3021,7 +3049,7 @@ var SERVER_NAME = "@muhaven/mcp";
3021
3049
  var SERVER_VERSION = resolveServerVersion();
3022
3050
  function resolveServerVersion() {
3023
3051
  {
3024
- return "0.2.2";
3052
+ return "0.2.4";
3025
3053
  }
3026
3054
  }
3027
3055
  function toJsonInputSchema(schema) {
@@ -3162,7 +3190,13 @@ async function runMcpStdioCli(opts = {}) {
3162
3190
  const bundler = config.bundlerUrl ? new BundlerClient({
3163
3191
  endpoint: config.bundlerUrl,
3164
3192
  requestTimeoutMs: config.bundlerTimeoutMs,
3165
- expectedChainId: config.chainId
3193
+ expectedChainId: config.chainId,
3194
+ // Wave 5 Path D 0.2.3 — Origin defaults to the dashboard URL
3195
+ // so ZeroDev's domain-allowlist accepts the MCP server's RPC
3196
+ // traffic. The dashboard URL is the natural match because it's
3197
+ // also the SIWE / passkey origin the project already trusts
3198
+ // for browser-side traffic.
3199
+ originHeader: config.dashboardBaseUrl
3166
3200
  }) : void 0;
3167
3201
  const baseRegistry = selectRegistry(config.readOnly);
3168
3202
  const registry = opts.filterRegistry ? opts.filterRegistry(baseRegistry) : baseRegistry;
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.2",
4
4
  "name": "muhaven-mcp",
5
5
  "display_name": "MuHaven (RWA portfolio)",
6
- "version": "0.2.2",
6
+ "version": "0.2.4",
7
7
  "description": "Confidential RWA portfolio management on Fhenix CoFHE. Read your encrypted balances, propose yield claims and policy changes — all signing happens in a sibling broker daemon, the LLM never sees your private key.",
8
8
  "long_description": "MuHaven MCP exposes 24 tools across read.* / position.* / policy.* / issuer.* / governance.* groups for managing real-world asset (RWA) tokens with FHE-encrypted balances. Authentication uses a one-time device-code ceremony (run `muhaven-broker login`); subsequent tool calls fetch the JWT from the broker over a Unix socket. Position / governance tools deep-link to the dashboard for passkey signing — they NEVER auto-submit to a bundler. The companion `muhaven-broker` daemon must be running before tools can be invoked. See README for setup.",
9
9
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhaven/mcp",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "MuHaven MCP server — read/position/policy toolsets bridging Claude Desktop / Cursor / Claude Code to the MuHaven backend, with a sibling muhaven-broker daemon holding the session-key private half over a local IPC socket",
5
5
  "type": "module",
6
6
  "repository": {