@muhaven/mcp 0.1.6 → 0.1.7

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,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] — 2026-05-18
11
+
12
+ `position.*` tools can now drive real on-chain action via @muhaven/mcp
13
+ — Path C of MCP Option A (dashboard URL elicitation → existing passkey
14
+ ceremony). Pre-0.1.7, position tools returned a placeholder UserOp
15
+ envelope + broker signature that no host could submit; the path was
16
+ attestation-only despite implying buy/sell/claim. 0.1.7 swaps the
17
+ envelope for a pre-filled dashboard deep-link URL the user opens to
18
+ review + tap their passkey through the existing dashboard flow.
19
+
20
+ ### Added
21
+
22
+ - **`muhaven.cash.wrap`** — new tool. Returns a `/cash?amount=` deep-
23
+ link for USDC → mhUSDC conversion. Common LLM chain: `read.portfolio`
24
+ → notice 0 mhUSDC → `cash.wrap` → then `position.buy` (each is its
25
+ own user-confirmed deep-link). Input is human-readable USDC ("100" =
26
+ $100). 23 tools total now (was 22).
27
+
28
+ - **Token identifier accepts symbols OR addresses.** Every `position.*`
29
+ tool's `token` field used to require a 0x-address. Now accepts either
30
+ a symbol ("TBILL1") or a 0x-address. The dashboard pages resolve the
31
+ symbol via the marketplace store; unknown identifiers leave the form
32
+ blank for the user to fill in. Saves the LLM a round-trip through
33
+ `read.tokens` for the common "buy 5 of TBILL1" flow.
34
+
35
+ - **Exported pure helpers** (`buildPositionDeeplink`,
36
+ `formatUsdc6ToDecimal`) so third-party MCP servers + tests can reason
37
+ about the URL shape without spawning anything.
38
+
39
+ ### Changed
40
+
41
+ - **`position.buy/sell/claim` return shape** is now `{ dashboardUrl,
42
+ action, instructions, echo }` instead of `{ intentHash,
43
+ unsignedUserOp, brokerSignature, signerAddress }`. The `instructions`
44
+ field is a pre-formatted two-line string the LLM can show the user
45
+ verbatim ("Open this link to review and authorize..."). The `echo`
46
+ field mirrors input for LLM self-verification. **Breaking** for any
47
+ consumer that pinned the 0.1.6 response shape — the prior shape was
48
+ itself never end-to-end usable (placeholder envelope), so the
49
+ practical impact is "MCP buy now actually works" rather than
50
+ regression.
51
+
52
+ - **`position.rebalance`** returns `not_implemented` with a clear
53
+ next-step hint pointing at single-leg `position.buy` / `position.sell`
54
+ or the dashboard. Multi-leg `execute_plan` (one URL, one passkey,
55
+ one batched UserOp) lands in Wave 5 with composite preview UI.
56
+
57
+ - **Broker dep no longer required for position tools.** Previously, a
58
+ `position.buy` call without a running `muhaven-broker` daemon
59
+ returned `broker.unavailable`. Now position tools talk only to the
60
+ dashboard URL — the broker is still needed for `read.*` / governance
61
+ / issuer / policy tools (those use the JWT-authed path).
62
+
63
+ ### Removed
64
+
65
+ - `signEnvelope` + `PositionEnvelopeData` + the per-process
66
+ `hasSessionKey` probe cache + `__resetSessionKeyProbeCacheForTests`'s
67
+ cache (the function is retained as a no-op for back-compat with any
68
+ test harness importing it). The whole broker-attestation path for
69
+ position tools is gone — they don't need a signing key at all.
70
+
71
+ ### Internal
72
+
73
+ - 21 new vitest cases in `__tests__/position-deeplink.test.ts`,
74
+ replacing `session-key-required.test.ts` (deleted — covered a
75
+ removed code path). Total `@muhaven/mcp` suite: **262/262 passing**
76
+ (was 241). Tool-hash count: **23** (was 22).
77
+
78
+ ### Operator notes
79
+
80
+ - Once installed, the fresh-install ritual is unchanged: `muhaven-broker
81
+ setup --register claude-code` still wires the MCP server into Claude
82
+ Code via `claude mcp add-json`.
83
+ - Existing 0.1.6 installs: `npm install -g @muhaven/mcp@latest` picks up
84
+ the new bin; no setup re-run needed.
85
+ - `MUHAVEN_DASHBOARD_URL` env var (defaults to `https://muhaven.app`)
86
+ now drives the deep-link URL prefix; staging operators set it to
87
+ `https://muhaven-staging.example` and the URLs flow through.
88
+
10
89
  ## [0.1.6] — 2026-05-17
