@muhaven/mcp 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -11,7 +11,6 @@ var zod = require('zod');
11
11
  var os = require('os');
12
12
  var net = require('net');
13
13
  var crypto = require('crypto');
14
- var viem = require('viem');
15
14
  var accounts = require('viem/accounts');
16
15
 
17
16
  // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
@@ -61,9 +60,38 @@ function deriveAllowedHosts(baseUrl) {
61
60
  function trimTrailingSlash(s) {
62
61
  return s.endsWith("/") ? s.slice(0, -1) : s;
63
62
  }
63
+ function validatePublicUrlEnv(name, value) {
64
+ let parsed;
65
+ try {
66
+ parsed = new URL(value);
67
+ } catch {
68
+ return `${name} is not a valid URL: ${value}`;
69
+ }
70
+ if (parsed.protocol === "https:") return null;
71
+ if (parsed.protocol === "http:") {
72
+ const host = parsed.hostname;
73
+ if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") return null;
74
+ return `${name} must use https:// (got http:// to ${host} \u2014 refusing to route MCP deep-links over cleartext to a non-loopback host)`;
75
+ }
76
+ return `${name} must use https:// (got ${parsed.protocol})`;
77
+ }
78
+ function resolvePublicUrlEnv(name, rawValue, defaultValue) {
79
+ const value = rawValue ?? defaultValue;
80
+ const err2 = validatePublicUrlEnv(name, value);
81
+ if (err2) throw new Error(err2);
82
+ return trimTrailingSlash(value);
83
+ }
64
84
  function loadMcpConfig(env = process.env) {
65
- const backendBaseUrl = trimTrailingSlash(env.MUHAVEN_BACKEND_URL ?? DEFAULT_BACKEND_URL);
66
- const dashboardBaseUrl = trimTrailingSlash(env.MUHAVEN_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL);
85
+ const backendBaseUrl = resolvePublicUrlEnv(
86
+ "MUHAVEN_BACKEND_URL",
87
+ env.MUHAVEN_BACKEND_URL,
88
+ DEFAULT_BACKEND_URL
89
+ );
90
+ const dashboardBaseUrl = resolvePublicUrlEnv(
91
+ "MUHAVEN_DASHBOARD_URL",
92
+ env.MUHAVEN_DASHBOARD_URL,
93
+ DEFAULT_DASHBOARD_URL
94
+ );
67
95
  const brokerEndpoint = env.MUHAVEN_BROKER_ENDPOINT ?? defaultBrokerEndpoint();
68
96
  const readOnly = readEnvBool("MUHAVEN_READ_ONLY", false, env);
69
97
  const requestTimeoutMs = readEnvInt("MUHAVEN_REQUEST_TIMEOUT_MS", DEFAULT_REQUEST_TIMEOUT_MS, env);
@@ -93,8 +121,16 @@ function loadBrokerConfig(env = process.env) {
93
121
  const endpoint = env.MUHAVEN_BROKER_ENDPOINT ?? defaultBrokerEndpoint();
94
122
  const maxRequestBytes = readEnvInt("MUHAVEN_BROKER_MAX_BYTES", DEFAULT_BROKER_MAX_BYTES, env);
95
123
  const requestTimeoutMs = readEnvInt("MUHAVEN_BROKER_TIMEOUT_MS", DEFAULT_BROKER_TIMEOUT_MS, env);
96
- const backendBaseUrl = trimTrailingSlash(env.MUHAVEN_BACKEND_URL ?? DEFAULT_BACKEND_URL);
97
- const dashboardBaseUrl = trimTrailingSlash(env.MUHAVEN_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL);
124
+ const backendBaseUrl = resolvePublicUrlEnv(
125
+ "MUHAVEN_BACKEND_URL",
126
+ env.MUHAVEN_BACKEND_URL,
127
+ DEFAULT_BACKEND_URL
128
+ );
129
+ const dashboardBaseUrl = resolvePublicUrlEnv(
130
+ "MUHAVEN_DASHBOARD_URL",
131
+ env.MUHAVEN_DASHBOARD_URL,
132
+ DEFAULT_DASHBOARD_URL
133
+ );
98
134
  return {
99
135
  endpoint,
100
136
  sessionKeyHex,
@@ -467,25 +503,32 @@ var TOOL_DESCRIPTORS = [
467
503
  {
468
504
  name: "muhaven.position.buy",
469
505
  group: "position",
470
- description: "PROPOSE a Subscription buy. Returns an unsigned UserOp envelope plus a broker-signed session-key signature. The host MUST present the unsigned envelope to the user for passkey confirmation before submission to the bundler \u2014 this tool NEVER auto-submits. Fails when the user is in Advisory or Paused tier.",
506
+ description: 'Prepare a Subscription buy. Returns a dashboard deep-link URL (muhaven.app/trade?mode=buy&...) the user opens to review the pre-filled form, then taps Authorize. The user\'s passkey + ZeroDev kernel sign on the dashboard \u2014 this MCP tool never holds or submits a signing key. Use after the user names a clear amount + token (e.g. "Buy 5 mhUSDC of TBILL1" \u2192 `amountUsdc: "5"`). Token accepts either a symbol ("TBILL1") or 0x-address. The `amountUsdc` field is HUMAN-DECIMAL mhUSDC ("5" = 5 mhUSDC, "0.5" = half a mhUSDC) \u2014 NOT base-6 integer. Max 6 fractional digits. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.portfolio after the user confirms done.',
471
507
  sensitive: true
472
508
  },
473
509
  {
474
510
  name: "muhaven.position.sell",
475
511
  group: "position",
476
- description: "PROPOSE a redemption-queue sell. Same envelope-plus-signature pattern as muhaven.position.buy. Requires the user to be in Confirm-per-action or Policy-bound tier on the MCP surface.",
512
+ description: "Prepare a Subscription sell. Returns a dashboard deep-link URL (muhaven.app/trade?mode=sell&...) with the form pre-filled. Same passkey + verify-after pattern as muhaven.position.buy. Input is amountShares (raw POSITIVE INTEGER share count, NOT mhUSDC notional) \u2014 fhERC-20 shares have no decimals so fractional inputs are rejected.",
477
513
  sensitive: true
478
514
  },
479
515
  {
480
516
  name: "muhaven.position.claim",
481
517
  group: "position",
482
- description: "PROPOSE a yield claim from RedemptionQueue / YieldSnapshot for a given token. Returns an unsigned UserOp + broker signature. Idempotent \u2014 proposing twice produces the same intent hash.",
518
+ description: "Prepare a yield claim. Returns a dashboard deep-link URL (muhaven.app/yields?...) pointing at the YieldsPage. When escrowId is set, the matching epoch row is highlighted + scrolled into view; when omitted, the page renders the user's full claimable list and they pick. User passkey-signs on the dashboard.",
483
519
  sensitive: true
484
520
  },
485
521
  {
486
522
  name: "muhaven.position.rebalance",
487
523
  group: "position",
488
- description: "PROPOSE a multi-leg atomic rebalance bundling buy + sell legs into a single UserOp. Each leg is constrained by the user's installed @zerodev/permissions CallPolicy.",
524
+ description: "NOT IMPLEMENTED in this release \u2014 returns an error pointing the user at single-leg position.buy / position.sell or the dashboard /trade page. Multi-leg execute_plan lands in Wave 5 with a composite preview UI + executeBatch on the kernel.",
525
+ sensitive: true
526
+ },
527
+ // ── Path C cash group (2026-05-18) ────────────────────────────────
528
+ {
529
+ name: "muhaven.cash.wrap",
530
+ group: "cash",
531
+ description: 'Prepare a USDC \u2192 mhUSDC wrap (the encrypted-balance conversion that funds buys). Returns a dashboard deep-link URL (muhaven.app/cash?action=wrap&...) with the amount pre-filled. Input amountUsdc is human-readable USDC ("100" = $100). Common LLM chain: read.portfolio \u2192 notice 0 mhUSDC \u2192 cash.wrap \u2192 then position.buy (each is its own user-confirmed deep-link). Settlement not observable from MCP \u2014 re-call read.portfolio to verify.',
489
532
  sensitive: true
490
533
  },
491
534
  {
@@ -611,6 +654,13 @@ function verifyDescriptorAgainstPin(descriptor, pinnedSha256) {
611
654
  }
612
655
  var HEX_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
613
656
  var addressSchema = zod.z.string().regex(HEX_ADDRESS_RE, "must be a 0x-prefixed 20-byte hex address");
657
+ var TOKEN_SYMBOL_RE = /^[A-Za-z][A-Za-z0-9]{0,11}$/;
658
+ var tokenIdentifierSchema = zod.z.string().refine(
659
+ (v) => HEX_ADDRESS_RE.test(v) || TOKEN_SYMBOL_RE.test(v),
660
+ {
661
+ message: "must be a 0x-prefixed 20-byte hex address OR a token symbol (1-12 alphanumeric chars, starting with a letter)"
662
+ }
663
+ );
614
664
  var tierSchema = zod.z.enum(["advisory", "confirm-per-action", "policy-bound", "paused"]);
615
665
  var surfaceSchema = zod.z.enum(["havenbot", "mcp", "openclaw", "checkout"]);
616
666
  zod.z.union([zod.z.literal(1), zod.z.literal(2), zod.z.literal(3), zod.z.literal(4)]);
@@ -646,30 +696,55 @@ var ReadAuditInputSchema = zod.z.object({
646
696
  cursor: zod.z.string().min(1).max(512).optional(),
647
697
  limit: zod.z.number().int().min(1).max(200).optional()
648
698
  }).strict();
699
+ var decimalUsdcAmountSchema = zod.z.string().regex(
700
+ /^(0|[1-9]\d*)(\.\d{1,6})?$/,
701
+ 'must be a positive decimal mhUSDC amount with at most 6 fractional digits (e.g. "5", "0.5", "1234.567")'
702
+ ).max(48, "must be at most 48 characters");
649
703
  var PositionBuyInputSchema = zod.z.object({
650
- token: addressSchema,
651
- /** Investor candidate spend, denominated in USDC base units (uint64). */
652
- amountUsdc6: zod.z.string().regex(/^\d+$/, "must be a base-10 integer string")
704
+ /** Symbol (e.g. "TBILL1") or 0x-address. Path C dashboard resolves either. */
705
+ token: tokenIdentifierSchema,
706
+ /**
707
+ * mhUSDC amount in human-decimal units ("5" = 5 mhUSDC, "0.5" =
708
+ * half a mhUSDC). Forwarded verbatim to the dashboard form via
709
+ * `/trade?amount=`. Replaces the prior `amountUsdc6` base-6 integer
710
+ * field — see schema doc above for rationale (LLM-footgun fix).
711
+ */
712
+ amountUsdc: decimalUsdcAmountSchema
653
713
  }).strict();
654
714
  var PositionSellInputSchema = zod.z.object({
655
- token: addressSchema,
656
- /** Encrypted-balance share count to redeem, denominated in fhERC-20 base units. */
657
- amountShares: zod.z.string().regex(/^\d+$/, "must be a base-10 integer string")
715
+ token: tokenIdentifierSchema,
716
+ /**
717
+ * Share count to redeem. fhERC-20 shares are integer base units
718
+ * (no decimals — see memory `project_decimals_lie_wave4_p0`).
719
+ * Regex rejects any fractional input so a deep-link can't pre-fill
720
+ * "2.5 shares" that would silently floor on the on-chain submit.
721
+ */
722
+ amountShares: zod.z.string().regex(/^[1-9]\d*$/, "must be a positive integer share count")
658
723
  }).strict();
659
724
  var PositionClaimInputSchema = zod.z.object({
660
- token: addressSchema,
661
- /** When set, claim only the named escrow id; else claim-all. */
725
+ token: tokenIdentifierSchema,
726
+ /** When set, deep-link highlights the specific epoch row; else /yields renders the full claimable list. */
662
727
  escrowId: zod.z.string().regex(/^\d+$/).optional()
663
728
  }).strict();
664
729
  var PositionRebalanceInputSchema = zod.z.object({
665
730
  legs: zod.z.array(
666
731
  zod.z.object({
667
- token: addressSchema,
732
+ token: tokenIdentifierSchema,
668
733
  side: zod.z.enum(["buy", "sell"]),
669
734
  amount: zod.z.string().regex(/^\d+$/)
670
735
  }).strict()
671
736
  ).min(2).max(8)
672
737
  }).strict();
738
+ var CashWrapInputSchema = zod.z.object({
739
+ /**
740
+ * USDC amount in human-decimal units ("100" for $100, "1.5" for
741
+ * $1.50). Same shape + same regex as `PositionBuyInputSchema.amountUsdc`
742
+ * so the LLM doesn't have to learn two different unit conventions
743
+ * across the Path C surface. Max 6 fractional digits (USDC's base
744
+ * unit floor); 48-char length cap is URL-bloat defense.
745
+ */
746
+ amountUsdc: decimalUsdcAmountSchema
747
+ }).strict();
673
748
  var PolicySetTierInputSchema = zod.z.object({
674
749
  targetTier: tierSchema,
675
750
  /** Returned by an earlier `request` call. Omit for step-down or first-call. */
@@ -740,15 +815,6 @@ function authRequiredPayload() {
740
815
  loginCommand: "muhaven-broker login"
741
816
  };
742
817
  }
743
- function sessionKeyRequiredPayload(dashboardBaseUrl = "https://muhaven.app") {
744
- const mintUrl = `${trimTrailingSlash(dashboardBaseUrl)}/agent/policy/transition`;
745
- return {
746
- ok: false,
747
- code: "SESSION_KEY_REQUIRED",
748
- message: `No session key loaded in broker (read-only posture). Mint one via the dashboard at ${mintUrl}, copy the 0x-prefixed hex into MUHAVEN_BROKER_SESSION_KEY, and restart the daemon. Do NOT run \`muhaven-broker login\` for this \u2014 that mints a JWT, not a session key.`,
749
- mintUrl
750
- };
751
- }
752
818
 
753
819
  // src/tools/handlers.ts
754
820
  function ok(data) {
@@ -765,11 +831,6 @@ function mapBackendError(e) {
765
831
  if (e instanceof Error) return err("backend.network", e.message);
766
832
  return err("backend.network", "unknown backend error");
767
833
  }
768
- function mapBrokerError(e) {
769
- if (e instanceof BrokerClientError) return err(`broker.${e.code}`, e.message);
770
- if (e instanceof Error) return err("broker.network", e.message);
771
- return err("broker.network", "unknown broker error");
772
- }
773
834
  async function readPortfolio(_input, deps) {
774
835
  try {
775
836
  const data = await deps.backend.get("/api/v1/portfolio");
@@ -823,103 +884,79 @@ async function readAudit(input, deps) {
823
884
  return mapBackendError(e);
824
885
  }
825
886
  }
826
- var PLACEHOLDER_INTENT_DOMAIN = "muhaven.placeholder.intent.v0:";
827
- function computeIntentHash(intent) {
828
- const canonical = JSON.stringify(sortKeys(intent));
829
- return viem.keccak256(viem.toBytes(PLACEHOLDER_INTENT_DOMAIN + canonical));
887
+ function buildPositionDeeplink(dashboardBaseUrl, action, params) {
888
+ const base = dashboardBaseUrl.replace(/\/+$/, "");
889
+ const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
890
+ const search = new URLSearchParams();
891
+ if (action === "buy" || action === "sell") search.set("mode", action);
892
+ for (const [k, v] of Object.entries(params)) search.set(k, v);
893
+ search.set("from", "mcp");
894
+ return `${base}${path}?${search.toString()}`;
830
895
  }
831
- function sortKeys(value) {
832
- if (Array.isArray(value)) return value.map(sortKeys);
833
- if (value && typeof value === "object") {
834
- const obj = value;
835
- const sorted = {};
836
- for (const k of Object.keys(obj).sort()) sorted[k] = sortKeys(obj[k]);
837
- return sorted;
838
- }
839
- return value;
840
- }
841
- var cachedHasSessionKeyProbe = null;
842
- async function signEnvelope(intent, toolName, summary, deps) {
843
- const intentHash = computeIntentHash(intent);
844
- if (!deps.broker) {
845
- return err(
846
- "broker.unavailable",
847
- "position tools require a running muhaven-broker daemon \u2014 see README \xA7Broker setup"
848
- );
849
- }
850
- const broker = deps.broker;
851
- if (cachedHasSessionKeyProbe === null) {
852
- cachedHasSessionKeyProbe = (async () => {
853
- try {
854
- const hello = await broker.hello();
855
- return hello.hasSessionKey ?? true;
856
- } catch (err2) {
857
- cachedHasSessionKeyProbe = null;
858
- throw err2;
859
- }
860
- })();
861
- }
862
- let hasSessionKey;
863
- try {
864
- hasSessionKey = await cachedHasSessionKeyProbe;
865
- } catch (e) {
866
- return mapBrokerError(e);
867
- }
868
- if (hasSessionKey === false) {
869
- return sessionKeyRequiredPayload(deps.dashboardBaseUrl);
870
- }
871
- try {
872
- const sig = await broker.signHash(intentHash, { tool: toolName, summary });
873
- return ok({
874
- intentHash,
875
- unsignedUserOp: {
876
- target: "see backend",
877
- data: "see backend",
878
- note: "P3 returns a placeholder envelope; P6 wires the canonical UserOp shape."
879
- },
880
- brokerSignature: sig.signature,
881
- signerAddress: sig.signerAddress
882
- });
883
- } catch (e) {
884
- if (e instanceof BrokerClientError && e.code === "broker_error" && /session_key_unavailable/.test(e.message)) {
885
- cachedHasSessionKeyProbe = Promise.resolve(false);
886
- return sessionKeyRequiredPayload(deps.dashboardBaseUrl);
887
- }
888
- return mapBrokerError(e);
889
- }
896
+ function resolveDashboardBaseUrl(deps) {
897
+ return deps.dashboardBaseUrl ?? "https://muhaven.app";
890
898
  }
891
899
  async function positionBuy(input, deps) {
892
- return signEnvelope(
893
- { kind: "buy", token: input.token, amountUsdc6: input.amountUsdc6 },
894
- "muhaven.position.buy",
895
- `buy ${input.amountUsdc6} USDC of ${input.token}`,
896
- deps
897
- );
900
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
901
+ token: input.token,
902
+ amount: input.amountUsdc
903
+ });
904
+ return ok({
905
+ dashboardUrl,
906
+ action: "buy",
907
+ instructions: `Open this link to review and authorize the buy of ${input.amountUsdc} mhUSDC of ${input.token}:
908
+ ${dashboardUrl}`,
909
+ echo: { action: "buy", token: input.token, amount: input.amountUsdc }
910
+ });
898
911
  }
899
912
  async function positionSell(input, deps) {
900
- return signEnvelope(
901
- { kind: "sell", token: input.token, amountShares: input.amountShares },
902
- "muhaven.position.sell",
903
- `sell ${input.amountShares} shares of ${input.token}`,
904
- deps
905
- );
913
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "sell", {
914
+ token: input.token,
915
+ shares: input.amountShares
916
+ });
917
+ return ok({
918
+ dashboardUrl,
919
+ action: "sell",
920
+ instructions: `Open this link to review and authorize the sale of ${input.amountShares} shares of ${input.token}:
921
+ ${dashboardUrl}`,
922
+ echo: { action: "sell", token: input.token, shares: input.amountShares }
923
+ });
906
924
  }
907
925
  async function positionClaim(input, deps) {
908
- return signEnvelope(
909
- { kind: "claim", token: input.token, escrowId: input.escrowId ?? null },
910
- "muhaven.position.claim",
911
- `claim ${input.token}${input.escrowId ? ` escrow#${input.escrowId}` : " (all)"}`,
912
- deps
913
- );
926
+ const params = { token: input.token };
927
+ if (input.escrowId) params.epoch = input.escrowId;
928
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "claim", params);
929
+ const claimDescriptor = input.escrowId ? `the claim for epoch #${input.escrowId} of ${input.token}` : `your claimable epochs for ${input.token}`;
930
+ return ok({
931
+ dashboardUrl,
932
+ action: "claim",
933
+ instructions: `Open this link to review and authorize ${claimDescriptor}:
934
+ ${dashboardUrl}`,
935
+ echo: {
936
+ action: "claim",
937
+ token: input.token,
938
+ ...input.escrowId ? { epoch: input.escrowId } : {}
939
+ }
940
+ });
914
941
  }
915
- async function positionRebalance(input, deps) {
916
- return signEnvelope(
917
- { kind: "rebalance", legs: input.legs },
918
- "muhaven.position.rebalance",
919
- `rebalance ${input.legs.length} legs`,
920
- deps
942
+ async function positionRebalance(_input, _deps) {
943
+ return err(
944
+ "not_implemented",
945
+ "position.rebalance is deferred to Wave 5. Today, ask the user to execute legs one at a time via position.buy / position.sell, or use the dashboard /trade page directly."
921
946
  );
922
947
  }
948
+ async function cashWrap(input, deps) {
949
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "wrap", {
950
+ amount: input.amountUsdc
951
+ });
952
+ return ok({
953
+ dashboardUrl,
954
+ action: "wrap",
955
+ instructions: `Open this link to review and authorize the conversion of ${input.amountUsdc} USDC into mhUSDC:
956
+ ${dashboardUrl}`,
957
+ echo: { action: "wrap", amount: input.amountUsdc }
958
+ });
959
+ }
923
960
  async function policySetTier(input, deps) {
924
961
  try {
925
962
  const data = await deps.backend.post("/api/v1/agent/policy/transition", {
@@ -1127,6 +1164,11 @@ var HANDLERS = {
1127
1164
  schema: PositionRebalanceInputSchema,
1128
1165
  handler: positionRebalance
1129
1166
  },
1167
+ // ── Path C cash group (2026-05-18) ────────────────────────────────
1168
+ "muhaven.cash.wrap": {
1169
+ schema: CashWrapInputSchema,
1170
+ handler: cashWrap
1171
+ },
1130
1172
  "muhaven.policy.set_tier": {
1131
1173
  schema: PolicySetTierInputSchema,
1132
1174
  handler: policySetTier
@@ -1210,7 +1252,7 @@ var SERVER_NAME = "@muhaven/mcp";
1210
1252
  var SERVER_VERSION = resolveServerVersion();
1211
1253
  function resolveServerVersion() {
1212
1254
  {
1213
- return "0.1.6";
1255
+ return "0.2.0";
1214
1256
  }
1215
1257
  }
1216
1258
  function toJsonInputSchema(schema) {
package/dist/index.d.cts CHANGED
@@ -298,7 +298,7 @@ interface ToolDescriptor {
298
298
  * `read` so `--read-only` keeps them available; the two propose
299
299
  * tools (`muhaven.governance.propose`, `muhaven.governance.cast_vote`)
300
300
  * are filtered off in read-only mode. */
301
- readonly group: 'read' | 'position' | 'policy' | 'issuer' | 'governance';
301
+ readonly group: 'read' | 'position' | 'policy' | 'issuer' | 'governance' | 'cash';
302
302
  /** Human-readable description shown in the host UI. */
303
303
  readonly description: string;
304
304
  /** When true, the host SHOULD render a confirmation cue before invoking. */
@@ -384,26 +384,6 @@ interface SessionKeyRequiredPayload {
384
384
  readonly mintUrl: string;
385
385
  }
386
386
 
387
- /**
388
- * Tool handlers — pure functions of `(input, deps)` returning a structured
389
- * tool result. The MCP server transport layer (`src/server.ts`) wires
390
- * these to the host LLM via `@modelcontextprotocol/sdk`.
391
- *
392
- * Design notes:
393
- * - Handlers NEVER throw. They translate every error into a structured
394
- * `{ ok: false, code, message }` payload so the host LLM can decide
395
- * how to surface it without crashing the MCP server. This matches the
396
- * MCPB error-presentation convention.
397
- * - Position handlers DO NOT submit UserOps. They return an unsigned
398
- * envelope plus a broker signature; the host (or the MuHaven
399
- * dashboard via deep-link) is responsible for bundler submission.
400
- * Splitting submission from signing is the lethal-trifecta defense.
401
- * - Backend errors with status >= 500 are surfaced as `server_error`
402
- * so the host can retry; client errors (4xx) bubble up as the
403
- * discriminating code (`unauthorized`, `forbidden`, etc.). The host
404
- * MUST NOT auto-retry 4xx.
405
- */
406
-
407
387
  interface ToolDeps {
408
388
  backend: BackendClient;
409
389
  broker?: BrokerClient;
package/dist/index.d.ts CHANGED
@@ -298,7 +298,7 @@ interface ToolDescriptor {
298
298
  * `read` so `--read-only` keeps them available; the two propose
299
299
  * tools (`muhaven.governance.propose`, `muhaven.governance.cast_vote`)
300
300
  * are filtered off in read-only mode. */
301
- readonly group: 'read' | 'position' | 'policy' | 'issuer' | 'governance';
301
+ readonly group: 'read' | 'position' | 'policy' | 'issuer' | 'governance' | 'cash';
302
302
  /** Human-readable description shown in the host UI. */
303
303
  readonly description: string;
304
304
  /** When true, the host SHOULD render a confirmation cue before invoking. */
@@ -384,26 +384,6 @@ interface SessionKeyRequiredPayload {
384
384
  readonly mintUrl: string;
385
385
  }
386
386
 
387
- /**
388
- * Tool handlers — pure functions of `(input, deps)` returning a structured
389
- * tool result. The MCP server transport layer (`src/server.ts`) wires
390
- * these to the host LLM via `@modelcontextprotocol/sdk`.
391
- *
392
- * Design notes:
393
- * - Handlers NEVER throw. They translate every error into a structured
394
- * `{ ok: false, code, message }` payload so the host LLM can decide
395
- * how to surface it without crashing the MCP server. This matches the
396
- * MCPB error-presentation convention.
397
- * - Position handlers DO NOT submit UserOps. They return an unsigned
398
- * envelope plus a broker signature; the host (or the MuHaven
399
- * dashboard via deep-link) is responsible for bundler submission.
400
- * Splitting submission from signing is the lethal-trifecta defense.
401
- * - Backend errors with status >= 500 are surfaced as `server_error`
402
- * so the host can retry; client errors (4xx) bubble up as the
403
- * discriminating code (`unauthorized`, `forbidden`, etc.). The host
404
- * MUST NOT auto-retry 4xx.
405
- */
406
-
407
387
  interface ToolDeps {
408
388
  backend: BackendClient;
409
389
  broker?: BrokerClient;