@muhaven/mcp 0.2.6 → 0.2.8

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,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.8] — 2026-05-23
11
+
12
+ ### Added
13
+
14
+ - **`pathDBundlerTrace` inline in the `muhaven.position.buy` echo on
15
+ fallback.** Every Path D attempt now buffers its bundler RPC
16
+ round-trips in a 20-event ring on the `BundlerClient`; on any
17
+ fallback return the handler drains the ring and inlines it into
18
+ the echo:
19
+
20
+ pathDBundlerTrace: [
21
+ {
22
+ method: 'eth_call',
23
+ id: 12,
24
+ requestBody: '{"jsonrpc":"2.0","id":12,"method":"eth_call",...}',
25
+ responseStatus: 200,
26
+ responseBody: '{"jsonrpc":"2.0","id":12,"result":"0x..."}',
27
+ elapsedMs: 87,
28
+ },
29
+ {
30
+ method: 'zd_sponsorUserOperation',
31
+ id: 14,
32
+ requestBody: '{"jsonrpc":"2.0","method":"zd_sponsorUserOperation","params":[...]}',
33
+ responseStatus: 400,
34
+ responseBody: '{"error":"AA23 reverted ..."}',
35
+ error: { code: 'http_error', message: '...' },
36
+ elapsedMs: 412,
37
+ },
38
+ ]
39
+
40
+ Bodies truncated at 2KB. The LLM (and the operator reading the
41
+ tool result) sees the EXACT wire payload that the bundler /
42
+ paymaster rejected — no need for curl repro or Claude Code
43
+ subprocess log digging. Confirmed 2026-05-23 that Claude Code's
44
+ MCP client only captures subprocess stderr at handshake (not
45
+ during tool calls), so the 0.2.7 stderr-verbose path never reached
46
+ the LLM context during the smoke iterations.
47
+
48
+ Ring buffer is always-on (no env-gate, ~80KB worst-case per
49
+ BundlerClient instance) so the next gate is self-diagnosing
50
+ immediately without a second `npm i -g` cycle.
51
+
52
+ `BundlerClient.drainTrace()` returns + clears the ring; called at
53
+ the start of `attemptPathD` (clear stale) and again right before
54
+ the fallback echo (collect + inline).
55
+
56
+ ### Tests
57
+
58
+ - bundler-client.test.ts: new ring-buffer behaviour cases (drain
59
+ returns a copy; drain clears; ring is bounded at 20; populates on
60
+ success + http_error + timeout + rpc_error).
61
+
62
+ ## [0.2.7] — 2026-05-23
63
+
64
+ ### Added
65
+
66
+ - **Startup banner (always on).** `runMcpStdioCli` writes one stderr
67
+ line at boot with the running version + verbose mode:
68
+
69
+ [muhaven-mcp] starting @muhaven/mcp@0.2.7 (verbose=off)
70
+
71
+ Multiple smoke iterations have been ambiguous about whether
72
+ `npm i -g @muhaven/mcp@<v>` actually updated the global binary
73
+ picked up by Claude Code (the subprocess only re-spawns on FULL
74
+ Claude Code restart, not `/mcp reconnect`). One stderr line at boot
75
+ makes the version mismatch impossible to miss.
76
+
77
+ - **Verbose paymaster/bundler logging (`MUHAVEN_MCP_VERBOSE=1`).**
78
+ Gated env var that emits two stderr lines per bundler RPC:
79
+
80
+ [muhaven-mcp] [bundler→] zd_sponsorUserOperation id=N body={...}
81
+ [muhaven-mcp] [bundler←] zd_sponsorUserOperation id=N resp={...}
82
+
83
+ Bodies truncated at 2KB; covers happy-path, http_error, timeout,
84
+ non-JSON, network failures. Add `"MUHAVEN_MCP_VERBOSE": "1"` to the
85
+ `.mcp.json` env block when triaging a `pathDFallbackReason` —
86
+ removes the need for curl repro entirely.
87
+
88
+ ### Fixed
89
+
90
+ - **Stale `pm_sponsorUserOperation` label in the
91
+ `paymaster_rejected` fallback message** (0.2.4 leftover). The
92
+ actual method call has been `zd_sponsorUserOperation` since 0.2.4,
93
+ but the error message label still said `pm_*`. No functional
94
+ impact — just a confusing log string when the gate fires.
95
+
10
96
  ## [0.2.6] — 2026-05-23
11
97
 
12
98
  ### Fixed
package/dist/broker.cjs CHANGED
@@ -2783,7 +2783,7 @@ function printUsage() {
2783
2783
  }
2784
2784
  function getBrokerPackageVersion() {
2785
2785
  {
2786
- return "0.2.6";
2786
+ return "0.2.8";
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.6";
2788
+ return "0.2.8";
2789
2789
  }
2790
2790
  }