11
90
 
12
91
  Adds `muhaven-broker setup --register HOST` so a fresh install no longer
package/dist/broker.cjs CHANGED
@@ -1750,7 +1750,7 @@ function printUsage() {
1750
1750
  }
1751
1751
  function getBrokerPackageVersion() {
1752
1752
  {
1753
- return "0.1.6";
1753
+ return "0.1.7";
1754
1754
  }
1755
1755
  }
1756
1756
  function printVersion() {
package/dist/broker.js CHANGED
@@ -1752,7 +1752,7 @@ function printUsage() {
1752
1752
  }
1753
1753
  function getBrokerPackageVersion() {
1754
1754
  {
1755
- return "0.1.6";
1755
+ return "0.1.7";
1756
1756
  }
1757
1757
  }
1758
1758
  function printVersion() {
package/dist/index.cjs CHANGED
@@ -11,7 +11,7 @@ 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');
14
+ require('viem');
15
15
  var accounts = require('viem/accounts');
16
16
 
17
17
  // ../../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
@@ -467,25 +467,32 @@ var TOOL_DESCRIPTORS = [
467
467
  {
468
468
  name: "muhaven.position.buy",
469
469
  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.",
470
+ 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"). Token accepts either a symbol ("TBILL1") or 0x-address. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.portfolio after the user confirms done.`,
471
471
  sensitive: true
472
472
  },
473
473
  {
474
474
  name: "muhaven.position.sell",
475
475
  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.",
476
+ 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 share count), NOT mhUSDC notional.",
477
477
  sensitive: true
478
478
  },
479
479
  {
480
480
  name: "muhaven.position.claim",
481
481
  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.",
482
+ 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
483
  sensitive: true
484
484
  },
485
485
  {
486
486
  name: "muhaven.position.rebalance",
487
487
  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.",
488
+ 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.",
489
+ sensitive: true
490
+ },
491
+ // ── Path C cash group (2026-05-18) ────────────────────────────────
492
+ {
493
+ name: "muhaven.cash.wrap",
494
+ group: "cash",
495
+ 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
496
  sensitive: true
490
497
  },
491
498
  {
@@ -611,6 +618,13 @@ function verifyDescriptorAgainstPin(descriptor, pinnedSha256) {
611
618
  }
612
619
  var HEX_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
613
620
  var addressSchema = zod.z.string().regex(HEX_ADDRESS_RE, "must be a 0x-prefixed 20-byte hex address");
621
+ var TOKEN_SYMBOL_RE = /^[A-Za-z][A-Za-z0-9]{0,11}$/;
622
+ var tokenIdentifierSchema = zod.z.string().refine(
623
+ (v) => HEX_ADDRESS_RE.test(v) || TOKEN_SYMBOL_RE.test(v),
624
+ {
625
+ message: "must be a 0x-prefixed 20-byte hex address OR a token symbol (1-12 alphanumeric chars, starting with a letter)"
626
+ }
627
+ );
614
628
  var tierSchema = zod.z.enum(["advisory", "confirm-per-action", "policy-bound", "paused"]);
615
629
  var surfaceSchema = zod.z.enum(["havenbot", "mcp", "openclaw", "checkout"]);
616
630
  zod.z.union([zod.z.literal(1), zod.z.literal(2), zod.z.literal(3), zod.z.literal(4)]);
@@ -647,29 +661,38 @@ var ReadAuditInputSchema = zod.z.object({
647
661
  limit: zod.z.number().int().min(1).max(200).optional()
648
662
  }).strict();
649
663
  var PositionBuyInputSchema = zod.z.object({
650
- token: addressSchema,
664
+ /** Symbol (e.g. "TBILL1") or 0x-address. Path C dashboard resolves either. */
665
+ token: tokenIdentifierSchema,
651
666
  /** Investor candidate spend, denominated in USDC base units (uint64). */
652
667
  amountUsdc6: zod.z.string().regex(/^\d+$/, "must be a base-10 integer string")
653
668
  }).strict();
654
669
  var PositionSellInputSchema = zod.z.object({
655
- token: addressSchema,
670
+ token: tokenIdentifierSchema,
656
671
  /** Encrypted-balance share count to redeem, denominated in fhERC-20 base units. */
657
672
  amountShares: zod.z.string().regex(/^\d+$/, "must be a base-10 integer string")
658
673
  }).strict();
659
674
  var PositionClaimInputSchema = zod.z.object({
660
- token: addressSchema,
661
- /** When set, claim only the named escrow id; else claim-all. */
675
+ token: tokenIdentifierSchema,
676
+ /** When set, deep-link highlights the specific epoch row; else /yields renders the full claimable list. */
662
677
  escrowId: zod.z.string().regex(/^\d+$/).optional()
663
678
  }).strict();
664
679
  var PositionRebalanceInputSchema = zod.z.object({
665
680
  legs: zod.z.array(
666
681
  zod.z.object({
667
- token: addressSchema,
682
+ token: tokenIdentifierSchema,
668
683
  side: zod.z.enum(["buy", "sell"]),
669
684
  amount: zod.z.string().regex(/^\d+$/)
670
685
  }).strict()
671
686
  ).min(2).max(8)
672
687
  }).strict();
688
+ var CashWrapInputSchema = zod.z.object({
689
+ /**
690
+ * USDC amount in human-readable units ("100" for $100, "1.5" for
691
+ * $1.50). Forwarded verbatim to `/cash?amount=`. Decimal optional
692
+ * — CashPage parses both `\d+` and `\d+\.\d+`.
693
+ */
694
+ amountUsdc: zod.z.string().regex(/^\d+(\.\d+)?$/, "must be a positive decimal number string")
695
+ }).strict();
673
696
  var PolicySetTierInputSchema = zod.z.object({
674
697
  targetTier: tierSchema,
675
698
  /** Returned by an earlier `request` call. Omit for step-down or first-call. */
@@ -740,15 +763,6 @@ function authRequiredPayload() {
740
763
  loginCommand: "muhaven-broker login"
741
764
  };
742
765
  }
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
766
 
753
767
  // src/tools/handlers.ts
754
768
  function ok(data) {
@@ -765,11 +779,6 @@ function mapBackendError(e) {
765
779
  if (e instanceof Error) return err("backend.network", e.message);
766
780
  return err("backend.network", "unknown backend error");
767
781
  }
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
782
  async function readPortfolio(_input, deps) {
774
783
  try {
775
784
  const data = await deps.backend.get("/api/v1/portfolio");
@@ -823,103 +832,95 @@ async function readAudit(input, deps) {
823
832
  return mapBackendError(e);
824
833
  }
825
834
  }
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));
835
+ function buildPositionDeeplink(dashboardBaseUrl, action, params) {
836
+ const base = dashboardBaseUrl.replace(/\/+$/, "");
837
+ const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
838
+ const search = new URLSearchParams();
839
+ if (action === "buy" || action === "sell") search.set("mode", action);
840
+ for (const [k, v] of Object.entries(params)) search.set(k, v);
841
+ search.set("from", "mcp");
842
+ return `${base}${path}?${search.toString()}`;
830
843
  }
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;
844
+ function formatUsdc6ToDecimal(amountUsdc6) {
845
+ if (!/^\d+$/.test(amountUsdc6)) {
846
+ throw new Error(`amountUsdc6 must be a non-negative integer string: ${amountUsdc6}`);
838
847
  }
839
- return value;
848
+ const padded = amountUsdc6.padStart(7, "0");
849
+ const intPart = padded.slice(0, -6).replace(/^0+(?=\d)/, "");
850
+ const fracPart = padded.slice(-6).replace(/0+$/, "");
851
+ return fracPart === "" ? intPart : `${intPart}.${fracPart}`;
840
852
  }
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
- }
853
+ function resolveDashboardBaseUrl(deps) {
854
+ return deps.dashboardBaseUrl ?? "https://muhaven.app";
890
855
  }
891
856
  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
- );
857
+ const amount = formatUsdc6ToDecimal(input.amountUsdc6);
858
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
859
+ token: input.token,
860
+ amount
861
+ });
862
+ return ok({
863
+ dashboardUrl,
864
+ action: "buy",
865
+ instructions: `Open this link to review and authorize the buy of ${amount} mhUSDC of ${input.token}:
866
+ ${dashboardUrl}`,
867
+ echo: { action: "buy", token: input.token, amount }
868
+ });
898
869
  }
