@muhaven/mcp 0.2.7 → 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 +52 -0
- package/dist/broker.cjs +1 -1
- package/dist/broker.js +1 -1
- package/dist/index.cjs +118 -13
- package/dist/index.d.cts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +118 -13
- package/manifest.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,58 @@ 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
|
+
|
|
10
62
|
## [0.2.7] — 2026-05-23
|
|
11
63
|
|
|
12
64
|
### Added
|
package/dist/broker.cjs
CHANGED
package/dist/broker.js
CHANGED
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=${
|
|
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=${
|
|
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: ${
|
|
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=${
|
|
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: ${
|
|
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
|
-
|
|
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: ${
|
|
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
|
};
|
|
@@ -2266,6 +2362,7 @@ async function attemptPathD(args, deps) {
|
|
|
2266
2362
|
if (!deps.broker || !deps.bundler) {
|
|
2267
2363
|
return { kind: "unconfigured" };
|
|
2268
2364
|
}
|
|
2365
|
+
deps.bundler.drainTrace();
|
|
2269
2366
|
if (!deps.subscriptionAddress) {
|
|
2270
2367
|
return {
|
|
2271
2368
|
kind: "fallback",
|
|
@@ -2706,6 +2803,7 @@ async function positionBuy(input, deps) {
|
|
|
2706
2803
|
let pathDFallbackReason;
|
|
2707
2804
|
let pathDFallbackDetail;
|
|
2708
2805
|
let pathDSubmittedUserOpHash;
|
|
2806
|
+
let pathDBundlerTrace;
|
|
2709
2807
|
const pathD = await attemptPathD(
|
|
2710
2808
|
{ shares, tokenAddress: token.address, tokenSymbol: token.symbol },
|
|
2711
2809
|
deps
|
|
@@ -2719,6 +2817,12 @@ async function positionBuy(input, deps) {
|
|
|
2719
2817
|
if (pathD.submittedUserOpHash) {
|
|
2720
2818
|
pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
|
|
2721
2819
|
}
|
|
2820
|
+
if (deps.bundler) {
|
|
2821
|
+
const trace = deps.bundler.drainTrace();
|
|
2822
|
+
if (trace.length > 0) {
|
|
2823
|
+
pathDBundlerTrace = trace;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2722
2826
|
}
|
|
2723
2827
|
const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
|
|
2724
2828
|
token: token.symbol,
|
|
@@ -2742,7 +2846,8 @@ ${dashboardUrl}`,
|
|
|
2742
2846
|
navUsd6: navUsd6.toString(),
|
|
2743
2847
|
...pathDFallbackReason ? { pathDFallbackReason } : {},
|
|
2744
2848
|
...pathDFallbackDetail ? { pathDFallbackDetail } : {},
|
|
2745
|
-
...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
|
|
2849
|
+
...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
|
|
2850
|
+
...pathDBundlerTrace ? { pathDBundlerTrace } : {}
|
|
2746
2851
|
}
|
|
2747
2852
|
});
|
|
2748
2853
|
}
|
|
@@ -3093,7 +3198,7 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
3093
3198
|
var SERVER_VERSION = resolveServerVersion();
|
|
3094
3199
|
function resolveServerVersion() {
|
|
3095
3200
|
{
|
|
3096
|
-
return "0.2.
|
|
3201
|
+
return "0.2.8";
|
|
3097
3202
|
}
|
|
3098
3203
|
}
|
|
3099
3204
|
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
|
@@ -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=${
|
|
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=${
|
|
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: ${
|
|
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=${
|
|
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: ${
|
|
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
|
-
|
|
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: ${
|
|
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
|
};
|
|
@@ -2262,6 +2358,7 @@ async function attemptPathD(args, deps) {
|
|
|
2262
2358
|
if (!deps.broker || !deps.bundler) {
|
|
2263
2359
|
return { kind: "unconfigured" };
|
|
2264
2360
|
}
|
|
2361
|
+
deps.bundler.drainTrace();
|
|
2265
2362
|
if (!deps.subscriptionAddress) {
|
|
2266
2363
|
return {
|
|
2267
2364
|
kind: "fallback",
|
|
@@ -2702,6 +2799,7 @@ async function positionBuy(input, deps) {
|
|
|
2702
2799
|
let pathDFallbackReason;
|
|
2703
2800
|
let pathDFallbackDetail;
|
|
2704
2801
|
let pathDSubmittedUserOpHash;
|
|
2802
|
+
let pathDBundlerTrace;
|
|
2705
2803
|
const pathD = await attemptPathD(
|
|
2706
2804
|
{ shares, tokenAddress: token.address, tokenSymbol: token.symbol },
|
|
2707
2805
|
deps
|
|
@@ -2715,6 +2813,12 @@ async function positionBuy(input, deps) {
|
|
|
2715
2813
|
if (pathD.submittedUserOpHash) {
|
|
2716
2814
|
pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
|
|
2717
2815
|
}
|
|
2816
|
+
if (deps.bundler) {
|
|
2817
|
+
const trace = deps.bundler.drainTrace();
|
|
2818
|
+
if (trace.length > 0) {
|
|
2819
|
+
pathDBundlerTrace = trace;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2718
2822
|
}
|
|
2719
2823
|
const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
|
|
2720
2824
|
token: token.symbol,
|
|
@@ -2738,7 +2842,8 @@ ${dashboardUrl}`,
|
|
|
2738
2842
|
navUsd6: navUsd6.toString(),
|
|
2739
2843
|
...pathDFallbackReason ? { pathDFallbackReason } : {},
|
|
2740
2844
|
...pathDFallbackDetail ? { pathDFallbackDetail } : {},
|
|
2741
|
-
...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
|
|
2845
|
+
...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
|
|
2846
|
+
...pathDBundlerTrace ? { pathDBundlerTrace } : {}
|
|
2742
2847
|
}
|
|
2743
2848
|
});
|
|
2744
2849
|
}
|
|
@@ -3089,7 +3194,7 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
3089
3194
|
var SERVER_VERSION = resolveServerVersion();
|
|
3090
3195
|
function resolveServerVersion() {
|
|
3091
3196
|
{
|
|
3092
|
-
return "0.2.
|
|
3197
|
+
return "0.2.8";
|
|
3093
3198
|
}
|
|
3094
3199
|
}
|
|
3095
3200
|
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.
|
|
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.
|
|
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": {
|