@cloakely/mcp 0.8.1 → 0.8.2

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 CHANGED
@@ -82,6 +82,12 @@ Then ask, for example:
82
82
  | | `my_bids` | The wallet's bids: opened, claimed, tokens won, refund |
83
83
  | | `claim` | Settle if nobody has, then claim tokens and refunds |
84
84
  | | `enable_private_trading` | Open a private vault for a settled launch's token |
85
+ | NFT drops | `list_drops` | Sealed NFT drops and WL spot sales, newest first, with phase, floor, WL boost and results |
86
+ | | `get_drop` | One drop in full: timing, floor, results, whether a wallet is on its GTD list or WL, and a spot sale's whitelist |
87
+ | | `place_drop_bid` | Seal a bid at the floor or more. A WL wallet gets its boost automatically |
88
+ | | `my_drop_bids` | The wallet's bids on a drop: sealed or opened, won, paid, refund |
89
+ | | `claim_drop` | Take NFTs (optionally to a fresh wallet) and refunds now, without waiting for the keeper |
90
+ | | `set_whitelist_wallets` | In a WL spot sale you won, name the Elysium wallets to whitelist, one per spot, until the deadline |
85
91
  | Private | `unlock` | Derive the private account from one local signature and return its `cloak:` address |
86
92
  | | `private_balance` | HYPE, USDC and launch tokens held privately |
87
93
  | | `shield` | HYPE or USDC from the wallet into the private balance (the one public step) |
package/dist/config.js CHANGED
@@ -32,6 +32,7 @@ export function loadSettings(env = process.env) {
32
32
  factoryBlock: launchesFromBlock,
33
33
  routerBlock: BigInt(f.routerBlock),
34
34
  vaultsBlock: f.vaultsBlock === undefined ? undefined : BigInt(f.vaultsBlock),
35
+ dropFactoryBlock: f.dropFactoryBlock === undefined ? undefined : BigInt(f.dropFactoryBlock),
35
36
  assets: f.assets?.map((a) => ({ ...a, fromBlock: BigInt(a.fromBlock) })),
36
37
  };
37
38
  }
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.8.1";
6
+ export declare const VERSION = "0.8.2";
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,11 +4,11 @@ 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, buildClaimLink, buildRequestLink, linkAddress, linkCheck, linkKeys, openLinkKeys, newLinkSecret, parseClaimLink, parseRequestLink, LINK_SHARES_MAX, } 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, CloakDrops, CloakSpots, } 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.8.1";
11
+ export const VERSION = "0.8.2";
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
 
@@ -18,6 +18,8 @@ Private balances: shield moves HYPE or USDC from the wallet into a private balan
18
18
 
19
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".
20
20
 