899
870
  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
- );
871
+ if (!/^\d+(\.\d+)?$/.test(input.amountShares)) {
872
+ return err(
873
+ "invalid_input",
874
+ `amountShares must be a non-negative number string: ${JSON.stringify(input.amountShares)}`
875
+ );
876
+ }
877
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "sell", {
878
+ token: input.token,
879
+ shares: input.amountShares
880
+ });
881
+ return ok({
882
+ dashboardUrl,
883
+ action: "sell",
884
+ instructions: `Open this link to review and authorize the sale of ${input.amountShares} shares of ${input.token}:
885
+ ${dashboardUrl}`,
886
+ echo: { action: "sell", token: input.token, shares: input.amountShares }
887
+ });
906
888
  }
907
889
  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
- );
890
+ const params = { token: input.token };
891
+ if (input.escrowId) params.epoch = input.escrowId;
892
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "claim", params);
893
+ const claimDescriptor = input.escrowId ? `the claim for epoch #${input.escrowId} of ${input.token}` : `your claimable epochs for ${input.token}`;
894
+ return ok({
895
+ dashboardUrl,
896
+ action: "claim",
897
+ instructions: `Open this link to review and authorize ${claimDescriptor}:
898
+ ${dashboardUrl}`,
899
+ echo: {
900
+ action: "claim",
901
+ token: input.token,
902
+ ...input.escrowId ? { epoch: input.escrowId } : {}
903
+ }
904
+ });
914
905
  }
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
906
+ async function positionRebalance(_input, _deps) {
907
+ return err(
908
+ "not_implemented",
909
+ "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
910
  );
922
911
  }