2791
2791
  function printVersion() {
package/dist/index.cjs CHANGED
@@ -999,7 +999,7 @@ var BundlerClientError = class extends Error {
999
999
  code;
1000
1000
  detail;
1001
1001
  };
1002
- var BundlerClient = class {
1002
+ var BundlerClient = class _BundlerClient {
1003
1003
  constructor(options) {
1004
1004
  this.options = options;
1005
1005
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -1007,6 +1007,35 @@ var BundlerClient = class {
1007
1007
  options;
1008
1008
  fetchImpl;
1009
1009
  nextRpcId = 1;
1010
+ /**
1011
+ * Ring buffer of the most recent RPC round-trips. Surfaced via
1012
+ * `drainTrace()` to the caller (attemptPathD) for inline diagnostic
1013
+ * in the position.buy echo. Always populated (no env-gate) — bounded
1014
+ * at 20 events × ~4KB each ≈ 80KB worst-case per client instance.
1015
+ * Cheap enough to keep on for all installs so the next paymaster
1016
+ * gate is self-diagnosing without a second binary spin.
1017
+ */
1018
+ static TRACE_BUFFER_SIZE = 20;
1019
+ recentTrace = [];
1020
+ /**
1021
+ * Return AND CLEAR the in-memory trace ring. attemptPathD calls this
1022
+ * right before constructing a fallback echo so the trace inlined
1023
+ * into the echo corresponds to THAT smoke iteration (subsequent
1024
+ * tool calls start with an empty buffer). Returns a copy so the
1025
+ * caller can safely serialize without races against in-flight
1026
+ * concurrent RPCs.
1027
+ */
1028
+ drainTrace() {
1029
+ const snapshot = this.recentTrace.slice();
1030
+ this.recentTrace = [];
1031
+ return snapshot;
1032
+ }
1033
+ pushTrace(event) {
1034
+ this.recentTrace.push(event);
1035
+ if (this.recentTrace.length > _BundlerClient.TRACE_BUFFER_SIZE) {
1036
+ this.recentTrace.shift();
1037
+ }
1038
+ }
1010
1039
  /**
1011
1040
  * Submit a signed UserOperation. Returns the userOpHash the bundler
1012
1041
  * computed (which must match the hash the broker signed — caller is
@@ -1211,6 +1240,16 @@ var BundlerClient = class {
1211
1240
  async rpc(method, params) {
1212
1241
  const id = this.nextRpcId++;
1213
1242
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
1243
+ const verbose = process.env.MUHAVEN_MCP_VERBOSE === "1";
1244
+ const truncate = (s) => s.length > 2048 ? s.slice(0, 2048) + "\u2026(truncated)" : s;
1245
+ const requestBody = truncate(body);
1246
+ const startMs = Date.now();
1247
+ if (verbose) {
1248
+ process.stderr.write(
1249
+ `[muhaven-mcp] [bundler\u2192] ${method} id=${id} body=${requestBody}
1250
+ `
1251
+ );
1252
+ }
1214
1253
  const ctrl = new AbortController();
1215
1254
  const timer = setTimeout(() => ctrl.abort(), this.options.requestTimeoutMs);
1216
1255
  let res;
@@ -1230,12 +1269,38 @@ var BundlerClient = class {
1230
1269
  });
1231
1270
  } catch (err2) {
1232
1271
  clearTimeout(timer);
1272
+ const elapsedMs2 = Date.now() - startMs;
1233
1273
  if (err2.name === "AbortError") {
1274
+ if (verbose) {
1275
+ process.stderr.write(`[muhaven-mcp] [bundler\u2717] ${method} id=${id} timeout
1276
+ `);
1277
+ }
1278
+ this.pushTrace({
1279
+ method,
1280
+ id,
1281
+ requestBody,
1282
+ error: { code: "timeout", message: `bundler ${method} timed out` },
1283
+ elapsedMs: elapsedMs2
1284
+ });
1234
1285
  throw new BundlerClientError("timeout", `bundler ${method} timed out`);
1235
1286
  }
1287
+ const netMsg = err2 instanceof Error ? err2.message : String(err2);
1288
+ if (verbose) {
1289
+ process.stderr.write(
1290
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} network err=${netMsg}
1291
+ `
1292
+ );
1293
+ }
1294
+ this.pushTrace({
1295
+ method,
1296
+ id,
1297
+ requestBody,
1298
+ error: { code: "network", message: `bundler ${method} network error: ${netMsg}` },
1299
+ elapsedMs: elapsedMs2
1300
+ });
1236
1301
  throw new BundlerClientError(
1237
1302
  "network",
1238
- `bundler ${method} network error: ${err2 instanceof Error ? err2.message : String(err2)}`,
1303
+ `bundler ${method} network error: ${netMsg}`,
1239
1304
  err2
1240
1305
  );
1241
1306
  } finally {
@@ -1247,6 +1312,24 @@ var BundlerClient = class {
1247
1312
  text = (await res.text()).slice(0, 256);
1248
1313
  } catch {
1249
1314
  }
1315
+ if (verbose) {
1316
+ process.stderr.write(
1317
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} HTTP ${res.status} body=${text}
1318
+ `
1319
+ );
1320
+ }
1321
+ this.pushTrace({
1322
+ method,
1323
+ id,
1324
+ requestBody,
1325
+ responseStatus: res.status,
1326
+ responseBody: text,
1327
+ error: {
1328
+ code: "http_error",
1329
+ message: `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
1330
+ },
1331
+ elapsedMs: Date.now() - startMs
1332
+ });
1250
1333
  throw new BundlerClientError(
1251
1334
  "http_error",
1252
1335
  `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
@@ -1256,12 +1339,43 @@ var BundlerClient = class {
1256
1339
  try {
1257
1340
  parsed = await res.json();
1258
1341
  } catch (err2) {
1342
+ const jsonErr = err2 instanceof Error ? err2.message : String(err2);
1343
+ if (verbose) {
1344
+ process.stderr.write(
1345
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} non-JSON err=${jsonErr}
1346
+ `
1347
+ );
1348
+ }
1349
+ this.pushTrace({
1350
+ method,
1351
+ id,
1352
+ requestBody,
1353
+ responseStatus: res.status,
1354
+ error: { code: "invalid_response", message: `bundler ${method} returned non-JSON: ${jsonErr}` },
1355
+ elapsedMs: Date.now() - startMs
1356
+ });
1259
1357
  throw new BundlerClientError(
1260
1358
  "invalid_response",
1261
- `bundler ${method} returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
1359
+ `bundler ${method} returned non-JSON: ${jsonErr}`
1262
1360
  );
1263
1361
  }
1362
+ const respDump = JSON.stringify(parsed);
1363
+ const respBody = truncate(respDump);
1364
+ if (verbose) {
1365
+ process.stderr.write(`[muhaven-mcp] [bundler\u2190] ${method} id=${id} resp=${respBody}
1366
+ `);
1367
+ }
1368
+ const elapsedMs = Date.now() - startMs;
1264
1369
  if (typeof parsed !== "object" || parsed === null) {
1370
+ this.pushTrace({
1371
+ method,
1372
+ id,
1373
+ requestBody,
1374
+ responseStatus: res.status,
1375
+ responseBody: respBody,
1376
+ error: { code: "invalid_response", message: `bundler ${method} returned non-object` },
1377
+ elapsedMs
1378
+ });
1265
1379
  throw new BundlerClientError(
1266
1380
  "invalid_response",
1267
1381
  `bundler ${method} returned non-object`
@@ -1270,12 +1384,30 @@ var BundlerClient = class {
1270
1384
  const obj = parsed;
1271
1385
  if (obj.error !== void 0 && obj.error !== null) {
1272
1386
  const err2 = obj.error;
1387
+ const errMsg = typeof err2.message === "string" ? err2.message : "<no message>";
1388
+ this.pushTrace({
1389
+ method,
1390
+ id,
1391
+ requestBody,
1392
+ responseStatus: res.status,
1393
+ responseBody: respBody,
1394
+ error: { code: "rpc_error", message: `bundler ${method} rpc error: ${errMsg}` },
1395
+ elapsedMs
1396
+ });
1273
1397
  throw new BundlerClientError(
1274
1398
  "rpc_error",
1275
- `bundler ${method} rpc error: ${typeof err2.message === "string" ? err2.message : "<no message>"}`,
1399
+ `bundler ${method} rpc error: ${errMsg}`,
1276
1400
  { code: err2.code, message: err2.message, data: err2.data }
1277
1401
  );
1278
1402
  }
1403
+ this.pushTrace({
1404
+ method,
1405
+ id,
1406
+ requestBody,
1407
+ responseStatus: res.status,
1408
+ responseBody: respBody,
1409
+ elapsedMs
1410
+ });
1279
1411
  return obj.result;
1280
1412
  }
1281
1413
  };
@@ -2230,6 +2362,7 @@ async function attemptPathD(args, deps) {
2230
2362
  if (!deps.broker || !deps.bundler) {
2231
2363
  return { kind: "unconfigured" };
2232
2364
  }
2365
+ deps.bundler.drainTrace();
2233
2366
  if (!deps.subscriptionAddress) {
2234
2367
  return {
2235
2368
  kind: "fallback",
@@ -2462,7 +2595,7 @@ async function attemptPathD(args, deps) {
2462
2595
  return {
2463
2596
  kind: "fallback",
2464
2597
  reason: "paymaster_rejected",
2465
- message: `pm_sponsorUserOperation rejected${detail}: ${safeMsg}`
2598
+ message: `zd_sponsorUserOperation rejected${detail}: ${safeMsg}`
2466
2599
  };
2467
2600
  }
2468
2601
  const userOpForHash = {
@@ -2670,6 +2803,7 @@ async function positionBuy(input, deps) {
2670
2803
  let pathDFallbackReason;
2671
2804
  let pathDFallbackDetail;
2672
2805
  let pathDSubmittedUserOpHash;
2806
+ let pathDBundlerTrace;
2673
2807
  const pathD = await attemptPathD(
2674
2808
  { shares, tokenAddress: token.address, tokenSymbol: token.symbol },
2675
2809
  deps
@@ -2683,6 +2817,12 @@ async function positionBuy(input, deps) {
2683
2817
  if (pathD.submittedUserOpHash) {
2684
2818
  pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
2685
2819
  }
2820
+ if (deps.bundler) {
2821
+ const trace = deps.bundler.drainTrace();
2822
+ if (trace.length > 0) {
2823
+ pathDBundlerTrace = trace;
2824
+ }
2825
+ }
2686
2826
  }
2687
2827
  const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
2688
2828
  token: token.symbol,
@@ -2706,7 +2846,8 @@ ${dashboardUrl}`,
2706
2846
  navUsd6: navUsd6.toString(),
2707
2847
  ...pathDFallbackReason ? { pathDFallbackReason } : {},
2708
2848
  ...pathDFallbackDetail ? { pathDFallbackDetail } : {},
2709
- ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
2849
+ ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
2850
+ ...pathDBundlerTrace ? { pathDBundlerTrace } : {}
2710
2851
  }
2711
2852
  });
2712
2853
  }
@@ -3057,7 +3198,7 @@ var SERVER_NAME = "@muhaven/mcp";
3057
3198
  var SERVER_VERSION = resolveServerVersion();
3058
3199
  function resolveServerVersion() {
3059
3200
  {
3060
- return "0.2.6";
3201
+ return "0.2.8";
3061
3202
  }
3062
3203
  }
3063
3204
  function toJsonInputSchema(schema) {
@@ -3176,6 +3317,10 @@ function toolJsonResponse(payload) {
3176
3317
  }
3177
3318
  async function runMcpStdioCli(opts = {}) {
3178
3319
  const config = loadMcpConfig();
3320
+ process.stderr.write(
3321
+ `[muhaven-mcp] starting @muhaven/mcp@${SERVER_VERSION} (verbose=${process.env.MUHAVEN_MCP_VERBOSE === "1" ? "on" : "off"})
3322
+ `
3323
+ );
3179
3324
  const pinned = await loadPinnedToolHashes();
3180
3325
  const verify = verifyToolHashes(pinned);
3181
3326
  if (!verify.ok) {
package/dist/index.d.cts CHANGED
@@ -755,6 +755,43 @@ interface UserOperationReceipt {
755
755
  readonly blockHash: `0x${string}`;
756
756
  };
757
757
  }
758
+ /**
759
+ * Wave 5 Path D 0.2.8 — single bundler RPC round-trip captured for
760
+ * downstream diagnostic. Stored in a ring buffer on the client; the
761
+ * caller (attemptPathD) drains the buffer at each fallback return and
762
+ * inlines it into the `position.buy` echo's `pathDFallbackDetail`
763
+ * field. Claude Code's MCP client only captures subprocess stderr at
764
+ * connection handshake (NOT during tool calls — verified 2026-05-23
765
+ * via Claude Code's `mcp-logs-muhaven` JSONL files), so the previous
766
+ * stderr-only verbose path was invisible to operators. Returning the
767
+ * trace IN the tool response is the diagnostic surface that actually
768
+ * reaches the LLM context.
769
+ *
770
+ * Bodies truncated at 2KB each to keep the echo small + bounded.
771
+ * Sensitive fields (signatures) are kept since the LLM already sees
772
+ * the unsigned UserOp shape elsewhere — there's no incremental
773
+ * disclosure beyond what `pathDFallbackDetail`'s sanitized message
774
+ * already carries.
775
+ */
776
+ interface BundlerTraceEvent {
777
+ /** RPC method (e.g. `eth_call`, `eth_gasPrice`, `zd_sponsorUserOperation`). */
778
+ readonly method: string;
779
+ /** Monotonically-incrementing per-client RPC id. */
780
+ readonly id: number;
781
+ /** Request body (JSON), truncated. */
782
+ readonly requestBody: string;
783
+ /** HTTP status of the response. Undefined for transport-layer failures. */
784
+ readonly responseStatus?: number;
785
+ /** Response body (text), truncated. Undefined on transport-layer failures. */
786
+ readonly responseBody?: string;
787
+ /** Set when the rpc threw — captures the BundlerClientError code+message. */
788
+ readonly error?: {
789
+ code: string;
790
+ message: string;
791
+ };
792
+ /** Wall-clock ms elapsed (request start → response received / failure). */
793
+ readonly elapsedMs: number;
794
+ }
758
795
  interface BundlerClientOptions {
759
796
  /** Bundler RPC endpoint — MUHAVEN_BUNDLER_URL. https-or-loopback validated
760
797
  * at config-load time (see `config.ts::validatePublicUrlEnv`). */
@@ -789,7 +826,27 @@ declare class BundlerClient {
789
826
  private readonly options;
790
827
  private readonly fetchImpl;
791
828
  private nextRpcId;
829
+ /**
830
+ * Ring buffer of the most recent RPC round-trips. Surfaced via
831
+ * `drainTrace()` to the caller (attemptPathD) for inline diagnostic
832
+ * in the position.buy echo. Always populated (no env-gate) — bounded
833
+ * at 20 events × ~4KB each ≈ 80KB worst-case per client instance.
834
+ * Cheap enough to keep on for all installs so the next paymaster
835
+ * gate is self-diagnosing without a second binary spin.
836
+ */
837
+ private static readonly TRACE_BUFFER_SIZE;
838
+ private recentTrace;
792
839
  constructor(options: BundlerClientOptions);
840
+ /**
841
+ * Return AND CLEAR the in-memory trace ring. attemptPathD calls this
842
+ * right before constructing a fallback echo so the trace inlined
843
+ * into the echo corresponds to THAT smoke iteration (subsequent
844
+ * tool calls start with an empty buffer). Returns a copy so the
845
+ * caller can safely serialize without races against in-flight
846
+ * concurrent RPCs.
847
+ */
848
+ drainTrace(): readonly BundlerTraceEvent[];
849
+ private pushTrace;
793
850
  /**
794
851
  * Submit a signed UserOperation. Returns the userOpHash the bundler
795
852
  * computed (which must match the hash the broker signed — caller is
package/dist/index.d.ts CHANGED
@@ -755,6 +755,43 @@ interface UserOperationReceipt {
755
755
  readonly blockHash: `0x${string}`;
756
756
  };
757
757
  }
758
+ /**
759
+ * Wave 5 Path D 0.2.8 — single bundler RPC round-trip captured for
760
+ * downstream diagnostic. Stored in a ring buffer on the client; the
761
+ * caller (attemptPathD) drains the buffer at each fallback return and
762
+ * inlines it into the `position.buy` echo's `pathDFallbackDetail`
763
+ * field. Claude Code's MCP client only captures subprocess stderr at
764
+ * connection handshake (NOT during tool calls — verified 2026-05-23
765
+ * via Claude Code's `mcp-logs-muhaven` JSONL files), so the previous
766
+ * stderr-only verbose path was invisible to operators. Returning the
767
+ * trace IN the tool response is the diagnostic surface that actually
768
+ * reaches the LLM context.
769
+ *
770
+ * Bodies truncated at 2KB each to keep the echo small + bounded.
771
+ * Sensitive fields (signatures) are kept since the LLM already sees
772
+ * the unsigned UserOp shape elsewhere — there's no incremental
773
+ * disclosure beyond what `pathDFallbackDetail`'s sanitized message
774
+ * already carries.
775
+ */
776
+ interface BundlerTraceEvent {
777
+ /** RPC method (e.g. `eth_call`, `eth_gasPrice`, `zd_sponsorUserOperation`). */
778
+ readonly method: string;
779
+ /** Monotonically-incrementing per-client RPC id. */
780
+ readonly id: number;
781
+ /** Request body (JSON), truncated. */
782
+ readonly requestBody: string;
783
+ /** HTTP status of the response. Undefined for transport-layer failures. */
784
+ readonly responseStatus?: number;
785
+ /** Response body (text), truncated. Undefined on transport-layer failures. */
786
+ readonly responseBody?: string;
787
+ /** Set when the rpc threw — captures the BundlerClientError code+message. */
788
+ readonly error?: {
789
+ code: string;
790
+ message: string;
791
+ };
792
+ /** Wall-clock ms elapsed (request start → response received / failure). */
793
+ readonly elapsedMs: number;
794
+ }
758
795
  interface BundlerClientOptions {
759
796
  /** Bundler RPC endpoint — MUHAVEN_BUNDLER_URL. https-or-loopback validated
760
797
  * at config-load time (see `config.ts::validatePublicUrlEnv`). */
@@ -789,7 +826,27 @@ declare class BundlerClient {
789
826
  private readonly options;
790
827
  private readonly fetchImpl;
791
828
  private nextRpcId;
829
+ /**
830
+ * Ring buffer of the most recent RPC round-trips. Surfaced via
831
+ * `drainTrace()` to the caller (attemptPathD) for inline diagnostic
832
+ * in the position.buy echo. Always populated (no env-gate) — bounded
833
+ * at 20 events × ~4KB each ≈ 80KB worst-case per client instance.
834
+ * Cheap enough to keep on for all installs so the next paymaster
835
+ * gate is self-diagnosing without a second binary spin.
836
+ */
837
+ private static readonly TRACE_BUFFER_SIZE;
838
+ private recentTrace;
792
839
  constructor(options: BundlerClientOptions);
840
+ /**
841
+ * Return AND CLEAR the in-memory trace ring. attemptPathD calls this
842
+ * right before constructing a fallback echo so the trace inlined
843
+ * into the echo corresponds to THAT smoke iteration (subsequent
844
+ * tool calls start with an empty buffer). Returns a copy so the
845
+ * caller can safely serialize without races against in-flight
846
+ * concurrent RPCs.
847
+ */
848
+ drainTrace(): readonly BundlerTraceEvent[];
849
+ private pushTrace;
793
850
  /**
794
851
  * Submit a signed UserOperation. Returns the userOpHash the bundler
795
852
  * computed (which must match the hash the broker signed — caller is
package/dist/index.js CHANGED
@@ -995,7 +995,7 @@ var BundlerClientError = class extends Error {
995
995
  code;
996
996
  detail;
997
997
  };
998
- var BundlerClient = class {
998
+ var BundlerClient = class _BundlerClient {
999
999
  constructor(options) {
1000
1000
  this.options = options;
1001
1001
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -1003,6 +1003,35 @@ var BundlerClient = class {
1003
1003
  options;
1004
1004
  fetchImpl;
1005
1005
  nextRpcId = 1;
1006
+ /**
1007
+ * Ring buffer of the most recent RPC round-trips. Surfaced via
1008
+ * `drainTrace()` to the caller (attemptPathD) for inline diagnostic
1009
+ * in the position.buy echo. Always populated (no env-gate) — bounded
1010
+ * at 20 events × ~4KB each ≈ 80KB worst-case per client instance.
1011
+ * Cheap enough to keep on for all installs so the next paymaster
1012
+ * gate is self-diagnosing without a second binary spin.
1013
+ */
1014
+ static TRACE_BUFFER_SIZE = 20;
1015
+ recentTrace = [];
1016
+ /**
1017
+ * Return AND CLEAR the in-memory trace ring. attemptPathD calls this
1018
+ * right before constructing a fallback echo so the trace inlined
1019
+ * into the echo corresponds to THAT smoke iteration (subsequent
1020
+ * tool calls start with an empty buffer). Returns a copy so the
1021
+ * caller can safely serialize without races against in-flight
1022
+ * concurrent RPCs.
1023
+ */
1024
+ drainTrace() {
1025
+ const snapshot = this.recentTrace.slice();
1026
+ this.recentTrace = [];
1027
+ return snapshot;
1028
+ }
1029
+ pushTrace(event) {
1030
+ this.recentTrace.push(event);
1031
+ if (this.recentTrace.length > _BundlerClient.TRACE_BUFFER_SIZE) {
1032
+ this.recentTrace.shift();
1033
+ }
1034
+ }
1006
1035
  /**
1007
1036
  * Submit a signed UserOperation. Returns the userOpHash the bundler
1008
1037
  * computed (which must match the hash the broker signed — caller is
@@ -1207,6 +1236,16 @@ var BundlerClient = class {
1207
1236
  async rpc(method, params) {
1208
1237
  const id = this.nextRpcId++;
1209
1238
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
1239
+ const verbose = process.env.MUHAVEN_MCP_VERBOSE === "1";
1240
+ const truncate = (s) => s.length > 2048 ? s.slice(0, 2048) + "\u2026(truncated)" : s;
1241
+ const requestBody = truncate(body);
1242
+ const startMs = Date.now();
1243
+ if (verbose) {
1244
+ process.stderr.write(
1245
+ `[muhaven-mcp] [bundler\u2192] ${method} id=${id} body=${requestBody}
1246
+ `
1247
+ );
1248
+ }
1210
1249
  const ctrl = new AbortController();
1211
1250
  const timer = setTimeout(() => ctrl.abort(), this.options.requestTimeoutMs);
1212
1251
  let res;
@@ -1226,12 +1265,38 @@ var BundlerClient = class {
1226
1265
  });
1227
1266
  } catch (err2) {
1228
1267
  clearTimeout(timer);
1268
+ const elapsedMs2 = Date.now() - startMs;
1229
1269
  if (err2.name === "AbortError") {
1270
+ if (verbose) {
1271
+ process.stderr.write(`[muhaven-mcp] [bundler\u2717] ${method} id=${id} timeout
1272
+ `);
1273
+ }
1274
+ this.pushTrace({
1275
+ method,
1276
+ id,
1277
+ requestBody,
1278
+ error: { code: "timeout", message: `bundler ${method} timed out` },
1279
+ elapsedMs: elapsedMs2
1280
+ });
1230
1281
  throw new BundlerClientError("timeout", `bundler ${method} timed out`);
1231
1282
  }
1283
+ const netMsg = err2 instanceof Error ? err2.message : String(err2);
1284
+ if (verbose) {
1285
+ process.stderr.write(
1286
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} network err=${netMsg}
1287
+ `
1288
+ );
1289
+ }
1290
+ this.pushTrace({
1291
+ method,
1292
+ id,
1293
+ requestBody,
1294
+ error: { code: "network", message: `bundler ${method} network error: ${netMsg}` },
1295
+ elapsedMs: elapsedMs2
1296
+ });
1232
1297
  throw new BundlerClientError(
1233
1298
  "network",
1234
- `bundler ${method} network error: ${err2 instanceof Error ? err2.message : String(err2)}`,
1299
+ `bundler ${method} network error: ${netMsg}`,
1235
1300
  err2
1236
1301
  );
1237
1302
  } finally {
@@ -1243,6 +1308,24 @@ var BundlerClient = class {
1243
1308
  text = (await res.text()).slice(0, 256);
1244
1309
  } catch {
1245
1310
  }
1311
+ if (verbose) {
1312
+ process.stderr.write(
1313
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} HTTP ${res.status} body=${text}
1314
+ `
1315
+ );
1316
+ }
1317
+ this.pushTrace({
1318
+ method,
1319
+ id,
1320
+ requestBody,
1321
+ responseStatus: res.status,
1322
+ responseBody: text,
1323
+ error: {
1324
+ code: "http_error",
1325
+ message: `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
1326
+ },
1327
+ elapsedMs: Date.now() - startMs
1328
+ });
1246
1329
  throw new BundlerClientError(
1247
1330
  "http_error",
1248
1331
  `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
@@ -1252,12 +1335,43 @@ var BundlerClient = class {
1252
1335
  try {
1253
1336
  parsed = await res.json();
1254
1337
  } catch (err2) {
1338
+ const jsonErr = err2 instanceof Error ? err2.message : String(err2);
1339
+ if (verbose) {
1340
+ process.stderr.write(
1341
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} non-JSON err=${jsonErr}
1342
+ `
1343
+ );
1344
+ }
1345
+ this.pushTrace({
1346
+ method,
1347
+ id,
1348
+ requestBody,
1349
+ responseStatus: res.status,
1350
+ error: { code: "invalid_response", message: `bundler ${method} returned non-JSON: ${jsonErr}` },
1351
+ elapsedMs: Date.now() - startMs
1352
+ });
1255
1353
  throw new BundlerClientError(
1256
1354
  "invalid_response",
1257
- `bundler ${method} returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
1355
+ `bundler ${method} returned non-JSON: ${jsonErr}`
1258
1356
  );
1259
1357
  }
1358
+ const respDump = JSON.stringify(parsed);
1359
+ const respBody = truncate(respDump);
1360
+ if (verbose) {
1361
+ process.stderr.write(`[muhaven-mcp] [bundler\u2190] ${method} id=${id} resp=${respBody}
1362
+ `);
1363
+ }
1364
+ const elapsedMs = Date.now() - startMs;
1260
1365
  if (typeof parsed !== "object" || parsed === null) {
1366
+ this.pushTrace({
1367
+ method,
1368
+ id,
1369
+ requestBody,
1370
+ responseStatus: res.status,
1371
+ responseBody: respBody,
1372
+ error: { code: "invalid_response", message: `bundler ${method} returned non-object` },
1373
+ elapsedMs
1374
+ });
1261
1375
  throw new BundlerClientError(
1262
1376
  "invalid_response",
1263
1377
  `bundler ${method} returned non-object`
@@ -1266,12 +1380,30 @@ var BundlerClient = class {
1266
1380
  const obj = parsed;
1267
1381
  if (obj.error !== void 0 && obj.error !== null) {
1268
1382
  const err2 = obj.error;
1383
+ const errMsg = typeof err2.message === "string" ? err2.message : "<no message>";
1384
+ this.pushTrace({
1385
+ method,
1386
+ id,
1387
+ requestBody,
1388
+ responseStatus: res.status,
1389
+ responseBody: respBody,
1390
+ error: { code: "rpc_error", message: `bundler ${method} rpc error: ${errMsg}` },
1391
+ elapsedMs
1392
+ });
1269
1393
  throw new BundlerClientError(
1270
1394
  "rpc_error",
1271
- `bundler ${method} rpc error: ${typeof err2.message === "string" ? err2.message : "<no message>"}`,
1395
+ `bundler ${method} rpc error: ${errMsg}`,
1272
1396
  { code: err2.code, message: err2.message, data: err2.data }
1273
1397
  );
1274
1398
  }
1399
+ this.pushTrace({
1400
+ method,
1401
+ id,
1402
+ requestBody,
1403
+ responseStatus: res.status,
1404
+ responseBody: respBody,
1405
+ elapsedMs
1406
+ });
1275
1407
  return obj.result;
1276
1408
  }
1277
1409
  };
@@ -2226,6 +2358,7 @@ async function attemptPathD(args, deps) {
2226
2358
  if (!deps.broker || !deps.bundler) {
2227
2359
  return { kind: "unconfigured" };
2228
2360
  }
2361
+ deps.bundler.drainTrace();
2229
2362
  if (!deps.subscriptionAddress) {
2230
2363
  return {
2231
2364
  kind: "fallback",
@@ -2458,7 +2591,7 @@ async function attemptPathD(args, deps) {
2458
2591
  return {
2459
2592
  kind: "fallback",
2460
2593
  reason: "paymaster_rejected",
2461
- message: `pm_sponsorUserOperation rejected${detail}: ${safeMsg}`
2594
+ message: `zd_sponsorUserOperation rejected${detail}: ${safeMsg}`
2462
2595
  };
2463
2596
  }
2464
2597
  const userOpForHash = {
@@ -2666,6 +2799,7 @@ async function positionBuy(input, deps) {
2666
2799
  let pathDFallbackReason;
2667
2800
  let pathDFallbackDetail;
2668
2801
  let pathDSubmittedUserOpHash;
2802
+ let pathDBundlerTrace;
2669
2803
  const pathD = await attemptPathD(
2670
2804
  { shares, tokenAddress: token.address, tokenSymbol: token.symbol },
2671
2805
  deps
@@ -2679,6 +2813,12 @@ async function positionBuy(input, deps) {
2679
2813
  if (pathD.submittedUserOpHash) {
2680
2814
  pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
2681
2815
  }
2816
+ if (deps.bundler) {
2817
+ const trace = deps.bundler.drainTrace();
2818
+ if (trace.length > 0) {
2819
+ pathDBundlerTrace = trace;
2820
+ }
2821
+ }
2682
2822
  }
2683
2823
  const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
2684
2824
  token: token.symbol,
@@ -2702,7 +2842,8 @@ ${dashboardUrl}`,
2702
2842
  navUsd6: navUsd6.toString(),
2703
2843
  ...pathDFallbackReason ? { pathDFallbackReason } : {},
2704
2844
  ...pathDFallbackDetail ? { pathDFallbackDetail } : {},
2705
- ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
2845
+ ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
2846
+ ...pathDBundlerTrace ? { pathDBundlerTrace } : {}
2706
2847
  }
2707
2848
  });
2708
2849
  }
@@ -3053,7 +3194,7 @@ var SERVER_NAME = "@muhaven/mcp";
3053
3194
  var SERVER_VERSION = resolveServerVersion();
3054
3195
  function resolveServerVersion() {
3055
3196
  {
3056
- return "0.2.6";
3197
+ return "0.2.8";
3057
3198
  }
3058
3199
  }
3059
3200
  function toJsonInputSchema(schema) {
@@ -3172,6 +3313,10 @@ function toolJsonResponse(payload) {
3172
3313
  }
3173
3314
  async function runMcpStdioCli(opts = {}) {
3174
3315
  const config = loadMcpConfig();
3316
+ process.stderr.write(
3317
+ `[muhaven-mcp] starting @muhaven/mcp@${SERVER_VERSION} (verbose=${process.env.MUHAVEN_MCP_VERBOSE === "1" ? "on" : "off"})
3318
+ `
3319
+ );
3175
3320
  const pinned = await loadPinnedToolHashes();
3176
3321
  const verify = verifyToolHashes(pinned);
3177
3322
  if (!verify.ok) {
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.6",
6
+ "version": "0.2.8",
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.6",
3
+ "version": "0.2.8",
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": {