@muhaven/mcp 0.2.7 → 0.2.9

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,106 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.9] — 2026-05-23
11
+
12
+ ### Added
13
+
14
+ - **`pathDDecodedCall` in the position.buy echo on Path D fallback.**
15
+ When the bundler trace contains a `zd_sponsorUserOperation` event,
16
+ the handler now decodes the userOp.callData inline and surfaces:
17
+
18
+ pathDDecodedCall: {
19
+ sender: '0x<kernel>',
20
+ kernelExecuteMode: '0x<32 bytes>',
21
+ kernelExecuteTarget: '0x<addr>', // should == MuHavenSubscription
22
+ kernelExecuteValue: '0',
23
+ innerSelector: '0xd29b624b', // purchase(...)
24
+ innerPurchaseTokenArg: '0x<addr>', // the RWA token (MuHavenToken)
25
+ innerPurchaseMaxSharesHint: '14',
26
+ innerPurchaseEphemeralEOA: '0x<addr>',
27
+ expectedSubscriptionAddress: '0x<addr>', // MCP env wiring
28
+ kernelExecuteTargetMatchesSubscription: true|false,
29
+ interpretation: 'kernel.execute target matches MuHavenSubscription. ...',
30
+ }
31
+
32
+ Lets the LLM read at-a-glance which contract the kernel was about
33
+ to call (kernel.execute target) vs which RWA token the inner
34
+ purchase() carries — the two are commonly confused (the inner
35
+ purchase's first arg IS the token address, but the kernel.execute
36
+ target should be the subscription).
37
+
38
+ The `interpretation` field branches on three cases:
39
+ 1. target == expected subscription → "shape is correct; AA23 is
40
+ downstream of validator signature decode; check
41
+ muhaven.policy.session_key_status."
42
+ 2. target == inner purchase.token → "kernel is dispatching to
43
+ the token instead of the subscription — code bug."
44
+ 3. Anything else → "unexpected third-address dispatch."
45
+
46
+ - **`decodeKernelExecuteSingleCall` exported from
47
+ `clients/kernel-encoder.ts`.** Sibling to the existing encoder;
48
+ reuses the same KERNEL_EXECUTE_ABI + single-call default mode.
49
+ Returns `null` on unsupported modes (batch / delegate / try) so
50
+ the diagnostic emits a clear gap instead of garbage.
51
+
52
+ - **`scripts/decode-userop-trace.ts`** — standalone CLI that takes
53
+ the raw `pathDBundlerTrace` JSON via stdin and prints the same
54
+ decode + cross-check. Useful when triaging against an older MCP
55
+ version that doesn't ship `pathDDecodedCall`, or for offline
56
+ analysis from a saved trace.
57
+
58
+ ## [0.2.8] — 2026-05-23
59
+
60
+ ### Added
61
+
62
+ - **`pathDBundlerTrace` inline in the `muhaven.position.buy` echo on
63
+ fallback.** Every Path D attempt now buffers its bundler RPC
64
+ round-trips in a 20-event ring on the `BundlerClient`; on any
65
+ fallback return the handler drains the ring and inlines it into
66
+ the echo:
67
+
68
+ pathDBundlerTrace: [
69
+ {
70
+ method: 'eth_call',
71
+ id: 12,
72
+ requestBody: '{"jsonrpc":"2.0","id":12,"method":"eth_call",...}',
73
+ responseStatus: 200,
74
+ responseBody: '{"jsonrpc":"2.0","id":12,"result":"0x..."}',
75
+ elapsedMs: 87,
76
+ },
77
+ {
78
+ method: 'zd_sponsorUserOperation',
79
+ id: 14,
80
+ requestBody: '{"jsonrpc":"2.0","method":"zd_sponsorUserOperation","params":[...]}',
81
+ responseStatus: 400,
82
+ responseBody: '{"error":"AA23 reverted ..."}',
83
+ error: { code: 'http_error', message: '...' },
84
+ elapsedMs: 412,
85
+ },
86
+ ]
87
+
88
+ Bodies truncated at 2KB. The LLM (and the operator reading the
89
+ tool result) sees the EXACT wire payload that the bundler /
90
+ paymaster rejected — no need for curl repro or Claude Code
91
+ subprocess log digging. Confirmed 2026-05-23 that Claude Code's
92
+ MCP client only captures subprocess stderr at handshake (not
93
+ during tool calls), so the 0.2.7 stderr-verbose path never reached
94
+ the LLM context during the smoke iterations.
95
+
96
+ Ring buffer is always-on (no env-gate, ~80KB worst-case per
97
+ BundlerClient instance) so the next gate is self-diagnosing
98
+ immediately without a second `npm i -g` cycle.
99
+
100
+ `BundlerClient.drainTrace()` returns + clears the ring; called at
101
+ the start of `attemptPathD` (clear stale) and again right before
102
+ the fallback echo (collect + inline).
103
+
104
+ ### Tests
105
+
106
+ - bundler-client.test.ts: new ring-buffer behaviour cases (drain
107
+ returns a copy; drain clears; ring is bounded at 20; populates on
108
+ success + http_error + timeout + rpc_error).
109
+
10
110
  ## [0.2.7] — 2026-05-23