912
+ async function cashWrap(input, deps) {
913
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "wrap", {
914
+ amount: input.amountUsdc
915
+ });
916
+ return ok({
917
+ dashboardUrl,
918
+ action: "wrap",
919
+ instructions: `Open this link to review and authorize the conversion of ${input.amountUsdc} USDC into mhUSDC:
920
+ ${dashboardUrl}`,
921
+ echo: { action: "wrap", amount: input.amountUsdc }
922
+ });
923
+ }
923
924
  async function policySetTier(input, deps) {
924
925
  try {
925
926
  const data = await deps.backend.post("/api/v1/agent/policy/transition", {
@@ -1127,6 +1128,11 @@ var HANDLERS = {
1127
1128
  schema: PositionRebalanceInputSchema,
1128
1129
  handler: positionRebalance
1129
1130
  },
1131
+ // ── Path C cash group (2026-05-18) ────────────────────────────────
1132
+ "muhaven.cash.wrap": {
1133
+ schema: CashWrapInputSchema,
1134
+ handler: cashWrap
1135
+ },
1130
1136
  "muhaven.policy.set_tier": {
1131
1137
  schema: PolicySetTierInputSchema,
1132
1138
  handler: policySetTier
@@ -1210,7 +1216,7 @@ var SERVER_NAME = "@muhaven/mcp";
1210
1216
  var SERVER_VERSION = resolveServerVersion();
1211
1217
  function resolveServerVersion() {
1212
1218
  {
1213
- return "0.1.6";
1219
+ return "0.1.7";
1214
1220
  }
1215
1221
  }
1216
1222
  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;
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { z, ZodError } from 'zod';
9
9
  import { platform, homedir } from 'os';
10
10
  import { connect, createServer } from 'net';
11
11
  import { createHash } from 'crypto';
12
- import { keccak256, toBytes } from 'viem';
12
+ import 'viem';
13
13
  import { privateKeyToAccount } from 'viem/accounts';
14
14
 
15
15
  // src/server.ts
@@ -463,25 +463,32 @@ var TOOL_DESCRIPTORS = [
463
463
  {
464
464
  name: "muhaven.position.buy",
465
465
  group: "position",
466
- 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.",
466
+ 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"). Token accepts either a symbol ("TBILL1") or 0x-address. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.portfolio after the user confirms done.`,
467
467
  sensitive: true
468
468
  },
469
469
  {
470
470
  name: "muhaven.position.sell",
471
471
  group: "position",
472
- 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.",
472
+ 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 share count), NOT mhUSDC notional.",
473
473
  sensitive: true
474
474
  },
475
475
  {
476
476
  name: "muhaven.position.claim",
477
477
  group: "position",
478
- 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.",
478
+ 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.",
479
479
  sensitive: true
480
480
  },
481
481
  {
482
482
  name: "muhaven.position.rebalance",
483
483
  group: "position",
484
- 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.",
484
+ 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.",
485
+ sensitive: true
486
+ },
487
+ // ── Path C cash group (2026-05-18) ────────────────────────────────
488
+ {
489
+ name: "muhaven.cash.wrap",
490
+ group: "cash",
491
+ 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.',
485
492
  sensitive: true
486
493
  },
487
494
  {
@@ -607,6 +614,13 @@ function verifyDescriptorAgainstPin(descriptor, pinnedSha256) {
607
614
  }
608
615
  var HEX_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
609
616
  var addressSchema = z.string().regex(HEX_ADDRESS_RE, "must be a 0x-prefixed 20-byte hex address");
617
+ var TOKEN_SYMBOL_RE = /^[A-Za-z][A-Za-z0-9]{0,11}$/;
618
+ var tokenIdentifierSchema = z.string().refine(
619
+ (v) => HEX_ADDRESS_RE.test(v) || TOKEN_SYMBOL_RE.test(v),
620
+ {
621
+ message: "must be a 0x-prefixed 20-byte hex address OR a token symbol (1-12 alphanumeric chars, starting with a letter)"
622
+ }
623
+ );
610
624
  var tierSchema = z.enum(["advisory", "confirm-per-action", "policy-bound", "paused"]);
611
625
  var surfaceSchema = z.enum(["havenbot", "mcp", "openclaw", "checkout"]);
612
626
  z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]);
@@ -643,29 +657,38 @@ var ReadAuditInputSchema = z.object({
643
657
  limit: z.number().int().min(1).max(200).optional()
644
658
  }).strict();
645
659
  var PositionBuyInputSchema = z.object({
646
- token: addressSchema,
660
+ /** Symbol (e.g. "TBILL1") or 0x-address. Path C dashboard resolves either. */
661
+ token: tokenIdentifierSchema,
647
662
  /** Investor candidate spend, denominated in USDC base units (uint64). */
648
663
  amountUsdc6: z.string().regex(/^\d+$/, "must be a base-10 integer string")
649
664
  }).strict();
650
665
  var PositionSellInputSchema = z.object({
651
- token: addressSchema,
666
+ token: tokenIdentifierSchema,
652
667
  /** Encrypted-balance share count to redeem, denominated in fhERC-20 base units. */
653
668
  amountShares: z.string().regex(/^\d+$/, "must be a base-10 integer string")
654
669
  }).strict();
655
670
  var PositionClaimInputSchema = z.object({
656
- token: addressSchema,
657
- /** When set, claim only the named escrow id; else claim-all. */
671
+ token: tokenIdentifierSchema,
672
+ /** When set, deep-link highlights the specific epoch row; else /yields renders the full claimable list. */
658
673
  escrowId: z.string().regex(/^\d+$/).optional()
659
674
  }).strict();
660
675
  var PositionRebalanceInputSchema = z.object({
661
676
  legs: z.array(
662
677
  z.object({
663
- token: addressSchema,
678
+ token: tokenIdentifierSchema,
664
679
  side: z.enum(["buy", "sell"]),
665
680
  amount: z.string().regex(/^\d+$/)
666
681
  }).strict()
667
682
  ).min(2).max(8)
668
683
  }).strict();
684
+ var CashWrapInputSchema = z.object({
685
+ /**
686
+ * USDC amount in human-readable units ("100" for $100, "1.5" for
687
+ * $1.50). Forwarded verbatim to `/cash?amount=`. Decimal optional
688
+ * — CashPage parses both `\d+` and `\d+\.\d+`.
689
+ */
690
+ amountUsdc: z.string().regex(/^\d+(\.\d+)?$/, "must be a positive decimal number string")
691
+ }).strict();
669
692
  var PolicySetTierInputSchema = z.object({
670
693
  targetTier: tierSchema,
671
694
  /** Returned by an earlier `request` call. Omit for step-down or first-call. */
@@ -736,15 +759,6 @@ function authRequiredPayload() {
736
759
  loginCommand: "muhaven-broker login"
737
760
  };
738
761
  }
739
- function sessionKeyRequiredPayload(dashboardBaseUrl = "https://muhaven.app") {
740
- const mintUrl = `${trimTrailingSlash(dashboardBaseUrl)}/agent/policy/transition`;
741
- return {
742
- ok: false,
743
- code: "SESSION_KEY_REQUIRED",
744
- 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.`,
745
- mintUrl
746
- };
747
- }
748
762
 
749
763
  // src/tools/handlers.ts
750
764
  function ok(data) {
@@ -761,11 +775,6 @@ function mapBackendError(e) {
761
775
  if (e instanceof Error) return err("backend.network", e.message);
762
776
  return err("backend.network", "unknown backend error");
763
777
  }
764
- function mapBrokerError(e) {
765
- if (e instanceof BrokerClientError) return err(`broker.${e.code}`, e.message);
766
- if (e instanceof Error) return err("broker.network", e.message);
767
- return err("broker.network", "unknown broker error");
768
- }
769
778
  async function readPortfolio(_input, deps) {
770
779
  try {
771
780
  const data = await deps.backend.get("/api/v1/portfolio");
@@ -819,103 +828,95 @@ async function readAudit(input, deps) {
819
828
  return mapBackendError(e);
820
829
  }
821
830
  }
822
- var PLACEHOLDER_INTENT_DOMAIN = "muhaven.placeholder.intent.v0:";
823
- function computeIntentHash(intent) {
824
- const canonical = JSON.stringify(sortKeys(intent));
825
- return keccak256(toBytes(PLACEHOLDER_INTENT_DOMAIN + canonical));
831
+ function buildPositionDeeplink(dashboardBaseUrl, action, params) {
832
+ const base = dashboardBaseUrl.replace(/\/+$/, "");
833
+ const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
834
+ const search = new URLSearchParams();
835
+ if (action === "buy" || action === "sell") search.set("mode", action);
836
+ for (const [k, v] of Object.entries(params)) search.set(k, v);
837
+ search.set("from", "mcp");
838
+ return `${base}${path}?${search.toString()}`;
826
839
  }
827
- function sortKeys(value) {
828
- if (Array.isArray(value)) return value.map(sortKeys);
829
- if (value && typeof value === "object") {
830
- const obj = value;
831
- const sorted = {};
832
- for (const k of Object.keys(obj).sort()) sorted[k] = sortKeys(obj[k]);
833
- return sorted;
840
+ function formatUsdc6ToDecimal(amountUsdc6) {
841
+ if (!/^\d+$/.test(amountUsdc6)) {
842
+ throw new Error(`amountUsdc6 must be a non-negative integer string: ${amountUsdc6}`);
834
843
  }
835
- return value;
844
+ const padded = amountUsdc6.padStart(7, "0");
845
+ const intPart = padded.slice(0, -6).replace(/^0+(?=\d)/, "");
846
+ const fracPart = padded.slice(-6).replace(/0+$/, "");
847
+ return fracPart === "" ? intPart : `${intPart}.${fracPart}`;
836
848
  }
837
- var cachedHasSessionKeyProbe = null;
838
- async function signEnvelope(intent, toolName, summary, deps) {
839
- const intentHash = computeIntentHash(intent);
840
- if (!deps.broker) {
841
- return err(
842
- "broker.unavailable",
843
- "position tools require a running muhaven-broker daemon \u2014 see README \xA7Broker setup"
844
- );
845
- }
846
- const broker = deps.broker;
847
- if (cachedHasSessionKeyProbe === null) {
848
- cachedHasSessionKeyProbe = (async () => {
849
- try {
850
- const hello = await broker.hello();
851
- return hello.hasSessionKey ?? true;
852
- } catch (err2) {
853
- cachedHasSessionKeyProbe = null;
854
- throw err2;
855
- }
856
- })();
857
- }
858
- let hasSessionKey;
859
- try {
860
- hasSessionKey = await cachedHasSessionKeyProbe;
861
- } catch (e) {
862
- return mapBrokerError(e);
863
- }
864
- if (hasSessionKey === false) {
865
- return sessionKeyRequiredPayload(deps.dashboardBaseUrl);
866
- }
867
- try {
868
- const sig = await broker.signHash(intentHash, { tool: toolName, summary });
869
- return ok({
870
- intentHash,
871
- unsignedUserOp: {
872
- target: "see backend",
873
- data: "see backend",
874
- note: "P3 returns a placeholder envelope; P6 wires the canonical UserOp shape."
875
- },
876
- brokerSignature: sig.signature,
877
- signerAddress: sig.signerAddress
878
- });
879
- } catch (e) {
880
- if (e instanceof BrokerClientError && e.code === "broker_error" && /session_key_unavailable/.test(e.message)) {
881
- cachedHasSessionKeyProbe = Promise.resolve(false);
882
- return sessionKeyRequiredPayload(deps.dashboardBaseUrl);
883
- }
884
- return mapBrokerError(e);
885
- }
849
+ function resolveDashboardBaseUrl(deps) {
850
+ return deps.dashboardBaseUrl ?? "https://muhaven.app";
886
851
  }
887
852
  async function positionBuy(input, deps) {
888
- return signEnvelope(
889
- { kind: "buy", token: input.token, amountUsdc6: input.amountUsdc6 },
890
- "muhaven.position.buy",
891
- `buy ${input.amountUsdc6} USDC of ${input.token}`,
892
- deps
893
- );
853
+ const amount = formatUsdc6ToDecimal(input.amountUsdc6);
854
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
855
+ token: input.token,
856
+ amount
857
+ });
858
+ return ok({
859
+ dashboardUrl,
860
+ action: "buy",
861
+ instructions: `Open this link to review and authorize the buy of ${amount} mhUSDC of ${input.token}:
862
+ ${dashboardUrl}`,
863
+ echo: { action: "buy", token: input.token, amount }
864
+ });
894
865
  }
895
866
  async function positionSell(input, deps) {
896
- return signEnvelope(
897
- { kind: "sell", token: input.token, amountShares: input.amountShares },
898
- "muhaven.position.sell",
899
- `sell ${input.amountShares} shares of ${input.token}`,
900
- deps
901
- );
867
+ if (!/^\d+(\.\d+)?$/.test(input.amountShares)) {
868
+ return err(
869
+ "invalid_input",
870
+ `amountShares must be a non-negative number string: ${JSON.stringify(input.amountShares)}`
871
+ );
872
+ }
873
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "sell", {
874
+ token: input.token,
875
+ shares: input.amountShares
876
+ });
877
+ return ok({
878
+ dashboardUrl,
879
+ action: "sell",
880
+ instructions: `Open this link to review and authorize the sale of ${input.amountShares} shares of ${input.token}:
881
+ ${dashboardUrl}`,
882
+ echo: { action: "sell", token: input.token, shares: input.amountShares }
883
+ });
902
884
  }
903
885
  async function positionClaim(input, deps) {
904
- return signEnvelope(
905
- { kind: "claim", token: input.token, escrowId: input.escrowId ?? null },
906
- "muhaven.position.claim",
907
- `claim ${input.token}${input.escrowId ? ` escrow#${input.escrowId}` : " (all)"}`,
908
- deps
909
- );
886
+ const params = { token: input.token };
887
+ if (input.escrowId) params.epoch = input.escrowId;
888
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "claim", params);
889
+ const claimDescriptor = input.escrowId ? `the claim for epoch #${input.escrowId} of ${input.token}` : `your claimable epochs for ${input.token}`;
890
+ return ok({
891
+ dashboardUrl,
892
+ action: "claim",
893
+ instructions: `Open this link to review and authorize ${claimDescriptor}:
894
+ ${dashboardUrl}`,
895
+ echo: {
896
+ action: "claim",
897
+ token: input.token,
898
+ ...input.escrowId ? { epoch: input.escrowId } : {}
899
+ }
900
+ });
910
901
  }
911
- async function positionRebalance(input, deps) {
912
- return signEnvelope(
913
- { kind: "rebalance", legs: input.legs },
914
- "muhaven.position.rebalance",
915
- `rebalance ${input.legs.length} legs`,
916
- deps
902
+ async function positionRebalance(_input, _deps) {
903
+ return err(
904
+ "not_implemented",
905
+ "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."
917
906
  );
918
907
  }
908
+ async function cashWrap(input, deps) {
909
+ const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "wrap", {
910
+ amount: input.amountUsdc
911
+ });
912
+ return ok({
913
+ dashboardUrl,
914
+ action: "wrap",
915
+ instructions: `Open this link to review and authorize the conversion of ${input.amountUsdc} USDC into mhUSDC:
916
+ ${dashboardUrl}`,
917
+ echo: { action: "wrap", amount: input.amountUsdc }
918
+ });
919
+ }
919
920
  async function policySetTier(input, deps) {
920
921
  try {
921
922
  const data = await deps.backend.post("/api/v1/agent/policy/transition", {
@@ -1123,6 +1124,11 @@ var HANDLERS = {
1123
1124
  schema: PositionRebalanceInputSchema,
1124
1125
  handler: positionRebalance
1125
1126
  },
1127
+ // ── Path C cash group (2026-05-18) ────────────────────────────────
1128
+ "muhaven.cash.wrap": {
1129
+ schema: CashWrapInputSchema,
1130
+ handler: cashWrap
1131
+ },
1126
1132
  "muhaven.policy.set_tier": {
1127
1133
  schema: PolicySetTierInputSchema,
1128
1134
  handler: policySetTier
@@ -1206,7 +1212,7 @@ var SERVER_NAME = "@muhaven/mcp";
1206
1212
  var SERVER_VERSION = resolveServerVersion();
1207
1213
  function resolveServerVersion() {
1208
1214
  {
1209
- return "0.1.6";
1215
+ return "0.1.7";
1210
1216
  }
1211
1217
  }
1212
1218
  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.1.6",
6
+ "version": "0.1.7",
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 22 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 return unsigned UserOps + broker signatures — 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.1.6",
3
+ "version": "0.1.7",
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": {
package/tool-hashes.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-05-07T07:40:21.258Z",
2
+ "generatedAt": "2026-05-17T18:17:06.859Z",
3
3
  "tools": [
4
4
  {
5
5
  "name": "muhaven.read.portfolio",
@@ -23,19 +23,23 @@
23
23
  },
24
24
  {
25
25
  "name": "muhaven.position.buy",
26
- "sha256": "638c905c66d8eea6dcfebb412ebf7020d768fb3e023b3b7ec5783718592e99d0"
26
+ "sha256": "f24c9628249e91ed485031f9114053fa6b174a5d55aedfb4109392eb5795c9e4"
27
27
  },
28
28
  {
29
29
  "name": "muhaven.position.sell",
30
- "sha256": "20eb118ca994c2a1d4fd688c4832ecdfebb3f350e94dc6c4756a683d181f51c4"
30
+ "sha256": "707545106fdd505d7d942153bcb34d7ead53c2539b9699e0733159a57c72177d"
31
31
  },
32
32
  {
33
33
  "name": "muhaven.position.claim",
34
- "sha256": "daa1c23ce6cf1a113f73ff1326e807e4a18c07c06db0f05c15e5ac01c8c15e86"
34
+ "sha256": "31a6b9472f8b34e69ca28b888e60178dafd01b8644e2a61e6f36515660b68838"
35
35
  },
36
36
  {
37
37
  "name": "muhaven.position.rebalance",
38
- "sha256": "750bfe5524ac611354d692b8f48d9d3ed41858a89340d2553e2260b34986a0d7"
38
+ "sha256": "ad32f2e9c2b6aafb6bd3acd33079dc7541035f0f405045178f95489f80cc922e"
39
+ },
40
+ {
41
+ "name": "muhaven.cash.wrap",
42
+ "sha256": "1ae4cb949e0d09e7fb7e8957e51bfc63b43397c253849c217b835aaa9505b8c9"
39
43
  },
40
44
  {
41
45
  "name": "muhaven.policy.set_tier",