21
+ NFT drops and WL spot sales: list_drops and get_drop show sealed NFT drops (and sales of a partner collection's whitelist spots). place_drop_bid seals a bid at the creator's price or more (WL wallets get their public boost automatically); bids open by themselves after bidding closes and every winner pays one price. claim_drop takes NFTs (optionally to a fresh wallet) and refunds without waiting for the keeper. In a WL spot sale, set_whitelist_wallets names the Elysium wallet to whitelist for each spot won, until the sale's deadline.
22
+
21
23
  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.`;
22
24
  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).
23
25
 
@@ -393,6 +395,88 @@ export function createCloakServer(settings, opts = {}) {
393
395
  }
394
396
  return { vaults: out, note: "Each vault is one shared pool of encrypted notes. The chain shows the total, not the owners or their balances." };
395
397
  }));
398
+ // ───────────── NFT drops and WL spot sales ─────────────
399
+ const dropsFor = (w) => {
400
+ if (!d.dropFactory)
401
+ throw new Error("NFT drops aren't deployed on this network yet.");
402
+ return new CloakDrops(pub, w, d.dropFactory, d.dropFactoryBlock ?? 0n);
403
+ };
404
+ const spotsFor = (w) => (d.spotBook ? new CloakSpots(dropsFor(w), d.spotBook) : null);
405
+ const dropPhase = (s) => (s.failed ? "ended with no sale (every bid refunded)" : s.phase === "gtd" ? "gtd (guaranteed list minting; bidding opens after)" : s.phase === "settle" || s.phase === "numbering" ? "settling" : s.phase);
406
+ async function dropView(s) {
407
+ const c = dropsFor();
408
+ const [det, sale] = await Promise.all([c.details(s.address).catch(() => ({})), spotsFor()?.sale(s.address).catch(() => null) ?? null]);
409
+ return {
410
+ drop: s.address,
411
+ name: s.name,
412
+ symbol: s.symbol,
413
+ kind: sale ? `WL spot sale for ${sale.partner}` : "NFT drop",
414
+ phase: dropPhase(s),
415
+ ...(s.phase === "gtd" ? { biddingOpens: iso(s.gtdEnd) } : {}),
416
+ biddingEnds: iso(s.commitEnd),
417
+ [sale ? "spots" : "supply"]: s.supply,
418
+ floor: `${formatEther(s.floor)} HYPE each (the creator's price; bids go up in ${formatEther(s.tickSize)} HYPE steps)`,
419
+ maxPerBid: s.maxPerBid,
420
+ ...(s.hasWl ? { wlBoost: `${s.wlBoostBps / 100}% (WL bids rank this much higher, and WL winners pay that much less, never under the floor)` } : {}),
421
+ ...(s.hasGtd ? { gtd: `${s.gtdMinted} minted at ${formatEther(s.gtdPrice)} HYPE` } : {}),
422
+ sealedBids: s.bids,
423
+ bidsOpened: s.reveals,
424
+ ...(s.settled && !s.failed ? { sold: s.sold, publicWinnersPaid: hype(s.publicPrice), ...(s.hasWl ? { wlWinnersPaid: hype(s.wlPrice) } : {}) } : {}),
425
+ ...(sale ? { walletsDeadline: iso(sale.deadline) } : {}),
426
+ ...(det.description ? { description: det.description } : {}),
427
+ page: `https://cloak.boblabs.tech/app/drop/${s.address}`,
428
+ };
429
+ }
430
+ const bidView = (b, settled) => ({
431
+ id: b.id.toString(),
432
+ wl: b.wl,
433
+ deposit: hype(b.deposit),
434
+ ...(b.revealed ? { bid: `${b.quantity} at ${formatEther(b.price)} HYPE` } : { bid: "sealed" }),
435
+ ...(settled ? (b.revealed ? { won: b.nfts, paid: hype(b.paid), refund: hype(b.refund) } : { won: 0, note: "never opened: 90% of the deposit comes back" }) : {}),
436
+ delivered: b.claimed,
437
+ });
438
+ server.registerTool("list_drops", {
439
+ title: "List NFT drops",
440
+ description: "Sealed NFT drops and WL spot sales, newest first, with phase, floor, WL boost and results.",
441
+ inputSchema: { limit: z.number().int().min(1).max(50).optional().describe("How many (default 10)"), live_only: z.boolean().optional().describe("Only drops still minting or taking bids") },
442
+ annotations: READ,
443
+ }, safe(async ({ limit, live_only }) => {
444
+ const c = dropsFor();
445
+ const all = (await c.list()).reverse();
446
+ const out = [];
447
+ for (const a of all) {
448
+ if (out.length >= (limit ?? 10))
449
+ break;
450
+ const s = await c.get(a);
451
+ if (live_only && s.phase !== "gtd" && s.phase !== "bidding")
452
+ continue;
453
+ out.push(await dropView(s));
454
+ }
455
+ return { total: all.length, drops: out };
456
+ }));
457
+ server.registerTool("get_drop", {
458
+ title: "Drop details",
459
+ description: "One NFT drop or WL spot sale: phase, timing, floor, WL boost, results once settled, whether a wallet is on its GTD list or WL, and for a spot sale the whitelist so far.",
460
+ inputSchema: { drop: addr, wallet: addr.optional().describe("A wallet to check against the drop's GTD list and WL") },
461
+ annotations: READ,
462
+ }, safe(async ({ drop, wallet: w }) => {
463
+ if (!isAddress(drop))
464
+ throw new Error("That isn't a drop address.");
465
+ const c = dropsFor();
466
+ const s = await c.get(drop);
467
+ const view = await dropView(s);
468
+ const who = (w && isAddress(w) ? w : account?.address);
469
+ const listed = who ? await c.listed(s.address, who) : null;
470
+ const sp = spotsFor();
471
+ const sale = sp ? await sp.sale(s.address) : null;
472
+ const whitelist = sale && s.phase === "settled" && !s.failed ? (await sp.winners(s.address)).flatMap((x) => Array.from({ length: x.spots }, (_, i) => ({ bid: x.bidId.toString(), wallet: x.wallets[i] ?? "not named yet" }))) : undefined;
473
+ return {
474
+ ...view,
475
+ ...(listed && who ? { listing: { wallet: who, onWl: listed.wl, gtdSpots: listed.gtdSpots, gtdMinted: listed.gtdMinted } } : {}),
476
+ ...(whitelist ? { whitelist } : {}),
477
+ ...(s.phase === "bidding" ? { note: "Bids are sealed: nobody can see demand until bidding closes." } : {}),
478
+ };
479
+ }));
396
480
  if (opts.hosted) {
397
481
  server.registerTool("plan_launch", {
398
482
  title: "Plan a launch",
@@ -976,5 +1060,123 @@ export function createCloakServer(settings, opts = {}) {
976
1060
  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
1061
  });
978
1062
  }));