11
111
 
12
112
  ### Added
package/dist/broker.cjs CHANGED
@@ -2783,7 +2783,7 @@ function printUsage() {
2783
2783
  }
2784
2784
  function getBrokerPackageVersion() {
2785
2785
  {
2786
- return "0.2.7";
2786
+ return "0.2.9";
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.7";
2788
+ return "0.2.9";
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
@@ -1212,10 +1241,12 @@ var BundlerClient = class {
1212
1241
  const id = this.nextRpcId++;
1213
1242
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
1214
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();
1215
1247
  if (verbose) {
1216
- const bodyDump = body.length > 2048 ? body.slice(0, 2048) + "\u2026(truncated)" : body;
1217
1248
  process.stderr.write(
1218
- `[muhaven-mcp] [bundler\u2192] ${method} id=${id} body=${bodyDump}
1249
+ `[muhaven-mcp] [bundler\u2192] ${method} id=${id} body=${requestBody}
1219
1250
  `
1220
1251
  );
1221
1252
  }
@@ -1238,22 +1269,38 @@ var BundlerClient = class {
1238
1269
  });
1239
1270
  } catch (err2) {
1240
1271
  clearTimeout(timer);
1272
+ const elapsedMs2 = Date.now() - startMs;
1241
1273
  if (err2.name === "AbortError") {
1242
1274
  if (verbose) {
1243
1275
  process.stderr.write(`[muhaven-mcp] [bundler\u2717] ${method} id=${id} timeout
1244
1276
  `);
1245
1277
  }
1278
+ this.pushTrace({
1279
+ method,
1280
+ id,
1281
+ requestBody,
1282
+ error: { code: "timeout", message: `bundler ${method} timed out` },
1283
+ elapsedMs: elapsedMs2
1284
+ });
1246
1285
  throw new BundlerClientError("timeout", `bundler ${method} timed out`);
1247
1286
  }
1287
+ const netMsg = err2 instanceof Error ? err2.message : String(err2);
1248
1288
  if (verbose) {
1249
1289
  process.stderr.write(
1250
- `[muhaven-mcp] [bundler\u2717] ${method} id=${id} network err=${err2 instanceof Error ? err2.message : String(err2)}
1290
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} network err=${netMsg}
1251
1291
  `
1252
1292
  );
1253
1293
  }
1294
+ this.pushTrace({
1295
+ method,
1296
+ id,
1297
+ requestBody,
1298
+ error: { code: "network", message: `bundler ${method} network error: ${netMsg}` },
1299
+ elapsedMs: elapsedMs2
1300
+ });
1254
1301
  throw new BundlerClientError(
1255
1302
  "network",
1256
- `bundler ${method} network error: ${err2 instanceof Error ? err2.message : String(err2)}`,
1303
+ `bundler ${method} network error: ${netMsg}`,
1257
1304
  err2
1258
1305
  );
1259
1306
  } finally {
@@ -1271,6 +1318,18 @@ var BundlerClient = class {
1271
1318
  `
1272
1319
  );
1273
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
+ });
1274
1333
  throw new BundlerClientError(
1275
1334
  "http_error",
1276
1335
  `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
@@ -1280,24 +1339,43 @@ var BundlerClient = class {
1280
1339
  try {
1281
1340
  parsed = await res.json();
1282
1341
  } catch (err2) {
1342
+ const jsonErr = err2 instanceof Error ? err2.message : String(err2);
1283
1343
  if (verbose) {
1284
1344
  process.stderr.write(
1285
- `[muhaven-mcp] [bundler\u2717] ${method} id=${id} non-JSON err=${err2 instanceof Error ? err2.message : String(err2)}
1345
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} non-JSON err=${jsonErr}
1286
1346
  `
1287
1347
  );
1288
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
+ });
1289
1357
  throw new BundlerClientError(
1290
1358
  "invalid_response",
1291
- `bundler ${method} returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
1359
+ `bundler ${method} returned non-JSON: ${jsonErr}`
1292
1360
  );
