@cloakely/mcp 0.7.1 → 0.8.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/README.md +6 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +205 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@ Then ask, for example:
|
|
|
65
65
|
- "Bid on EMBR up to 0.00002 HYPE for 50,000 tokens. Dry run first."
|
|
66
66
|
- "Shield 0.5 HYPE, then buy CLOAK privately with 0.1 of it."
|
|
67
67
|
- "Send 0.05 private HYPE to cloak:…, then withdraw the rest to 0x…"
|
|
68
|
+
- "Make a red envelope: 0.1 HYPE split into 5 shares, with the message gm"
|
|
68
69
|
|
|
69
70
|
## Tools
|
|
70
71
|
|
|
@@ -88,6 +89,11 @@ Then ask, for example:
|
|
|
88
89
|
| | `batch_order` | Trade in standard sizes inside a batch round, which hides the trade's size |
|
|
89
90
|
| | `private_send` | Pay a `cloak:` address. Sender, recipient and amount stay hidden |
|
|
90
91
|
| | `withdraw` | Private balance to a normal address (ideally a fresh one) |
|
|
92
|
+
| | `create_payment_link` | HYPE or USDC in a link anyone can claim, to a private balance or any wallet with no gas. Message, passcode and riddle, or split into shares (a red envelope) |
|
|
93
|
+
| | `claim_payment_link` | Claim a link into the private balance or to a `0x` address; also takes back your own unclaimed link |
|
|
94
|
+
| | `check_payment_link` | What a link still holds, and how many shares are left |
|
|
95
|
+
| | `request_payment_link` | A link that asks to be paid HYPE or USDC, with an optional amount, note and name |
|
|
96
|
+
| | `pay_payment_request` | Pay a request link from the private balance, the exact amount asked |
|
|
91
97
|
|
|
92
98
|
Amounts are in whole units (`"0.5"` HYPE). `private_send`, `withdraw` and a `private_swap` sell accept `"max"`.
|
|
93
99
|
|
package/dist/server.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type Prover } from "@cloakely/sdk";
|
|
|
3
3
|
import type { Settings } from "./config.js";
|
|
4
4
|
export { loadSettings, type Settings, type Limits } from "./config.js";
|
|
5
5
|
export { Spending } from "./limits.js";
|
|
6
|
-
export declare const VERSION = "0.
|
|
6
|
+
export declare const VERSION = "0.8.1";
|
|
7
7
|
export declare const HOSTED_URL = "https://cloak.boblabs.tech/mcp";
|
|
8
8
|
/** Turn an SDK or chain error into one plain sentence an agent can act on. */
|
|
9
9
|
export declare function explain(e: unknown): string;
|
package/dist/server.js
CHANGED
|
@@ -4,17 +4,19 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { createPublicClient, createWalletClient, erc20Abi, formatEther, formatUnits, http, isAddress, parseEther, parseUnits, zeroAddress, } from "viem";
|
|
6
6
|
import { privateKeyToAccount } from "viem/accounts";
|
|
7
|
-
import { Cloak, CloakPrivate, cloakAuctionAbi, cloakPairAbi, cloakVaultsAbi, legacyFactories, createProver, loadProver, createSharedProver, loadSharedProver, privateRouterAbi, relayerSubmitter, sharedRelayerSubmitter, sharedVaultAbi, SharedVaultClient, shieldedPoolAbi, } from "@cloakely/sdk";
|
|
7
|
+
import { Cloak, CloakPrivate, cloakAuctionAbi, cloakPairAbi, cloakVaultsAbi, legacyFactories, createProver, loadProver, createSharedProver, loadSharedProver, privateRouterAbi, relayerSubmitter, sharedRelayerSubmitter, sharedVaultAbi, SharedVaultClient, shieldedPoolAbi, buildClaimLink, buildRequestLink, linkAddress, linkCheck, linkKeys, openLinkKeys, newLinkSecret, parseClaimLink, parseRequestLink, LINK_SHARES_MAX, } from "@cloakely/sdk";
|
|
8
8
|
import { Spending } from "./limits.js";
|
|
9
9
|
export { loadSettings } from "./config.js";
|
|
10
10
|
export { Spending } from "./limits.js";
|
|
11
|
-
export const VERSION = "0.
|
|
11
|
+
export const VERSION = "0.8.1";
|
|
12
12
|
export const HOSTED_URL = "https://cloak.boblabs.tech/mcp";
|
|
13
13
|
const INSTRUCTIONS = `Cloak: sealed-bid token launches and private trading on Kinetiq's Elysium (HYPE is the gas and base currency).
|
|
14
14
|
|
|
15
15
|
Launches: create_launch starts a sealed-bid auction. place_sealed_bid commits a bid that nobody can read until bidding closes; bids then open by themselves (the keeper reveals them from the drand beacon), so there is nothing to come back for. Everyone who wins pays one clearing price. After settlement, claim puts won tokens straight into the private balance (and refunds into the wallet); the keeper opens each launch's private vault at settlement, and pushes bids nobody claimed to wallets publicly about an hour later. enable_private_trading opens a private vault for an older launch if nobody has yet.
|
|
16
16
|
|
|
17
|
-
Private balances: shield moves HYPE or USDC from the wallet into a private balance (the one public step). private_swap, batch_order, private_send and withdraw then run from that balance: each is proven on this machine and sent by a relayer, so the wallet never appears on chain. batch_order also hides trade size. withdraw lands funds at a normal address, ideally a fresh one.
|
|
17
|
+
Private balances: shield moves HYPE or USDC from the wallet into a private balance (the one public step). private_swap, batch_order, private_send and withdraw then run from that balance: each is proven on this machine and sent by a relayer, so the wallet never appears on chain. batch_order also hides trade size. withdraw lands funds at a normal address, ideally a fresh one.
|
|
18
|
+
|
|
19
|
+
Payment links (HYPE, USDC): create_payment_link puts money in a link anyone can claim (into their private balance, or to any wallet with no gas), optionally locked with a passcode and riddle or split into shares; claim_payment_link claims one (or takes back your own), check_payment_link shows what is left. request_payment_link makes a link that asks to be paid, and pay_payment_request pays one. Amounts are in whole units ("0.5" HYPE), and send, sell and withdraw accept "max".
|
|
18
20
|
|
|
19
21
|
Safety: every action that moves value is checked against per-transaction and daily spending limits (see cloak_info), and any write tool takes dry_run: true to see exactly what would happen without signing anything. Use dry_run first when unsure.`;
|
|
20
22
|
const HOSTED_INSTRUCTIONS = `Cloak's hosted MCP server: read and plan for Cloak, sealed-bid token launches and private trading on Kinetiq's Elysium testnet (HYPE is the gas and base currency).
|
|
@@ -774,5 +776,205 @@ export function createCloakServer(settings, opts = {}) {
|
|
|
774
776
|
return { withdrew: fmtAmt(a, x), arrived: fmtAmt(a, arrives), to, privateBalance: fmtAmt(a, await balanceOf(a)), tx: link(h) };
|
|
775
777
|
});
|
|
776
778
|
}));
|
|
779
|
+
// ───────────── payment links ─────────────
|
|
780
|
+
/** HYPE or USDC: the only tokens a payment link carries today. */
|
|
781
|
+
async function linkAsset(asset) {
|
|
782
|
+
const a = await resolve(asset);
|
|
783
|
+
if (a.kind === "token")
|
|
784
|
+
throw new Error("Payment links carry HYPE or USDC.");
|
|
785
|
+
return a;
|
|
786
|
+
}
|
|
787
|
+
/** A wallet-less client for the private account a claim link opens. */
|
|
788
|
+
async function linkClient(url, passcode) {
|
|
789
|
+
const l = parseClaimLink(url);
|
|
790
|
+
if (l.locked && passcode === undefined)
|
|
791
|
+
throw new Error(`This link is locked. Pass its passcode${l.hint ? ` (the answer to: "${l.hint}")` : ""}.`);
|
|
792
|
+
// a locked link with a check refuses a wrong passcode here, before anything touches the network
|
|
793
|
+
const c = new CloakPrivate(pub, d.router, await openLinkKeys(l, passcode), prover, submit, d.vaultsBlock ?? d.routerBlock, d.batcher, d.migrator);
|
|
794
|
+
for (const a of d.assets ?? [])
|
|
795
|
+
c.addAsset(a);
|
|
796
|
+
await c.loadMarkets();
|
|
797
|
+
return { l, c };
|
|
798
|
+
}
|
|
799
|
+
const linkTokenOf = (c, a) => (a.kind === "hype" ? c.hypeMarket.token : a.token);
|
|
800
|
+
server.registerTool("create_payment_link", {
|
|
801
|
+
title: "Send money as a link",
|
|
802
|
+
description: "Put HYPE or USDC from the private balance into a link anyone can claim: into their private balance, or straight to any wallet with no gas (the relayer takes its fee from the link). Optional: a message, a passcode (with a riddle shown to whoever opens it), or several equal shares that different people each claim (a red envelope). Treat the link like cash and keep it: it is the only way to the money, and take_back works with it until someone claims.",
|
|
803
|
+
inputSchema: {
|
|
804
|
+
asset: z.enum(["HYPE", "USDC"]),
|
|
805
|
+
amount: amt("Total the link holds (split evenly across shares)"),
|
|
806
|
+
message: z.string().max(140).optional().describe("A message shown to whoever opens it"),
|
|
807
|
+
from: z.string().max(40).optional().describe("A name to show as the sender"),
|
|
808
|
+
passcode: z.string().max(80).optional().describe("Lock it: the link alone then opens nothing"),
|
|
809
|
+
hint: z.string().max(120).optional().describe("A riddle or hint shown on the claim page, whose answer is the passcode"),
|
|
810
|
+
shares: z.number().int().min(1).max(LINK_SHARES_MAX).optional().describe("Split into this many equal shares (default 1)"),
|
|
811
|
+
cover_claim_fee: z.boolean().optional().describe("Add the relayer's claim fee so they get the full amount (default true)"),
|
|
812
|
+
dry_run: dry,
|
|
813
|
+
},
|
|
814
|
+
annotations: WRITE,
|
|
815
|
+
}, safe(async (x) => {
|
|
816
|
+
const { spending } = needWallet();
|
|
817
|
+
const a = await linkAsset(x.asset);
|
|
818
|
+
const whole = amountOf(x.amount, a.decimals);
|
|
819
|
+
if (whole === null)
|
|
820
|
+
throw new Error("Give an exact amount for the link.");
|
|
821
|
+
if (x.hint && !x.passcode)
|
|
822
|
+
throw new Error("A hint needs a passcode: the hint is the riddle, the passcode its answer.");
|
|
823
|
+
const n = x.shares ?? 1;
|
|
824
|
+
const share = whole / BigInt(n);
|
|
825
|
+
if (share === 0n)
|
|
826
|
+
throw new Error("That's too little to split that many ways.");
|
|
827
|
+
const p = await priv();
|
|
828
|
+
const kinds = a.kind === "hype" ? ["transact", "unshieldHype"] : ["transact"];
|
|
829
|
+
const fees = await Promise.all(kinds.map((k) => p.relayFee(k, a.pool)));
|
|
830
|
+
const claimFee = x.cover_claim_fee === false ? 0n : fees.reduce((m, q) => (q.fee > m ? q.fee : m), 0n);
|
|
831
|
+
const each = share + claimFee;
|
|
832
|
+
const total = each * BigInt(n);
|
|
833
|
+
const c = await cost(a, total);
|
|
834
|
+
spending.check(c.budget, c.value);
|
|
835
|
+
if (isDry(x.dry_run))
|
|
836
|
+
return { dryRun: true, wouldLock: `${fmtAmt(a, total)} in ${n} share${n > 1 ? "s" : ""} of ${fmtAmt(a, each)}`, claimFeeCovered: fmtAmt(a, claimFee * BigInt(n)), locked: !!x.passcode };
|
|
837
|
+
return exclusive(async () => {
|
|
838
|
+
const secret = newLinkSecret();
|
|
839
|
+
const keys = await linkKeys(secret, x.passcode);
|
|
840
|
+
const to = await linkAddress(keys);
|
|
841
|
+
const check = x.passcode ? await linkCheck(keys) : undefined;
|
|
842
|
+
let block = 0n;
|
|
843
|
+
const txs = [];
|
|
844
|
+
for (let i = 0; i < n; i++) {
|
|
845
|
+
const h = await p.send(linkTokenOf(p, a), to, each);
|
|
846
|
+
block = (await receipt(h)).blockNumber;
|
|
847
|
+
txs.push(link(h));
|
|
848
|
+
}
|
|
849
|
+
spending.record(c.budget, c.value);
|
|
850
|
+
const url = buildClaimLink({ secret, block, token: a.symbol, amount: formatUnits(n > 1 ? share : whole, a.decimals), message: x.message, from: x.from, locked: !!x.passcode, hint: x.hint, shares: n, check });
|
|
851
|
+
return {
|
|
852
|
+
link: url,
|
|
853
|
+
holds: `${fmtAmt(a, whole)}${n > 1 ? ` in ${n} shares of ${fmtAmt(a, share)}` : ""}`,
|
|
854
|
+
...(x.passcode ? { passcode: x.passcode, sendThePasscodeSeparately: true } : {}),
|
|
855
|
+
txs,
|
|
856
|
+
keepThisLink: "Anyone with the link can claim it. To take the money back before then, call claim_payment_link with it.",
|
|
857
|
+
};
|
|
858
|
+
});
|
|
859
|
+
}));
|
|
860
|
+
server.registerTool("check_payment_link", {
|
|
861
|
+
title: "Check a payment link",
|
|
862
|
+
description: "What a claim link still holds (and how many shares are left), without claiming it.",
|
|
863
|
+
inputSchema: { link: z.string().describe("The claim link (https://…/claim#…)"), passcode: z.string().optional() },
|
|
864
|
+
annotations: READ,
|
|
865
|
+
}, safe(async ({ link: url, passcode }) => {
|
|
866
|
+
const { l, c } = await linkClient(url, passcode);
|
|
867
|
+
const out = {};
|
|
868
|
+
let notes = 0;
|
|
869
|
+
let spent = 0;
|
|
870
|
+
for (const sym of ["HYPE", "USDC"]) {
|
|
871
|
+
const a = await linkAsset(sym).catch(() => null);
|
|
872
|
+
if (!a)
|
|
873
|
+
continue;
|
|
874
|
+
const all = await c.history(a.pool);
|
|
875
|
+
const live = all.filter((x) => !x.spent);
|
|
876
|
+
notes += live.length;
|
|
877
|
+
spent += all.length - live.length;
|
|
878
|
+
out[sym] = fmtAmt(a, live.reduce((s, x) => s + x.amount, 0n));
|
|
879
|
+
}
|
|
880
|
+
const status = notes > 0 ? (spent > 0 ? "partly claimed" : "waiting") : spent > 0 ? "claimed" : l.locked && !l.check ? "empty (a wrong passcode, already claimed, or not funded yet)" : "not funded yet";
|
|
881
|
+
return { holds: out, status, claimed: spent, ...(l.shares ? { sharesLeft: `${notes} of ${l.shares}` } : {}), ...(l.message ? { message: l.message } : {}), ...(l.from ? { from: l.from } : {}), empty: notes === 0 };
|
|
882
|
+
}));
|
|
883
|
+
server.registerTool("claim_payment_link", {
|
|
884
|
+
title: "Claim a payment link",
|
|
885
|
+
description: "Claim the money in a claim link: into this account's private balance (default), or out to any 0x address. Also how a sender takes back a link nobody claimed. The relayer submits it and takes its fee from the link, so no gas is needed. On a link split into shares, it claims one share.",
|
|
886
|
+
inputSchema: { link: z.string().describe("The claim link"), passcode: z.string().optional(), to: z.string().optional().describe('"private" (default) or a 0x address'), dry_run: dry },
|
|
887
|
+
annotations: WRITE,
|
|
888
|
+
}, safe(async ({ link: url, passcode, to, dry_run }) => {
|
|
889
|
+
const { l, c } = await linkClient(url, passcode);
|
|
890
|
+
const out = to && to !== "private";
|
|
891
|
+
if (out && !isAddress(to))
|
|
892
|
+
throw new Error('`to` must be "private" or a 0x address.');
|
|
893
|
+
const held = [];
|
|
894
|
+
let spent = 0;
|
|
895
|
+
for (const sym of ["HYPE", "USDC"]) {
|
|
896
|
+
const a = await linkAsset(sym).catch(() => null);
|
|
897
|
+
if (!a)
|
|
898
|
+
continue;
|
|
899
|
+
const all = await c.history(a.pool);
|
|
900
|
+
spent += all.filter((x) => x.spent).length;
|
|
901
|
+
if (all.some((x) => !x.spent))
|
|
902
|
+
held.push(a);
|
|
903
|
+
}
|
|
904
|
+
if (!held.length)
|
|
905
|
+
throw new Error(spent > 0 ? "This link is empty: it was already claimed, or the sender took it back."
|
|
906
|
+
: l.locked && !l.check ? "Nothing opens with that passcode: it's wrong, the link was already claimed, or its payment hasn't landed yet."
|
|
907
|
+
: "This link holds nothing yet: the sender's payment hasn't landed. Try again in a minute.");
|
|
908
|
+
if (isDry(dry_run))
|
|
909
|
+
return { dryRun: true, holds: await Promise.all(held.map(async (a) => fmtAmt(a, await c.balance(a.pool)))), wouldClaimTo: out ? to : "this account's private balance", ...(l.shares ? { oneShareOf: l.shares } : {}) };
|
|
910
|
+
return exclusive(async () => {
|
|
911
|
+
const dest = out ? to : await (await priv()).address();
|
|
912
|
+
const claimed = [];
|
|
913
|
+
for (const a of held) {
|
|
914
|
+
let r;
|
|
915
|
+
for (let attempt = 0;; attempt++) {
|
|
916
|
+
try {
|
|
917
|
+
r = await c.sweep(linkTokenOf(c, a), dest, () => { }, { one: !!l.shares });
|
|
918
|
+
await receipt(r.txHash);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
catch (e) {
|
|
922
|
+
if (!l.shares || attempt >= 3 || !/already|spent|nullifier|being used/i.test(String(e)))
|
|
923
|
+
throw e;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const got = out ? await pub.readContract({ address: a.pool, abi: shieldedPoolAbi, functionName: "afterFee", args: [r.amount] }) : r.amount;
|
|
927
|
+
claimed.push({ claimed: fmtAmt(a, got), tx: link(r.txHash) });
|
|
928
|
+
if (l.shares)
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
return { claimed, to: out ? to : "this account's private balance", ...(l.message ? { message: l.message } : {}) };
|
|
932
|
+
});
|
|
933
|
+
}));
|
|
934
|
+
server.registerTool("request_payment_link", {
|
|
935
|
+
title: "Request a payment with a link",
|
|
936
|
+
description: "A link that asks to be paid HYPE or USDC, with an optional amount (leave it out for a tip jar), note and name. By default it pays this account's cloak: address privately; pass a 0x address to be paid there instead. Nothing goes on chain until someone pays.",
|
|
937
|
+
inputSchema: { asset: z.enum(["HYPE", "USDC"]), amount: z.string().optional().describe('Amount in whole units, e.g. "25"'), note: z.string().max(140).optional(), name: z.string().max(40).optional(), to: z.string().optional().describe("A 0x address or cloak: address to be paid at (default: this account's cloak: address)") },
|
|
938
|
+
annotations: READ,
|
|
939
|
+
}, safe(async ({ asset, amount, note, name, to }) => {
|
|
940
|
+
const payTo = to ?? (await (await priv()).address());
|
|
941
|
+
return { link: buildRequestLink({ to: payTo, token: asset, amount, note, name }), payTo, howTheyPay: payTo.startsWith("cloak:") ? "a private transfer: nobody sees who paid whom, or how much" : "from their private balance to this address: their wallet never shows; the amount and this address are public" };
|
|
942
|
+
}));
|
|
943
|
+
server.registerTool("pay_payment_request", {
|
|
944
|
+
title: "Pay a request link",
|
|
945
|
+
description: "Pay a request link (https://…/pay#to=…) from the private balance: a private transfer to a cloak: address, or a withdrawal to a 0x address that covers Cloak's fee so the payee gets the exact amount.",
|
|
946
|
+
inputSchema: { link: z.string().describe("The request link"), amount: z.string().optional().describe("Needed when the request leaves the amount to the payer"), dry_run: dry },
|
|
947
|
+
annotations: WRITE,
|
|
948
|
+
}, safe(async ({ link: url, amount, dry_run }) => {
|
|
949
|
+
const { spending } = needWallet();
|
|
950
|
+
const r = parseRequestLink(url);
|
|
951
|
+
const a = await linkAsset(r.token);
|
|
952
|
+
const raw = r.amount ?? amount;
|
|
953
|
+
if (!raw)
|
|
954
|
+
throw new Error("This request leaves the amount to you: pass `amount`.");
|
|
955
|
+
if (r.amount && amount && amount !== r.amount)
|
|
956
|
+
throw new Error(`The request is for ${r.amount} ${r.token}; it can't be paid with a different amount.`);
|
|
957
|
+
const units = amountOf(raw, a.decimals);
|
|
958
|
+
if (units === null)
|
|
959
|
+
throw new Error("Give an exact amount.");
|
|
960
|
+
const p = await priv();
|
|
961
|
+
const toCloak = r.to.startsWith("cloak:");
|
|
962
|
+
let gross = units;
|
|
963
|
+
if (!toCloak) {
|
|
964
|
+
const one = 10n ** BigInt(a.decimals);
|
|
965
|
+
const after = await pub.readContract({ address: a.pool, abi: shieldedPoolAbi, functionName: "afterFee", args: [one] });
|
|
966
|
+
gross = after > 0n ? (units * one + after - 1n) / after : units;
|
|
967
|
+
}
|
|
968
|
+
const c = await cost(a, gross);
|
|
969
|
+
spending.check(c.budget, c.value);
|
|
970
|
+
if (isDry(dry_run))
|
|
971
|
+
return { dryRun: true, wouldPay: fmtAmt(a, units), to: r.to, ...(r.note ? { note: r.note } : {}), ...(r.name ? { requestedBy: r.name } : {}), ...(toCloak ? {} : { leavesYourBalance: fmtAmt(a, gross) }) };
|
|
972
|
+
return exclusive(async () => {
|
|
973
|
+
const h = toCloak ? await p.send(linkTokenOf(p, a), r.to, gross) : a.kind === "hype" ? await p.unshieldHype(r.to, gross) : await p.unshieldToken(a.token, r.to, gross);
|
|
974
|
+
await receipt(h);
|
|
975
|
+
spending.record(c.budget, c.value);
|
|
976
|
+
return { paid: fmtAmt(a, units), to: r.to, ...(r.note ? { for: r.note } : {}), tx: link(h), privateBalance: fmtAmt(a, await p.balance(a.pool)) };
|
|
977
|
+
});
|
|
978
|
+
}));
|
|
777
979
|
return server;
|
|
778
980
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloakely/mcp",
|
|
3
3
|
"mcpName": "io.github.NECOKIZZ/cloak",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"description": "Cloak's MCP server: let an AI agent launch tokens with sealed bids and trade them privately on Elysium, with spending limits, testnet by default and a local signer.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
48
48
|
"viem": "2.21.54",
|
|
49
49
|
"zod": "4.6.7",
|
|
50
|
-
"@cloakely/sdk": "^0.
|
|
50
|
+
"@cloakely/sdk": "^0.8.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "22.10.2",
|