1063
+ server.registerTool("place_drop_bid", {
1064
+ title: "Bid on an NFT drop",
1065
+ description: "Seal a bid on an NFT drop or WL spot sale: up to max_price HYPE each for quantity NFTs (or spots). Nobody can read it until bidding closes; it opens by itself afterwards. A wallet on the drop's WL bids with its boost automatically. Spends the deposit from the wallet; whatever isn't paid at the clearing price comes back.",
1066
+ inputSchema: { drop: addr, max_price: amt("Most you'd pay for each, in HYPE (at least the floor)"), quantity: z.number().int().min(1).max(20).optional().describe("How many (default 1)"), dry_run: dry },
1067
+ annotations: WRITE,
1068
+ }, safe(async ({ drop, max_price, quantity, dry_run }) => {
1069
+ const { wallet: w, account, spending } = needWallet();
1070
+ if (!isAddress(drop))
1071
+ throw new Error("That isn't a drop address.");
1072
+ const c = dropsFor(w);
1073
+ const s = await c.get(drop);
1074
+ if (s.phase !== "bidding")
1075
+ throw new Error(s.phase === "gtd" ? `Bidding opens at ${iso(s.gtdEnd)}, when the GTD window closes.` : "Bidding on this drop has closed.");
1076
+ const price = amountOf(max_price, 18);
1077
+ if (price === null)
1078
+ throw new Error(`"${max_price}" isn't an amount of HYPE.`);
1079
+ if (price < s.floor)
1080
+ throw new Error(`Bid at least the floor, ${formatEther(s.floor)} HYPE.`);
1081
+ const qty = quantity ?? 1;
1082
+ if (qty > s.maxPerBid)
1083
+ throw new Error(`This drop takes at most ${s.maxPerBid} per bid.`);
1084
+ const tick = CloakDrops.tickFor(s, price);
1085
+ const level = CloakDrops.priceAt(s, tick);
1086
+ const deposit = level * BigInt(qty);
1087
+ const { wl } = await c.listed(s.address, account.address);
1088
+ spending.check("hype", deposit);
1089
+ const plan = { drop: s.address, quantity: qty, bidLevel: `${formatEther(level)} HYPE each${level < price ? ` (your ${max_price} snaps down to the nearest level)` : ""}`, deposit: hype(deposit), wl, ...(wl ? { ranksAs: `${formatEther((level * BigInt(10_000 + s.wlBoostBps)) / 10000n)} HYPE each` } : {}), biddingEnds: iso(s.commitEnd) };
1090
+ if (isDry(dry_run))
1091
+ return { dryRun: true, wouldBid: plan };
1092
+ return exclusive(async () => {
1093
+ const b = await c.bid(s.address, { price: level, quantity: qty, wl });
1094
+ spending.record("hype", deposit);
1095
+ return { placed: true, bidId: b.id.toString(), ...plan, tx: link(b.txHash), next: "The bid opens by itself after bidding closes. Check results with get_drop or my_drop_bids, then claim_drop." };
1096
+ });
1097
+ }));
1098
+ server.registerTool("my_drop_bids", { title: "My drop bids", description: "This wallet's bids on a drop: sealed or opened, and once settled what each won, paid and gets back.", inputSchema: { drop: addr }, annotations: READ }, safe(async ({ drop }) => {
1099
+ const { account } = needWallet();
1100
+ if (!isAddress(drop))
1101
+ throw new Error("That isn't a drop address.");
1102
+ const c = dropsFor();
1103
+ const s = await c.get(drop);
1104
+ const mine = await c.bids(s.address, account.address);
1105
+ const held = s.settled ? await c.refundOwed(s.address, account.address) : 0n;
1106
+ return { drop: s.address, phase: dropPhase(s), bids: mine.map((b) => bidView(b, s.phase === "settled")), ...(held > 0n ? { refundWaiting: hype(held) } : {}) };
1107
+ }));
1108
+ server.registerTool("claim_drop", {
1109
+ title: "Claim from a drop",
1110
+ description: "Once a drop settles, take this wallet's NFTs and refunds now instead of waiting for the keeper (which delivers about an hour after settling). to sends the NFTs to another wallet, a fresh one say; refunds always come back to the bidding wallet.",
1111
+ inputSchema: { drop: addr, to: addr.optional().describe("Where the NFTs go (default: this wallet)"), dry_run: dry },
1112
+ annotations: WRITE,
1113
+ }, safe(async ({ drop, to, dry_run }) => {
1114
+ const { wallet: w, account } = needWallet();
1115
+ if (!isAddress(drop))
1116
+ throw new Error("That isn't a drop address.");
1117
+ if (to && !isAddress(to))
1118
+ throw new Error(`${to} isn't a wallet address.`);
1119
+ const c = dropsFor(w);
1120
+ const s = await c.get(drop);
1121
+ if (s.phase !== "settled")
1122
+ throw new Error("This drop hasn't settled yet. Bids open by themselves after bidding closes, then it settles.");
1123
+ const open = (await c.bids(s.address, account.address)).filter((b) => !b.claimed);
1124
+ const held = await c.refundOwed(s.address, account.address);
1125
+ if (!open.length && held === 0n)
1126
+ return { nothingToClaim: true, note: "Every bid from this wallet has already been delivered." };
1127
+ const plan = { bids: open.map((b) => ({ id: b.id.toString(), nfts: b.nfts, refund: hype(b.refund) })), nftsTo: to ?? account.address, ...(held > 0n ? { heldRefund: hype(held) } : {}) };
1128
+ if (isDry(dry_run))
1129
+ return { dryRun: true, wouldClaim: plan };
1130
+ return exclusive(async () => {
1131
+ const txs = [];
1132
+ for (const b of open)
1133
+ txs.push(link(to && b.nfts > 0 ? await c.claimTo(s.address, b.id, to) : await c.claim(s.address, b.id)));
1134
+ if (held > 0n)
1135
+ txs.push(link(await c.withdrawRefund(s.address, account.address)));
1136
+ return { claimed: true, ...plan, txs };
1137
+ });
1138
+ }));
1139
+ server.registerTool("set_whitelist_wallets", {
1140
+ title: "Name wallets for WL spots",
1141
+ description: "In a WL spot sale this wallet won, name the Elysium wallets to whitelist, one per spot won (a fresh wallet is fine). Calling again replaces them, until the sale's deadline.",
1142
+ inputSchema: { drop: addr, wallets: z.array(z.string()).min(1).max(20).describe("One 0x wallet per spot won, in order"), dry_run: dry },
1143
+ annotations: WRITE,
1144
+ }, safe(async ({ drop, wallets, dry_run }) => {
1145
+ const { wallet: w, account } = needWallet();
1146
+ if (!isAddress(drop))
1147
+ throw new Error("That isn't a drop address.");
1148
+ const sp = spotsFor(w);
1149
+ if (!sp)
1150
+ throw new Error("WL spot sales aren't deployed on this network yet.");
1151
+ const sale = await sp.sale(drop);
1152
+ if (!sale)
1153
+ throw new Error("This drop isn't a WL spot sale.");
1154
+ const s = await sp.drops.get(drop);
1155
+ if (s.phase !== "settled")
1156
+ throw new Error("Wallets can be named once the sale settles.");
1157
+ if (Date.now() / 1000 >= sale.deadline)
1158
+ throw new Error(`The deadline to name wallets passed at ${iso(sale.deadline)}, so the whitelist is final.`);
1159
+ const wins = (await sp.drops.bids(s.address, account.address)).filter((b) => b.nfts > 0);
1160
+ const spots = wins.reduce((a, b) => a + b.nfts, 0);
1161
+ if (!spots)
1162
+ throw new Error("This wallet didn't win a spot in this sale.");
1163
+ const bad = wallets.find((x) => !isAddress(x.trim()));
1164
+ if (bad !== undefined)
1165
+ throw new Error(`${bad} isn't a wallet address.`);
1166
+ if (wallets.length !== spots)
1167
+ throw new Error(`This wallet won ${spots} spot${spots > 1 ? "s" : ""}: name exactly ${spots} wallet${spots > 1 ? "s" : ""}.`);
1168
+ if (new Set(wallets.map((x) => x.trim().toLowerCase())).size !== wallets.length)
1169
+ throw new Error("Each spot needs a different wallet.");
1170
+ let k = 0;
1171
+ const plan = wins.map((b) => ({ bid: b.id.toString(), wallets: wallets.slice(k, (k += b.nfts)).map((x) => x.trim()) }));
1172
+ if (isDry(dry_run))
1173
+ return { dryRun: true, partner: sale.partner, wouldName: plan, deadline: iso(sale.deadline) };
1174
+ return exclusive(async () => {
1175
+ const txs = [];
1176
+ for (const p of plan)
1177
+ txs.push(link(await sp.setWallets(s.address, BigInt(p.bid), p.wallets)));
1178
+ return { named: true, partner: sale.partner, wallets: plan, deadline: iso(sale.deadline), txs, note: "Change them any time before the deadline by calling this again." };
1179
+ });
1180
+ }));
979
1181
  return server;
980
1182
  }
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.8.1",
4
+ "version": "0.8.2",
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.8.1"
50
+ "@cloakely/sdk": "^0.8.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "22.10.2",