1293
1361
  }
1362
+ const respDump = JSON.stringify(parsed);
1363
+ const respBody = truncate(respDump);
1294
1364
  if (verbose) {
1295
- const respDump = JSON.stringify(parsed);
1296
- const trunc = respDump.length > 2048 ? respDump.slice(0, 2048) + "\u2026(truncated)" : respDump;
1297
- process.stderr.write(`[muhaven-mcp] [bundler\u2190] ${method} id=${id} resp=${trunc}
1365
+ process.stderr.write(`[muhaven-mcp] [bundler\u2190] ${method} id=${id} resp=${respBody}
1298
1366
  `);
1299
1367
  }
1368
+ const elapsedMs = Date.now() - startMs;
1300
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
+ });
1301
1379
  throw new BundlerClientError(
1302
1380
  "invalid_response",
1303
1381
  `bundler ${method} returned non-object`
@@ -1306,12 +1384,30 @@ var BundlerClient = class {
1306
1384
  const obj = parsed;
1307
1385
  if (obj.error !== void 0 && obj.error !== null) {
1308
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
+ });
1309
1397
  throw new BundlerClientError(
1310
1398
  "rpc_error",
1311
- `bundler ${method} rpc error: ${typeof err2.message === "string" ? err2.message : "<no message>"}`,
1399
+ `bundler ${method} rpc error: ${errMsg}`,
1312
1400
  { code: err2.code, message: err2.message, data: err2.data }
1313
1401
  );
1314
1402
  }
1403
+ this.pushTrace({
1404
+ method,
1405
+ id,
1406
+ requestBody,
1407
+ responseStatus: res.status,
1408
+ responseBody: respBody,
1409
+ elapsedMs
1410
+ });
1315
1411
  return obj.result;
1316
1412
  }
1317
1413
  };
@@ -1872,6 +1968,26 @@ function encodeKernelExecuteSingleCall(input) {
1872
1968
  args: [KERNEL_V3_SINGLE_CALL_MODE_DEFAULT, executionCalldata]
1873
1969
  });
1874
1970
  }
1971
+ function decodeKernelExecuteSingleCall(data) {
1972
+ let decoded;
1973
+ try {
1974
+ decoded = viem.decodeFunctionData({ abi: KERNEL_EXECUTE_ABI, data });
1975
+ } catch {
1976
+ return null;
1977
+ }
1978
+ const [mode, executionCalldata] = decoded.args;
1979
+ if (mode !== KERNEL_V3_SINGLE_CALL_MODE_DEFAULT) {
1980
+ return null;
1981
+ }
1982
+ const ec = executionCalldata.slice(2);
1983
+ if (ec.length < 20 * 2 + 32 * 2) {
1984
+ return null;
1985
+ }
1986
+ const target = `0x${ec.slice(0, 40)}`;
1987
+ const value = BigInt(`0x${ec.slice(40, 40 + 64)}`);
1988
+ const innerCallData = `0x${ec.slice(40 + 64)}`;
1989
+ return { mode, target, value, innerCallData };
1990
+ }
1875
1991
  var ECDSA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
1876
1992
  var PERMISSION_USE_PREFIX = "0xff";
1877
1993
  function buildKernelSessionKeySignature(input) {
@@ -2261,11 +2377,85 @@ async function syncSnapshotFromMirror(deps, brokerSignerAddress) {
2261
2377
  }
2262
2378
  return { kind: "ok", sessionId: activeId };
2263
2379
  }
2380
+ var PURCHASE_SELECTOR_LOWER = SUBSCRIPTION_PURCHASE_SELECTOR.toLowerCase();
2381
+ function buildPathDDecodedCall(trace, deps) {
2382
+ const sponsorEvent = trace.find((e) => e.method === "zd_sponsorUserOperation");
2383
+ if (!sponsorEvent) return void 0;
2384
+ let req;
2385
+ try {
2386
+ req = JSON.parse(sponsorEvent.requestBody);
2387
+ } catch {
2388
+ return void 0;
2389
+ }
2390
+ const userOp = req.params?.[0]?.userOp;
2391
+ if (!userOp || typeof userOp.callData !== "string" || !userOp.callData.startsWith("0x")) {
2392
+ return void 0;
2393
+ }
2394
+ const sender = userOp.sender ?? "<missing>";
2395
+ const decoded = decodeKernelExecuteSingleCall(userOp.callData);
2396
+ if (!decoded) {
2397
+ return {
2398
+ sender,
2399
+ kernelExecuteMode: "<undecodable>",
2400
+ kernelExecuteTarget: "<undecodable>",
2401
+ kernelExecuteValue: "<undecodable>",
2402
+ innerSelector: "<undecodable>",
2403
+ interpretation: "kernel.execute callData could not be decoded as single-call default mode. The mode word is non-zero or the executionCalldata layout is unexpected. Manual decode required."
2404
+ };
2405
+ }
2406
+ const innerSelector = decoded.innerCallData.slice(0, 10).toLowerCase();
2407
+ let innerPurchaseTokenArg;
2408
+ let innerPurchaseMaxSharesHint;
2409
+ let innerPurchaseEphemeralEOA;
2410
+ if (innerSelector === PURCHASE_SELECTOR_LOWER) {
2411
+ try {
2412
+ const inner = viem.decodeFunctionData({
2413
+ abi: SUBSCRIPTION_PURCHASE_ABI,
2414
+ data: decoded.innerCallData
2415
+ });
2416
+ const [tokenArg, , maxSharesHint, ephemeralEOA] = inner.args;
2417
+ innerPurchaseTokenArg = tokenArg;
2418
+ innerPurchaseMaxSharesHint = maxSharesHint.toString();
2419
+ innerPurchaseEphemeralEOA = ephemeralEOA;
2420
+ } catch {
2421
+ }
2422
+ }
2423
+ const expectedSubscriptionAddress = deps.subscriptionAddress?.toLowerCase();
2424
+ const kernelTargetLower = decoded.target.toLowerCase();
2425
+ let kernelExecuteTargetMatchesSubscription;
2426
+ let interpretation;
2427
+ if (expectedSubscriptionAddress === void 0) {
2428
+ interpretation = `kernel.execute target=${decoded.target}; inner purchase token=${innerPurchaseTokenArg ?? "<unknown>"}; MUHAVEN_SUBSCRIPTION_ADDRESS not wired on this MCP server \u2014 cannot cross-check.`;
2429
+ } else if (kernelTargetLower === expectedSubscriptionAddress) {
2430
+ kernelExecuteTargetMatchesSubscription = true;
2431
+ interpretation = `kernel.execute target matches MuHavenSubscription (${decoded.target}). Inner purchase token = ${innerPurchaseTokenArg ?? "<unknown>"}. The shape is correct; the AA23 revert is downstream of the validator's signature decode \u2014 likely either an on-chain signer-vs-installed-permission mismatch, a target/selector not in the on-chain policy, or a cap-arg breach. Check muhaven.policy.session_key_status for the installed permission state.`;
2432
+ } else if (innerPurchaseTokenArg !== void 0 && kernelTargetLower === innerPurchaseTokenArg.toLowerCase()) {
2433
+ kernelExecuteTargetMatchesSubscription = false;
2434
+ interpretation = `kernel.execute target = ${decoded.target} = the RWA MuHavenToken (purchase.token arg0). Expected MuHavenSubscription (${deps.subscriptionAddress}). The kernel is dispatching purchase() to the token contract instead of the subscription \u2014 token doesn't have a purchase() selector, so fallback returns empty revert data (= AA23 reverted 0x). This is a code-side bug in the kernel.execute target wiring.`;
2435
+ } else {
2436
+ kernelExecuteTargetMatchesSubscription = false;
2437
+ interpretation = `kernel.execute target = ${decoded.target} \u2014 NEITHER the expected MuHavenSubscription (${deps.subscriptionAddress}) NOR the inner purchase.token arg (${innerPurchaseTokenArg ?? "<none>"}). This is an unexpected third-address dispatch; inspect deps.subscriptionAddress env wiring.`;
2438
+ }
2439
+ return {
2440
+ sender,
2441
+ kernelExecuteMode: decoded.mode,
2442
+ kernelExecuteTarget: decoded.target,
2443
+ kernelExecuteValue: decoded.value.toString(),
2444
+ innerSelector,
2445
+ innerPurchaseTokenArg,
2446
+ innerPurchaseMaxSharesHint,
2447
+ innerPurchaseEphemeralEOA,
2448
+ expectedSubscriptionAddress: deps.subscriptionAddress,
2449
+ kernelExecuteTargetMatchesSubscription,
2450
+ interpretation
2451
+ };
2452
+ }
2264
2453
  async function attemptPathD(args, deps) {
2265
2454
  const { shares, tokenAddress, tokenSymbol } = args;
2266
2455
  if (!deps.broker || !deps.bundler) {
2267
2456
  return { kind: "unconfigured" };
2268
2457
  }
2458
+ deps.bundler.drainTrace();
2269
2459
  if (!deps.subscriptionAddress) {
2270
2460
  return {
2271
2461
  kind: "fallback",
@@ -2706,6 +2896,8 @@ async function positionBuy(input, deps) {
2706
2896
  let pathDFallbackReason;
2707
2897
  let pathDFallbackDetail;
2708
2898
  let pathDSubmittedUserOpHash;
2899
+ let pathDBundlerTrace;
2900
+ let pathDDecodedCall;
2709
2901
  const pathD = await attemptPathD(
2710
2902
  { shares, tokenAddress: token.address, tokenSymbol: token.symbol },
2711
2903
  deps
@@ -2719,6 +2911,13 @@ async function positionBuy(input, deps) {
2719
2911
  if (pathD.submittedUserOpHash) {
2720
2912
  pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
2721
2913
  }
2914
+ if (deps.bundler) {
2915
+ const trace = deps.bundler.drainTrace();
2916
+ if (trace.length > 0) {
2917
+ pathDBundlerTrace = trace;
2918
+ pathDDecodedCall = buildPathDDecodedCall(trace, deps);
2919
+ }
2920
+ }
2722
2921
  }
2723
2922
  const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
2724
2923
  token: token.symbol,
@@ -2742,7 +2941,9 @@ ${dashboardUrl}`,
2742
2941
  navUsd6: navUsd6.toString(),
2743
2942
  ...pathDFallbackReason ? { pathDFallbackReason } : {},
2744
2943
  ...pathDFallbackDetail ? { pathDFallbackDetail } : {},
2745
- ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
2944
+ ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
2945
+ ...pathDBundlerTrace ? { pathDBundlerTrace } : {},
2946
+ ...pathDDecodedCall ? { pathDDecodedCall } : {}
2746
2947
  }
2747
2948
  });
2748
2949
  }
@@ -3093,7 +3294,7 @@ var SERVER_NAME = "@muhaven/mcp";
3093
3294
  var SERVER_VERSION = resolveServerVersion();
3094
3295
  function resolveServerVersion() {
3095
3296
  {
3096
- return "0.2.7";
3297
+ return "0.2.9";
3097
3298
  }
3098
3299
  }
3099
3300
  function toJsonInputSchema(schema) {
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
@@ -10,7 +10,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
10
10
  import { platform, homedir } from 'os';
11
11
  import { connect, createServer } from 'net';
12
12
  import { setTimeout as setTimeout$1 } from 'timers/promises';
13
- import { parseAbi, toFunctionSelector, encodeFunctionData, encodePacked, pad, concatHex, decodeAbiParameters } from 'viem';
13
+ import { parseAbi, toFunctionSelector, encodeFunctionData, decodeFunctionData, encodePacked, pad, concatHex, decodeAbiParameters } from 'viem';
14
14
  import { createHash, randomBytes } from 'crypto';
15
15
  import { getUserOperationHash } from 'viem/account-abstraction';
16
16
  import { privateKeyToAccount } from 'viem/accounts';
@@ -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
@@ -1208,10 +1237,12 @@ var BundlerClient = class {
1208
1237
  const id = this.nextRpcId++;
1209
1238
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
1210
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();
1211
1243
  if (verbose) {
1212
- const bodyDump = body.length > 2048 ? body.slice(0, 2048) + "\u2026(truncated)" : body;
1213
1244
  process.stderr.write(
1214
- `[muhaven-mcp] [bundler\u2192] ${method} id=${id} body=${bodyDump}
1245
+ `[muhaven-mcp] [bundler\u2192] ${method} id=${id} body=${requestBody}
1215
1246
  `
1216
1247
  );
1217
1248
  }
@@ -1234,22 +1265,38 @@ var BundlerClient = class {
1234
1265
  });
1235
1266
  } catch (err2) {
1236
1267
  clearTimeout(timer);
1268
+ const elapsedMs2 = Date.now() - startMs;
1237
1269
  if (err2.name === "AbortError") {
1238
1270
  if (verbose) {
1239
1271
  process.stderr.write(`[muhaven-mcp] [bundler\u2717] ${method} id=${id} timeout
1240
1272
  `);
1241
1273
  }
1274
+ this.pushTrace({
1275
+ method,
1276
+ id,
1277
+ requestBody,
1278
+ error: { code: "timeout", message: `bundler ${method} timed out` },
1279
+ elapsedMs: elapsedMs2
1280
+ });
1242
1281
  throw new BundlerClientError("timeout", `bundler ${method} timed out`);
1243
1282
  }
1283
+ const netMsg = err2 instanceof Error ? err2.message : String(err2);
1244
1284
  if (verbose) {
1245
1285
  process.stderr.write(
1246
- `[muhaven-mcp] [bundler\u2717] ${method} id=${id} network err=${err2 instanceof Error ? err2.message : String(err2)}
1286
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} network err=${netMsg}
1247
1287
  `
1248
1288
  );
1249
1289
  }
1290
+ this.pushTrace({
1291
+ method,
1292
+ id,
1293
+ requestBody,
1294
+ error: { code: "network", message: `bundler ${method} network error: ${netMsg}` },
1295
+ elapsedMs: elapsedMs2
1296
+ });
1250
1297
  throw new BundlerClientError(
1251
1298
  "network",
1252
- `bundler ${method} network error: ${err2 instanceof Error ? err2.message : String(err2)}`,
1299
+ `bundler ${method} network error: ${netMsg}`,
1253
1300
  err2
1254
1301
  );
1255
1302
  } finally {
@@ -1267,6 +1314,18 @@ var BundlerClient = class {
1267
1314
  `
1268
1315
  );
1269
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
+ });
1270
1329
  throw new BundlerClientError(
1271
1330
  "http_error",
1272
1331
  `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
@@ -1276,24 +1335,43 @@ var BundlerClient = class {
1276
1335
  try {
1277
1336
  parsed = await res.json();
1278
1337
  } catch (err2) {
1338
+ const jsonErr = err2 instanceof Error ? err2.message : String(err2);
1279
1339
  if (verbose) {
1280
1340
  process.stderr.write(
1281
- `[muhaven-mcp] [bundler\u2717] ${method} id=${id} non-JSON err=${err2 instanceof Error ? err2.message : String(err2)}
1341
+ `[muhaven-mcp] [bundler\u2717] ${method} id=${id} non-JSON err=${jsonErr}
1282
1342
  `
1283
1343
  );
1284
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
+ });
1285
1353
  throw new BundlerClientError(
1286
1354
  "invalid_response",
1287
- `bundler ${method} returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
1355
+ `bundler ${method} returned non-JSON: ${jsonErr}`
1288
1356
  );
1289
1357
  }
1358
+ const respDump = JSON.stringify(parsed);
1359
+ const respBody = truncate(respDump);
1290
1360
  if (verbose) {
1291
- const respDump = JSON.stringify(parsed);
1292
- const trunc = respDump.length > 2048 ? respDump.slice(0, 2048) + "\u2026(truncated)" : respDump;
1293
- process.stderr.write(`[muhaven-mcp] [bundler\u2190] ${method} id=${id} resp=${trunc}
1361
+ process.stderr.write(`[muhaven-mcp] [bundler\u2190] ${method} id=${id} resp=${respBody}
1294
1362
  `);
1295
1363
  }
1364
+ const elapsedMs = Date.now() - startMs;
1296
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
+ });
1297
1375
  throw new BundlerClientError(
1298
1376
  "invalid_response",
1299
1377
  `bundler ${method} returned non-object`
@@ -1302,12 +1380,30 @@ var BundlerClient = class {
1302
1380
  const obj = parsed;
1303
1381
  if (obj.error !== void 0 && obj.error !== null) {
1304
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
+ });
1305
1393
  throw new BundlerClientError(
1306
1394
  "rpc_error",
1307
- `bundler ${method} rpc error: ${typeof err2.message === "string" ? err2.message : "<no message>"}`,
1395
+ `bundler ${method} rpc error: ${errMsg}`,
1308
1396
  { code: err2.code, message: err2.message, data: err2.data }
1309
1397
  );
1310
1398
  }
1399
+ this.pushTrace({
1400
+ method,
1401
+ id,
1402
+ requestBody,
1403
+ responseStatus: res.status,
1404
+ responseBody: respBody,
1405
+ elapsedMs
1406
+ });
1311
1407
  return obj.result;
1312
1408
  }
1313
1409
  };
@@ -1868,6 +1964,26 @@ function encodeKernelExecuteSingleCall(input) {
1868
1964
  args: [KERNEL_V3_SINGLE_CALL_MODE_DEFAULT, executionCalldata]
1869
1965
  });
1870
1966
  }
1967
+ function decodeKernelExecuteSingleCall(data) {
1968
+ let decoded;
1969
+ try {
1970
+ decoded = decodeFunctionData({ abi: KERNEL_EXECUTE_ABI, data });
1971
+ } catch {
1972
+ return null;
1973
+ }
1974
+ const [mode, executionCalldata] = decoded.args;
1975
+ if (mode !== KERNEL_V3_SINGLE_CALL_MODE_DEFAULT) {
1976
+ return null;
1977
+ }
1978
+ const ec = executionCalldata.slice(2);
1979
+ if (ec.length < 20 * 2 + 32 * 2) {
1980
+ return null;
1981
+ }
1982
+ const target = `0x${ec.slice(0, 40)}`;
1983
+ const value = BigInt(`0x${ec.slice(40, 40 + 64)}`);
1984
+ const innerCallData = `0x${ec.slice(40 + 64)}`;
1985
+ return { mode, target, value, innerCallData };
1986
+ }
1871
1987
  var ECDSA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
1872
1988
  var PERMISSION_USE_PREFIX = "0xff";
1873
1989
  function buildKernelSessionKeySignature(input) {
@@ -2257,11 +2373,85 @@ async function syncSnapshotFromMirror(deps, brokerSignerAddress) {
2257
2373
  }
2258
2374
  return { kind: "ok", sessionId: activeId };
2259
2375
  }
2376
+ var PURCHASE_SELECTOR_LOWER = SUBSCRIPTION_PURCHASE_SELECTOR.toLowerCase();
2377
+ function buildPathDDecodedCall(trace, deps) {
2378
+ const sponsorEvent = trace.find((e) => e.method === "zd_sponsorUserOperation");
2379
+ if (!sponsorEvent) return void 0;
2380
+ let req;
2381
+ try {
2382
+ req = JSON.parse(sponsorEvent.requestBody);
2383
+ } catch {
2384
+ return void 0;
2385
+ }
2386
+ const userOp = req.params?.[0]?.userOp;
2387
+ if (!userOp || typeof userOp.callData !== "string" || !userOp.callData.startsWith("0x")) {
2388
+ return void 0;
2389
+ }
2390
+ const sender = userOp.sender ?? "<missing>";
2391
+ const decoded = decodeKernelExecuteSingleCall(userOp.callData);
2392
+ if (!decoded) {
2393
+ return {
2394
+ sender,
2395
+ kernelExecuteMode: "<undecodable>",
2396
+ kernelExecuteTarget: "<undecodable>",
2397
+ kernelExecuteValue: "<undecodable>",
2398
+ innerSelector: "<undecodable>",
2399
+ interpretation: "kernel.execute callData could not be decoded as single-call default mode. The mode word is non-zero or the executionCalldata layout is unexpected. Manual decode required."
2400
+ };
2401
+ }
2402
+ const innerSelector = decoded.innerCallData.slice(0, 10).toLowerCase();
2403
+ let innerPurchaseTokenArg;
2404
+ let innerPurchaseMaxSharesHint;
2405
+ let innerPurchaseEphemeralEOA;
2406
+ if (innerSelector === PURCHASE_SELECTOR_LOWER) {
2407
+ try {
2408
+ const inner = decodeFunctionData({
2409
+ abi: SUBSCRIPTION_PURCHASE_ABI,
2410
+ data: decoded.innerCallData
2411
+ });
2412
+ const [tokenArg, , maxSharesHint, ephemeralEOA] = inner.args;
2413
+ innerPurchaseTokenArg = tokenArg;
2414
+ innerPurchaseMaxSharesHint = maxSharesHint.toString();
2415
+ innerPurchaseEphemeralEOA = ephemeralEOA;
2416
+ } catch {
2417
+ }
2418
+ }
2419
+ const expectedSubscriptionAddress = deps.subscriptionAddress?.toLowerCase();
2420
+ const kernelTargetLower = decoded.target.toLowerCase();
2421
+ let kernelExecuteTargetMatchesSubscription;
2422
+ let interpretation;
2423
+ if (expectedSubscriptionAddress === void 0) {
2424
+ interpretation = `kernel.execute target=${decoded.target}; inner purchase token=${innerPurchaseTokenArg ?? "<unknown>"}; MUHAVEN_SUBSCRIPTION_ADDRESS not wired on this MCP server \u2014 cannot cross-check.`;
2425
+ } else if (kernelTargetLower === expectedSubscriptionAddress) {
2426
+ kernelExecuteTargetMatchesSubscription = true;
2427
+ interpretation = `kernel.execute target matches MuHavenSubscription (${decoded.target}). Inner purchase token = ${innerPurchaseTokenArg ?? "<unknown>"}. The shape is correct; the AA23 revert is downstream of the validator's signature decode \u2014 likely either an on-chain signer-vs-installed-permission mismatch, a target/selector not in the on-chain policy, or a cap-arg breach. Check muhaven.policy.session_key_status for the installed permission state.`;
2428
+ } else if (innerPurchaseTokenArg !== void 0 && kernelTargetLower === innerPurchaseTokenArg.toLowerCase()) {
2429
+ kernelExecuteTargetMatchesSubscription = false;
2430
+ interpretation = `kernel.execute target = ${decoded.target} = the RWA MuHavenToken (purchase.token arg0). Expected MuHavenSubscription (${deps.subscriptionAddress}). The kernel is dispatching purchase() to the token contract instead of the subscription \u2014 token doesn't have a purchase() selector, so fallback returns empty revert data (= AA23 reverted 0x). This is a code-side bug in the kernel.execute target wiring.`;
2431
+ } else {
2432
+ kernelExecuteTargetMatchesSubscription = false;
2433
+ interpretation = `kernel.execute target = ${decoded.target} \u2014 NEITHER the expected MuHavenSubscription (${deps.subscriptionAddress}) NOR the inner purchase.token arg (${innerPurchaseTokenArg ?? "<none>"}). This is an unexpected third-address dispatch; inspect deps.subscriptionAddress env wiring.`;
2434
+ }
2435
+ return {
2436
+ sender,
2437
+ kernelExecuteMode: decoded.mode,
2438
+ kernelExecuteTarget: decoded.target,
2439
+ kernelExecuteValue: decoded.value.toString(),
2440
+ innerSelector,
2441
+ innerPurchaseTokenArg,
2442
+ innerPurchaseMaxSharesHint,
2443
+ innerPurchaseEphemeralEOA,
2444
+ expectedSubscriptionAddress: deps.subscriptionAddress,
2445
+ kernelExecuteTargetMatchesSubscription,
2446
+ interpretation
2447
+ };
2448
+ }
2260
2449
  async function attemptPathD(args, deps) {
2261
2450
  const { shares, tokenAddress, tokenSymbol } = args;
2262
2451
  if (!deps.broker || !deps.bundler) {
2263
2452
  return { kind: "unconfigured" };
2264
2453
  }
2454
+ deps.bundler.drainTrace();
2265
2455
  if (!deps.subscriptionAddress) {
2266
2456
  return {
2267
2457
  kind: "fallback",
@@ -2702,6 +2892,8 @@ async function positionBuy(input, deps) {
2702
2892
  let pathDFallbackReason;
2703
2893
  let pathDFallbackDetail;
2704
2894
  let pathDSubmittedUserOpHash;
2895
+ let pathDBundlerTrace;
2896
+ let pathDDecodedCall;
2705
2897
  const pathD = await attemptPathD(
2706
2898
  { shares, tokenAddress: token.address, tokenSymbol: token.symbol },
2707
2899
  deps
@@ -2715,6 +2907,13 @@ async function positionBuy(input, deps) {
2715
2907
  if (pathD.submittedUserOpHash) {
2716
2908
  pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
2717
2909
  }
2910
+ if (deps.bundler) {
2911
+ const trace = deps.bundler.drainTrace();
2912
+ if (trace.length > 0) {
2913
+ pathDBundlerTrace = trace;
2914
+ pathDDecodedCall = buildPathDDecodedCall(trace, deps);
2915
+ }
2916
+ }
2718
2917
  }
2719
2918
  const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
2720
2919
  token: token.symbol,
@@ -2738,7 +2937,9 @@ ${dashboardUrl}`,
2738
2937
  navUsd6: navUsd6.toString(),
2739
2938
  ...pathDFallbackReason ? { pathDFallbackReason } : {},
2740
2939
  ...pathDFallbackDetail ? { pathDFallbackDetail } : {},
2741
- ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
2940
+ ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
2941
+ ...pathDBundlerTrace ? { pathDBundlerTrace } : {},
2942
+ ...pathDDecodedCall ? { pathDDecodedCall } : {}
2742
2943
  }
2743
2944
  });
2744
2945
  }
@@ -3089,7 +3290,7 @@ var SERVER_NAME = "@muhaven/mcp";
3089
3290
  var SERVER_VERSION = resolveServerVersion();
3090
3291
  function resolveServerVersion() {
3091
3292
  {
3092
- return "0.2.7";
3293
+ return "0.2.9";
3093
3294
  }
3094
3295
  }
3095
3296
  function toJsonInputSchema(schema) {
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.7",
6
+ "version": "0.2.9",
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.7",
3
+ "version": "0.2.9",
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": {