@muhaven/mcp 0.2.0 → 0.2.1
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/broker.cjs +1 -1
- package/dist/broker.js +1 -1
- package/dist/index.cjs +164 -8
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +164 -8
- package/manifest.json +4 -3
- package/package.json +1 -1
- package/tool-hashes.json +8 -4
package/dist/broker.cjs
CHANGED
package/dist/broker.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -377,6 +377,23 @@ var BackendClient = class {
|
|
|
377
377
|
false
|
|
378
378
|
);
|
|
379
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* GET variant that sends no Authorization header. Use for backend
|
|
382
|
+
* endpoints that are intentionally public (e.g. `/api/v1/tokens`
|
|
383
|
+
* which the marketplace + the 0.2.1 `positionBuy` NAV-conversion
|
|
384
|
+
* both read). Avoids triggering the AUTH_REQUIRED branch for the
|
|
385
|
+
* "not yet logged in" case on read paths that don't need auth.
|
|
386
|
+
*/
|
|
387
|
+
async getUnauth(path, query) {
|
|
388
|
+
const url = this.buildUrl(path, query);
|
|
389
|
+
return this.exchange(
|
|
390
|
+
"GET",
|
|
391
|
+
url,
|
|
392
|
+
void 0,
|
|
393
|
+
/* withAuth */
|
|
394
|
+
false
|
|
395
|
+
);
|
|
396
|
+
}
|
|
380
397
|
buildUrl(path, query) {
|
|
381
398
|
if (!path.startsWith("/")) {
|
|
382
399
|
throw new BackendError("bad_request", `path must start with "/": ${path}`);
|
|
@@ -500,16 +517,22 @@ var TOOL_DESCRIPTORS = [
|
|
|
500
517
|
description: `Return the authenticated user's tiered-autonomy audit log entries. Cursor-paginated. Useful for forensic review ("why was I paused?") and grant-reviewer demos. Read-only \u2014 never exposes other users' data.`,
|
|
501
518
|
sensitive: false
|
|
502
519
|
},
|
|
520
|
+
{
|
|
521
|
+
name: "muhaven.read.activity",
|
|
522
|
+
group: "read",
|
|
523
|
+
description: "Return the authenticated investor's on-chain activity feed (buys / sells / wraps / unwraps / yield claims / transfers). Each row carries token address, tx hash, block timestamp, and event type \u2014 but NEVER cleartext amounts (encrypted handles only, decryptable client-side via permit). USE THIS to verify a Path C dashboard action settled: after position.buy / position.sell / cash.wrap, the user opens the deep-link, taps Authorize, the on-chain tx lands \u2192 a new row appears here. Far more reliable than re-calling read.portfolio (which only changes shape when a NEW token enters the catalog).",
|
|
524
|
+
sensitive: false
|
|
525
|
+
},
|
|
503
526
|
{
|
|
504
527
|
name: "muhaven.position.buy",
|
|
505
528
|
group: "position",
|
|
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.
|
|
529
|
+
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. The tool fetches the current on-chain NAV for the token and converts the notional to integer shares (floor) before building the URL \u2014 so "Buy 3 mhUSDC of GOLD1" at NAV $0.01 becomes "Buy 300 GOLD1 shares (~3 mhUSDC)". Refuses with `amount_too_small_for_share` when the notional won\'t buy at least 1 share at current NAV; the error message tells the user the minimum mhUSDC needed. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.activity after the user confirms done (a new "buy" row with the tx hash will appear).',
|
|
507
530
|
sensitive: true
|
|
508
531
|
},
|
|
509
532
|
{
|
|
510
533
|
name: "muhaven.position.sell",
|
|
511
534
|
group: "position",
|
|
512
|
-
description:
|
|
535
|
+
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. Verify settlement by calling muhaven.read.activity (look for a "sell" or "sell-queued" row with the tx hash).',
|
|
513
536
|
sensitive: true
|
|
514
537
|
},
|
|
515
538
|
{
|
|
@@ -528,7 +551,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
528
551
|
{
|
|
529
552
|
name: "muhaven.cash.wrap",
|
|
530
553
|
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).
|
|
554
|
+
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). Verify settlement by calling muhaven.read.activity (a new "wrap" row will appear with the tx hash).',
|
|
532
555
|
sensitive: true
|
|
533
556
|
},
|
|
534
557
|
{
|
|
@@ -696,6 +719,10 @@ var ReadAuditInputSchema = zod.z.object({
|
|
|
696
719
|
cursor: zod.z.string().min(1).max(512).optional(),
|
|
697
720
|
limit: zod.z.number().int().min(1).max(200).optional()
|
|
698
721
|
}).strict();
|
|
722
|
+
var ReadActivityInputSchema = zod.z.object({
|
|
723
|
+
limit: zod.z.number().int().min(1).max(50).optional(),
|
|
724
|
+
offset: zod.z.number().int().min(0).max(1e3).optional()
|
|
725
|
+
}).strict();
|
|
699
726
|
var decimalUsdcAmountSchema = zod.z.string().regex(
|
|
700
727
|
/^(0|[1-9]\d*)(\.\d{1,6})?$/,
|
|
701
728
|
'must be a positive decimal mhUSDC amount with at most 6 fractional digits (e.g. "5", "0.5", "1234.567")'
|
|
@@ -816,6 +843,37 @@ function authRequiredPayload() {
|
|
|
816
843
|
};
|
|
817
844
|
}
|
|
818
845
|
|
|
846
|
+
// src/tools/decimal.ts
|
|
847
|
+
function parseDecimalToUsd6(decimal) {
|
|
848
|
+
const m = /^(\d+)(?:\.(\d+))?$/.exec(decimal);
|
|
849
|
+
if (!m) {
|
|
850
|
+
throw new Error(`Invalid decimal price: ${JSON.stringify(decimal)}`);
|
|
851
|
+
}
|
|
852
|
+
const intPart = m[1];
|
|
853
|
+
const fracPart = m[2] ?? "";
|
|
854
|
+
const fracPadded = (fracPart + "000000").slice(0, 6);
|
|
855
|
+
return BigInt(intPart + fracPadded);
|
|
856
|
+
}
|
|
857
|
+
function computeSharesFromUsd6(notionalUsd6, navUsd6) {
|
|
858
|
+
if (navUsd6 <= 0n) {
|
|
859
|
+
throw new Error("navUsd6 must be positive");
|
|
860
|
+
}
|
|
861
|
+
if (notionalUsd6 < 0n) {
|
|
862
|
+
throw new Error("notionalUsd6 must be non-negative");
|
|
863
|
+
}
|
|
864
|
+
return notionalUsd6 / navUsd6;
|
|
865
|
+
}
|
|
866
|
+
function formatUsd6AsDecimal(usd6) {
|
|
867
|
+
if (usd6 < 0n) {
|
|
868
|
+
throw new Error("usd6 must be non-negative");
|
|
869
|
+
}
|
|
870
|
+
const whole = usd6 / 1000000n;
|
|
871
|
+
const frac = usd6 % 1000000n;
|
|
872
|
+
if (frac === 0n) return whole.toString();
|
|
873
|
+
const fracStr = frac.toString().padStart(6, "0").replace(/0+$/, "");
|
|
874
|
+
return `${whole.toString()}.${fracStr}`;
|
|
875
|
+
}
|
|
876
|
+
|
|
819
877
|
// src/tools/handlers.ts
|
|
820
878
|
function ok(data) {
|
|
821
879
|
return { ok: true, data };
|
|
@@ -884,6 +942,17 @@ async function readAudit(input, deps) {
|
|
|
884
942
|
return mapBackendError(e);
|
|
885
943
|
}
|
|
886
944
|
}
|
|
945
|
+
async function readActivity(input, deps) {
|
|
946
|
+
try {
|
|
947
|
+
const data = await deps.backend.get("/api/v1/activity", {
|
|
948
|
+
limit: input.limit,
|
|
949
|
+
offset: input.offset
|
|
950
|
+
});
|
|
951
|
+
return ok(data);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
return mapBackendError(e);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
887
956
|
function buildPositionDeeplink(dashboardBaseUrl, action, params) {
|
|
888
957
|
const base = dashboardBaseUrl.replace(/\/+$/, "");
|
|
889
958
|
const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
|
|
@@ -896,17 +965,100 @@ function buildPositionDeeplink(dashboardBaseUrl, action, params) {
|
|
|
896
965
|
function resolveDashboardBaseUrl(deps) {
|
|
897
966
|
return deps.dashboardBaseUrl ?? "https://muhaven.app";
|
|
898
967
|
}
|
|
968
|
+
function resolveTokenInCatalog(identifier, catalog) {
|
|
969
|
+
const needle = identifier.toLowerCase();
|
|
970
|
+
return catalog.find(
|
|
971
|
+
(t) => t.address.toLowerCase() === needle || t.symbol.toLowerCase() === needle
|
|
972
|
+
) ?? null;
|
|
973
|
+
}
|
|
974
|
+
function sanitizeSymbolForLlmContext(raw) {
|
|
975
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "?");
|
|
976
|
+
return cleaned.length > 16 ? cleaned.slice(0, 16) : cleaned;
|
|
977
|
+
}
|
|
899
978
|
async function positionBuy(input, deps) {
|
|
979
|
+
let catalog;
|
|
980
|
+
try {
|
|
981
|
+
catalog = await deps.backend.getUnauth("/api/v1/tokens");
|
|
982
|
+
} catch (e) {
|
|
983
|
+
return mapBackendError(e);
|
|
984
|
+
}
|
|
985
|
+
const token = resolveTokenInCatalog(input.token, catalog.tokens ?? []);
|
|
986
|
+
if (!token) {
|
|
987
|
+
return err(
|
|
988
|
+
"token_not_found",
|
|
989
|
+
`Token "${sanitizeSymbolForLlmContext(input.token)}" is not in the MuHaven catalog. Call muhaven.read.tokens for the canonical symbol list.`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
const safeSymbol = sanitizeSymbolForLlmContext(token.symbol);
|
|
993
|
+
if (!token.latest_nav || !token.latest_nav.nav) {
|
|
994
|
+
return err(
|
|
995
|
+
"nav_unavailable",
|
|
996
|
+
`No NAV snapshot available for ${safeSymbol} yet. The nav-worker may not have written one \u2014 retry shortly, or use the dashboard /trade page directly.`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
let navUsd6;
|
|
1000
|
+
try {
|
|
1001
|
+
navUsd6 = parseDecimalToUsd6(token.latest_nav.nav);
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
return err(
|
|
1004
|
+
"nav_malformed",
|
|
1005
|
+
`NAV for ${safeSymbol} is not a valid decimal price. Open the dashboard /trade page directly.`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
if (navUsd6 <= 0n) {
|
|
1009
|
+
return err(
|
|
1010
|
+
"nav_non_positive",
|
|
1011
|
+
`NAV for ${safeSymbol} is non-positive. Cannot quote a buy.`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
let notionalUsd6;
|
|
1015
|
+
try {
|
|
1016
|
+
notionalUsd6 = parseDecimalToUsd6(input.amountUsdc);
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
return err(
|
|
1019
|
+
"invalid_amount",
|
|
1020
|
+
`amountUsdc "${input.amountUsdc}" is not a valid decimal mhUSDC amount.`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
if (notionalUsd6 <= 0n) {
|
|
1024
|
+
return err(
|
|
1025
|
+
"invalid_amount",
|
|
1026
|
+
"amountUsdc must be greater than zero."
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const shares = computeSharesFromUsd6(notionalUsd6, navUsd6);
|
|
1030
|
+
if (shares <= 0n) {
|
|
1031
|
+
const navDisplay2 = formatUsd6AsDecimal(navUsd6);
|
|
1032
|
+
return err(
|
|
1033
|
+
"amount_too_small_for_share",
|
|
1034
|
+
`${input.amountUsdc} mhUSDC isn't enough to buy 1 share of ${safeSymbol} at the current NAV of $${navDisplay2}/share. Need at least ${navDisplay2} mhUSDC to buy 1 share. Ask the user for a larger amount, or chain muhaven.cash.wrap first if they're short on mhUSDC.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
const effectiveNotionalUsd6 = shares * navUsd6;
|
|
1038
|
+
const effectiveNotionalDisplay = formatUsd6AsDecimal(effectiveNotionalUsd6);
|
|
1039
|
+
const navDisplay = formatUsd6AsDecimal(navUsd6);
|
|
1040
|
+
const sharesStr = shares.toString();
|
|
900
1041
|
const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
|
|
901
|
-
token:
|
|
902
|
-
amount:
|
|
1042
|
+
token: token.symbol,
|
|
1043
|
+
amount: sharesStr
|
|
903
1044
|
});
|
|
904
1045
|
return ok({
|
|
905
1046
|
dashboardUrl,
|
|
906
1047
|
action: "buy",
|
|
907
|
-
instructions: `Open this link to review and authorize the buy of ${
|
|
1048
|
+
instructions: `Open this link to review and authorize the buy of ${sharesStr} ${safeSymbol} shares (~${effectiveNotionalDisplay} mhUSDC at current NAV $${navDisplay}/share):
|
|
908
1049
|
${dashboardUrl}`,
|
|
909
|
-
echo: {
|
|
1050
|
+
echo: {
|
|
1051
|
+
action: "buy",
|
|
1052
|
+
token: token.symbol,
|
|
1053
|
+
amount: sharesStr,
|
|
1054
|
+
shares: sharesStr,
|
|
1055
|
+
// Carry the original request + the conversion math so the LLM
|
|
1056
|
+
// (and a human auditor reading the trace) can see why the URL
|
|
1057
|
+
// shows the share count instead of the user-stated notional.
|
|
1058
|
+
amountUsdc: input.amountUsdc,
|
|
1059
|
+
effectiveNotionalUsd6: effectiveNotionalUsd6.toString(),
|
|
1060
|
+
navUsd6: navUsd6.toString()
|
|
1061
|
+
}
|
|
910
1062
|
});
|
|
911
1063
|
}
|
|
912
1064
|
async function positionSell(input, deps) {
|
|
@@ -1148,6 +1300,10 @@ var HANDLERS = {
|
|
|
1148
1300
|
schema: ReadAuditInputSchema,
|
|
1149
1301
|
handler: readAudit
|
|
1150
1302
|
},
|
|
1303
|
+
"muhaven.read.activity": {
|
|
1304
|
+
schema: ReadActivityInputSchema,
|
|
1305
|
+
handler: readActivity
|
|
1306
|
+
},
|
|
1151
1307
|
"muhaven.position.buy": {
|
|
1152
1308
|
schema: PositionBuyInputSchema,
|
|
1153
1309
|
handler: positionBuy
|
|
@@ -1252,7 +1408,7 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
1252
1408
|
var SERVER_VERSION = resolveServerVersion();
|
|
1253
1409
|
function resolveServerVersion() {
|
|
1254
1410
|
{
|
|
1255
|
-
return "0.2.
|
|
1411
|
+
return "0.2.1";
|
|
1256
1412
|
}
|
|
1257
1413
|
}
|
|
1258
1414
|
function toJsonInputSchema(schema) {
|
package/dist/index.d.cts
CHANGED
|
@@ -263,6 +263,14 @@ declare class BackendClient {
|
|
|
263
263
|
* Authorization header.
|
|
264
264
|
*/
|
|
265
265
|
postUnauth<T>(path: string, body: unknown): Promise<T>;
|
|
266
|
+
/**
|
|
267
|
+
* GET variant that sends no Authorization header. Use for backend
|
|
268
|
+
* endpoints that are intentionally public (e.g. `/api/v1/tokens`
|
|
269
|
+
* which the marketplace + the 0.2.1 `positionBuy` NAV-conversion
|
|
270
|
+
* both read). Avoids triggering the AUTH_REQUIRED branch for the
|
|
271
|
+
* "not yet logged in" case on read paths that don't need auth.
|
|
272
|
+
*/
|
|
273
|
+
getUnauth<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
266
274
|
private buildUrl;
|
|
267
275
|
private exchangeWithRetry;
|
|
268
276
|
private exchange;
|
|
@@ -305,14 +313,15 @@ interface ToolDescriptor {
|
|
|
305
313
|
readonly sensitive: boolean;
|
|
306
314
|
}
|
|
307
315
|
/**
|
|
308
|
-
* The
|
|
309
|
-
* muhaven.read.* (
|
|
316
|
+
* The 23 MCP tools across five groups:
|
|
317
|
+
* muhaven.read.* (8 — incl. P11 protection_coverage + kyc_attestation
|
|
318
|
+
* + 0.2.1 read.activity for Path C settle verify)
|
|
310
319
|
* muhaven.position.* (4)
|
|
311
320
|
* muhaven.policy.* (4)
|
|
312
321
|
* muhaven.issuer.* (5 — P7)
|
|
313
322
|
* muhaven.governance.* (2 — P11; cast_vote frontend runner deferred to Wave 5)
|
|
314
323
|
*
|
|
315
|
-
* `MUHAVEN_READ_ONLY=true` exposes only the
|
|
324
|
+
* `MUHAVEN_READ_ONLY=true` exposes only the 8 `muhaven.read.*` tools.
|
|
316
325
|
* P5's `muhaven.checkout.*` namespace was retired before Wave 4 close — the
|
|
317
326
|
* hosted checkout surface ships as a separate Vite SPA (apps/checkout-pay/),
|
|
318
327
|
* not as an MCP tool group.
|
package/dist/index.d.ts
CHANGED
|
@@ -263,6 +263,14 @@ declare class BackendClient {
|
|
|
263
263
|
* Authorization header.
|
|
264
264
|
*/
|
|
265
265
|
postUnauth<T>(path: string, body: unknown): Promise<T>;
|
|
266
|
+
/**
|
|
267
|
+
* GET variant that sends no Authorization header. Use for backend
|
|
268
|
+
* endpoints that are intentionally public (e.g. `/api/v1/tokens`
|
|
269
|
+
* which the marketplace + the 0.2.1 `positionBuy` NAV-conversion
|
|
270
|
+
* both read). Avoids triggering the AUTH_REQUIRED branch for the
|
|
271
|
+
* "not yet logged in" case on read paths that don't need auth.
|
|
272
|
+
*/
|
|
273
|
+
getUnauth<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
266
274
|
private buildUrl;
|
|
267
275
|
private exchangeWithRetry;
|
|
268
276
|
private exchange;
|
|
@@ -305,14 +313,15 @@ interface ToolDescriptor {
|
|
|
305
313
|
readonly sensitive: boolean;
|
|
306
314
|
}
|
|
307
315
|
/**
|
|
308
|
-
* The
|
|
309
|
-
* muhaven.read.* (
|
|
316
|
+
* The 23 MCP tools across five groups:
|
|
317
|
+
* muhaven.read.* (8 — incl. P11 protection_coverage + kyc_attestation
|
|
318
|
+
* + 0.2.1 read.activity for Path C settle verify)
|
|
310
319
|
* muhaven.position.* (4)
|
|
311
320
|
* muhaven.policy.* (4)
|
|
312
321
|
* muhaven.issuer.* (5 — P7)
|
|
313
322
|
* muhaven.governance.* (2 — P11; cast_vote frontend runner deferred to Wave 5)
|
|
314
323
|
*
|
|
315
|
-
* `MUHAVEN_READ_ONLY=true` exposes only the
|
|
324
|
+
* `MUHAVEN_READ_ONLY=true` exposes only the 8 `muhaven.read.*` tools.
|
|
316
325
|
* P5's `muhaven.checkout.*` namespace was retired before Wave 4 close — the
|
|
317
326
|
* hosted checkout surface ships as a separate Vite SPA (apps/checkout-pay/),
|
|
318
327
|
* not as an MCP tool group.
|
package/dist/index.js
CHANGED
|
@@ -373,6 +373,23 @@ var BackendClient = class {
|
|
|
373
373
|
false
|
|
374
374
|
);
|
|
375
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* GET variant that sends no Authorization header. Use for backend
|
|
378
|
+
* endpoints that are intentionally public (e.g. `/api/v1/tokens`
|
|
379
|
+
* which the marketplace + the 0.2.1 `positionBuy` NAV-conversion
|
|
380
|
+
* both read). Avoids triggering the AUTH_REQUIRED branch for the
|
|
381
|
+
* "not yet logged in" case on read paths that don't need auth.
|
|
382
|
+
*/
|
|
383
|
+
async getUnauth(path, query) {
|
|
384
|
+
const url = this.buildUrl(path, query);
|
|
385
|
+
return this.exchange(
|
|
386
|
+
"GET",
|
|
387
|
+
url,
|
|
388
|
+
void 0,
|
|
389
|
+
/* withAuth */
|
|
390
|
+
false
|
|
391
|
+
);
|
|
392
|
+
}
|
|
376
393
|
buildUrl(path, query) {
|
|
377
394
|
if (!path.startsWith("/")) {
|
|
378
395
|
throw new BackendError("bad_request", `path must start with "/": ${path}`);
|
|
@@ -496,16 +513,22 @@ var TOOL_DESCRIPTORS = [
|
|
|
496
513
|
description: `Return the authenticated user's tiered-autonomy audit log entries. Cursor-paginated. Useful for forensic review ("why was I paused?") and grant-reviewer demos. Read-only \u2014 never exposes other users' data.`,
|
|
497
514
|
sensitive: false
|
|
498
515
|
},
|
|
516
|
+
{
|
|
517
|
+
name: "muhaven.read.activity",
|
|
518
|
+
group: "read",
|
|
519
|
+
description: "Return the authenticated investor's on-chain activity feed (buys / sells / wraps / unwraps / yield claims / transfers). Each row carries token address, tx hash, block timestamp, and event type \u2014 but NEVER cleartext amounts (encrypted handles only, decryptable client-side via permit). USE THIS to verify a Path C dashboard action settled: after position.buy / position.sell / cash.wrap, the user opens the deep-link, taps Authorize, the on-chain tx lands \u2192 a new row appears here. Far more reliable than re-calling read.portfolio (which only changes shape when a NEW token enters the catalog).",
|
|
520
|
+
sensitive: false
|
|
521
|
+
},
|
|
499
522
|
{
|
|
500
523
|
name: "muhaven.position.buy",
|
|
501
524
|
group: "position",
|
|
502
|
-
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.
|
|
525
|
+
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. The tool fetches the current on-chain NAV for the token and converts the notional to integer shares (floor) before building the URL \u2014 so "Buy 3 mhUSDC of GOLD1" at NAV $0.01 becomes "Buy 300 GOLD1 shares (~3 mhUSDC)". Refuses with `amount_too_small_for_share` when the notional won\'t buy at least 1 share at current NAV; the error message tells the user the minimum mhUSDC needed. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.activity after the user confirms done (a new "buy" row with the tx hash will appear).',
|
|
503
526
|
sensitive: true
|
|
504
527
|
},
|
|
505
528
|
{
|
|
506
529
|
name: "muhaven.position.sell",
|
|
507
530
|
group: "position",
|
|
508
|
-
description:
|
|
531
|
+
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. Verify settlement by calling muhaven.read.activity (look for a "sell" or "sell-queued" row with the tx hash).',
|
|
509
532
|
sensitive: true
|
|
510
533
|
},
|
|
511
534
|
{
|
|
@@ -524,7 +547,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
524
547
|
{
|
|
525
548
|
name: "muhaven.cash.wrap",
|
|
526
549
|
group: "cash",
|
|
527
|
-
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).
|
|
550
|
+
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). Verify settlement by calling muhaven.read.activity (a new "wrap" row will appear with the tx hash).',
|
|
528
551
|
sensitive: true
|
|
529
552
|
},
|
|
530
553
|
{
|
|
@@ -692,6 +715,10 @@ var ReadAuditInputSchema = z.object({
|
|
|
692
715
|
cursor: z.string().min(1).max(512).optional(),
|
|
693
716
|
limit: z.number().int().min(1).max(200).optional()
|
|
694
717
|
}).strict();
|
|
718
|
+
var ReadActivityInputSchema = z.object({
|
|
719
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
720
|
+
offset: z.number().int().min(0).max(1e3).optional()
|
|
721
|
+
}).strict();
|
|
695
722
|
var decimalUsdcAmountSchema = z.string().regex(
|
|
696
723
|
/^(0|[1-9]\d*)(\.\d{1,6})?$/,
|
|
697
724
|
'must be a positive decimal mhUSDC amount with at most 6 fractional digits (e.g. "5", "0.5", "1234.567")'
|
|
@@ -812,6 +839,37 @@ function authRequiredPayload() {
|
|
|
812
839
|
};
|
|
813
840
|
}
|
|
814
841
|
|
|
842
|
+
// src/tools/decimal.ts
|
|
843
|
+
function parseDecimalToUsd6(decimal) {
|
|
844
|
+
const m = /^(\d+)(?:\.(\d+))?$/.exec(decimal);
|
|
845
|
+
if (!m) {
|
|
846
|
+
throw new Error(`Invalid decimal price: ${JSON.stringify(decimal)}`);
|
|
847
|
+
}
|
|
848
|
+
const intPart = m[1];
|
|
849
|
+
const fracPart = m[2] ?? "";
|
|
850
|
+
const fracPadded = (fracPart + "000000").slice(0, 6);
|
|
851
|
+
return BigInt(intPart + fracPadded);
|
|
852
|
+
}
|
|
853
|
+
function computeSharesFromUsd6(notionalUsd6, navUsd6) {
|
|
854
|
+
if (navUsd6 <= 0n) {
|
|
855
|
+
throw new Error("navUsd6 must be positive");
|
|
856
|
+
}
|
|
857
|
+
if (notionalUsd6 < 0n) {
|
|
858
|
+
throw new Error("notionalUsd6 must be non-negative");
|
|
859
|
+
}
|
|
860
|
+
return notionalUsd6 / navUsd6;
|
|
861
|
+
}
|
|
862
|
+
function formatUsd6AsDecimal(usd6) {
|
|
863
|
+
if (usd6 < 0n) {
|
|
864
|
+
throw new Error("usd6 must be non-negative");
|
|
865
|
+
}
|
|
866
|
+
const whole = usd6 / 1000000n;
|
|
867
|
+
const frac = usd6 % 1000000n;
|
|
868
|
+
if (frac === 0n) return whole.toString();
|
|
869
|
+
const fracStr = frac.toString().padStart(6, "0").replace(/0+$/, "");
|
|
870
|
+
return `${whole.toString()}.${fracStr}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
815
873
|
// src/tools/handlers.ts
|
|
816
874
|
function ok(data) {
|
|
817
875
|
return { ok: true, data };
|
|
@@ -880,6 +938,17 @@ async function readAudit(input, deps) {
|
|
|
880
938
|
return mapBackendError(e);
|
|
881
939
|
}
|
|
882
940
|
}
|
|
941
|
+
async function readActivity(input, deps) {
|
|
942
|
+
try {
|
|
943
|
+
const data = await deps.backend.get("/api/v1/activity", {
|
|
944
|
+
limit: input.limit,
|
|
945
|
+
offset: input.offset
|
|
946
|
+
});
|
|
947
|
+
return ok(data);
|
|
948
|
+
} catch (e) {
|
|
949
|
+
return mapBackendError(e);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
883
952
|
function buildPositionDeeplink(dashboardBaseUrl, action, params) {
|
|
884
953
|
const base = dashboardBaseUrl.replace(/\/+$/, "");
|
|
885
954
|
const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
|
|
@@ -892,17 +961,100 @@ function buildPositionDeeplink(dashboardBaseUrl, action, params) {
|
|
|
892
961
|
function resolveDashboardBaseUrl(deps) {
|
|
893
962
|
return deps.dashboardBaseUrl ?? "https://muhaven.app";
|
|
894
963
|
}
|
|
964
|
+
function resolveTokenInCatalog(identifier, catalog) {
|
|
965
|
+
const needle = identifier.toLowerCase();
|
|
966
|
+
return catalog.find(
|
|
967
|
+
(t) => t.address.toLowerCase() === needle || t.symbol.toLowerCase() === needle
|
|
968
|
+
) ?? null;
|
|
969
|
+
}
|
|
970
|
+
function sanitizeSymbolForLlmContext(raw) {
|
|
971
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "?");
|
|
972
|
+
return cleaned.length > 16 ? cleaned.slice(0, 16) : cleaned;
|
|
973
|
+
}
|
|
895
974
|
async function positionBuy(input, deps) {
|
|
975
|
+
let catalog;
|
|
976
|
+
try {
|
|
977
|
+
catalog = await deps.backend.getUnauth("/api/v1/tokens");
|
|
978
|
+
} catch (e) {
|
|
979
|
+
return mapBackendError(e);
|
|
980
|
+
}
|
|
981
|
+
const token = resolveTokenInCatalog(input.token, catalog.tokens ?? []);
|
|
982
|
+
if (!token) {
|
|
983
|
+
return err(
|
|
984
|
+
"token_not_found",
|
|
985
|
+
`Token "${sanitizeSymbolForLlmContext(input.token)}" is not in the MuHaven catalog. Call muhaven.read.tokens for the canonical symbol list.`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
const safeSymbol = sanitizeSymbolForLlmContext(token.symbol);
|
|
989
|
+
if (!token.latest_nav || !token.latest_nav.nav) {
|
|
990
|
+
return err(
|
|
991
|
+
"nav_unavailable",
|
|
992
|
+
`No NAV snapshot available for ${safeSymbol} yet. The nav-worker may not have written one \u2014 retry shortly, or use the dashboard /trade page directly.`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
let navUsd6;
|
|
996
|
+
try {
|
|
997
|
+
navUsd6 = parseDecimalToUsd6(token.latest_nav.nav);
|
|
998
|
+
} catch (e) {
|
|
999
|
+
return err(
|
|
1000
|
+
"nav_malformed",
|
|
1001
|
+
`NAV for ${safeSymbol} is not a valid decimal price. Open the dashboard /trade page directly.`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (navUsd6 <= 0n) {
|
|
1005
|
+
return err(
|
|
1006
|
+
"nav_non_positive",
|
|
1007
|
+
`NAV for ${safeSymbol} is non-positive. Cannot quote a buy.`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
let notionalUsd6;
|
|
1011
|
+
try {
|
|
1012
|
+
notionalUsd6 = parseDecimalToUsd6(input.amountUsdc);
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
return err(
|
|
1015
|
+
"invalid_amount",
|
|
1016
|
+
`amountUsdc "${input.amountUsdc}" is not a valid decimal mhUSDC amount.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (notionalUsd6 <= 0n) {
|
|
1020
|
+
return err(
|
|
1021
|
+
"invalid_amount",
|
|
1022
|
+
"amountUsdc must be greater than zero."
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const shares = computeSharesFromUsd6(notionalUsd6, navUsd6);
|
|
1026
|
+
if (shares <= 0n) {
|
|
1027
|
+
const navDisplay2 = formatUsd6AsDecimal(navUsd6);
|
|
1028
|
+
return err(
|
|
1029
|
+
"amount_too_small_for_share",
|
|
1030
|
+
`${input.amountUsdc} mhUSDC isn't enough to buy 1 share of ${safeSymbol} at the current NAV of $${navDisplay2}/share. Need at least ${navDisplay2} mhUSDC to buy 1 share. Ask the user for a larger amount, or chain muhaven.cash.wrap first if they're short on mhUSDC.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const effectiveNotionalUsd6 = shares * navUsd6;
|
|
1034
|
+
const effectiveNotionalDisplay = formatUsd6AsDecimal(effectiveNotionalUsd6);
|
|
1035
|
+
const navDisplay = formatUsd6AsDecimal(navUsd6);
|
|
1036
|
+
const sharesStr = shares.toString();
|
|
896
1037
|
const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
|
|
897
|
-
token:
|
|
898
|
-
amount:
|
|
1038
|
+
token: token.symbol,
|
|
1039
|
+
amount: sharesStr
|
|
899
1040
|
});
|
|
900
1041
|
return ok({
|
|
901
1042
|
dashboardUrl,
|
|
902
1043
|
action: "buy",
|
|
903
|
-
instructions: `Open this link to review and authorize the buy of ${
|
|
1044
|
+
instructions: `Open this link to review and authorize the buy of ${sharesStr} ${safeSymbol} shares (~${effectiveNotionalDisplay} mhUSDC at current NAV $${navDisplay}/share):
|
|
904
1045
|
${dashboardUrl}`,
|
|
905
|
-
echo: {
|
|
1046
|
+
echo: {
|
|
1047
|
+
action: "buy",
|
|
1048
|
+
token: token.symbol,
|
|
1049
|
+
amount: sharesStr,
|
|
1050
|
+
shares: sharesStr,
|
|
1051
|
+
// Carry the original request + the conversion math so the LLM
|
|
1052
|
+
// (and a human auditor reading the trace) can see why the URL
|
|
1053
|
+
// shows the share count instead of the user-stated notional.
|
|
1054
|
+
amountUsdc: input.amountUsdc,
|
|
1055
|
+
effectiveNotionalUsd6: effectiveNotionalUsd6.toString(),
|
|
1056
|
+
navUsd6: navUsd6.toString()
|
|
1057
|
+
}
|
|
906
1058
|
});
|
|
907
1059
|
}
|
|
908
1060
|
async function positionSell(input, deps) {
|
|
@@ -1144,6 +1296,10 @@ var HANDLERS = {
|
|
|
1144
1296
|
schema: ReadAuditInputSchema,
|
|
1145
1297
|
handler: readAudit
|
|
1146
1298
|
},
|
|
1299
|
+
"muhaven.read.activity": {
|
|
1300
|
+
schema: ReadActivityInputSchema,
|
|
1301
|
+
handler: readActivity
|
|
1302
|
+
},
|
|
1147
1303
|
"muhaven.position.buy": {
|
|
1148
1304
|
schema: PositionBuyInputSchema,
|
|
1149
1305
|
handler: positionBuy
|
|
@@ -1248,7 +1404,7 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
1248
1404
|
var SERVER_VERSION = resolveServerVersion();
|
|
1249
1405
|
function resolveServerVersion() {
|
|
1250
1406
|
{
|
|
1251
|
-
return "0.2.
|
|
1407
|
+
return "0.2.1";
|
|
1252
1408
|
}
|
|
1253
1409
|
}
|
|
1254
1410
|
function toJsonInputSchema(schema) {
|
package/manifest.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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.1",
|
|
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
|
-
"long_description": "MuHaven MCP exposes
|
|
8
|
+
"long_description": "MuHaven MCP exposes 23 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": {
|
|
10
10
|
"name": "MuHaven",
|
|
11
11
|
"email": "hello@muhaven.app",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
{ "name": "muhaven.read.distribution", "description": "Distribution status for a (token, epoch).", "sensitive": false },
|
|
43
43
|
{ "name": "muhaven.read.tokens", "description": "RWA tokens the user holds.", "sensitive": false },
|
|
44
44
|
{ "name": "muhaven.read.audit", "description": "User's tiered-autonomy audit log.", "sensitive": false },
|
|
45
|
-
{ "name": "muhaven.
|
|
45
|
+
{ "name": "muhaven.read.activity", "description": "User's on-chain activity feed (buys / sells / wraps / yields) — verify Path C settles.", "sensitive": false },
|
|
46
|
+
{ "name": "muhaven.position.buy", "description": "Prepare a Subscription buy (converts mhUSDC → integer shares at current NAV). Returns dashboard deep-link.", "sensitive": true },
|
|
46
47
|
{ "name": "muhaven.position.sell", "description": "Propose a redemption-queue sell. Returns unsigned UserOp.", "sensitive": true },
|
|
47
48
|
{ "name": "muhaven.position.claim", "description": "Propose a yield claim. Returns unsigned UserOp.", "sensitive": true },
|
|
48
49
|
{ "name": "muhaven.position.rebalance", "description": "Propose a multi-leg atomic rebalance.", "sensitive": true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhaven/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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-
|
|
2
|
+
"generatedAt": "2026-05-18T03:07:17.347Z",
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "muhaven.read.portfolio",
|
|
@@ -21,13 +21,17 @@
|
|
|
21
21
|
"name": "muhaven.read.audit",
|
|
22
22
|
"sha256": "a55b698911e8774b8022c98e86582368e76b12a7c8447afa57a4cfa9d3d6ae04"
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
"name": "muhaven.read.activity",
|
|
26
|
+
"sha256": "43294a47fa629b5a009a4fa3860aef4764d7c67ce7f9349cf70c4a1614c42d29"
|
|
27
|
+
},
|
|
24
28
|
{
|
|
25
29
|
"name": "muhaven.position.buy",
|
|
26
|
-
"sha256": "
|
|
30
|
+
"sha256": "3a26611be6f189d5c67d74e9db18f1c18888e2d9851222f94c320d975005b5c5"
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
33
|
"name": "muhaven.position.sell",
|
|
30
|
-
"sha256": "
|
|
34
|
+
"sha256": "2cd3cd7b542de0273b7314d82f8d8c811cff439d62c3f16fa38109a493bb9c7f"
|
|
31
35
|
},
|
|
32
36
|
{
|
|
33
37
|
"name": "muhaven.position.claim",
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
},
|
|
40
44
|
{
|
|
41
45
|
"name": "muhaven.cash.wrap",
|
|
42
|
-
"sha256": "
|
|
46
|
+
"sha256": "e74607e49092bd7090b9ce6df92936713c01e2da4ad5a728f55064d5026bcaec"
|
|
43
47
|
},
|
|
44
48
|
{
|
|
45
49
|
"name": "muhaven.policy.set_tier",
|