@cloakely/mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -23,3 +23,8 @@ export interface Limits {
23
23
  launchesPerDay: number;
24
24
  }
25
25
  export declare function loadSettings(env?: NodeJS.ProcessEnv): Settings;
26
+ /**
27
+ * `settings` with the current factory, router, batcher, migrator and vaults read from CloakConfig's registry, so a
28
+ * router added on-chain is used without a new release. Keeps the configured deployment if the registry can't be read.
29
+ */
30
+ export declare function withLiveDeployment(settings: Settings): Promise<Settings>;
package/dist/config.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { defineChain, parseEther, parseUnits } from "viem";
5
- import { deploymentFor, elysiumTestnet } from "@cloakely/sdk";
4
+ import { createPublicClient, defineChain, http, parseEther, parseUnits } from "viem";
5
+ import { deploymentFor, elysiumTestnet, liveDeployment } from "@cloakely/sdk";
6
6
  const NETWORKS = {
7
7
  testnet: { chain: elysiumTestnet },
8
8
  };
@@ -24,7 +24,14 @@ export function loadSettings(env = process.env) {
24
24
  rpcUrl = f.rpcUrl;
25
25
  chain = defineChain({ id: f.chainId, name: `Cloak custom (${f.chainId})`, nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } }, testnet: true });
26
26
  launchesFromBlock = BigInt(f.launchesFromBlock ?? 0);
27
- deployment = { ...f, version: f.version ?? 0, factoryBlock: launchesFromBlock, routerBlock: BigInt(f.routerBlock), assets: f.assets?.map((a) => ({ ...a, fromBlock: BigInt(a.fromBlock) })) };
27
+ deployment = {
28
+ ...f,
29
+ version: f.version ?? 0,
30
+ factoryBlock: launchesFromBlock,
31
+ routerBlock: BigInt(f.routerBlock),
32
+ vaultsBlock: f.vaultsBlock === undefined ? undefined : BigInt(f.vaultsBlock),
33
+ assets: f.assets?.map((a) => ({ ...a, fromBlock: BigInt(a.fromBlock) })),
34
+ };
28
35
  }
29
36
  else {
30
37
  const n = NETWORKS[network];
@@ -71,3 +78,13 @@ export function loadSettings(env = process.env) {
71
78
  },
72
79
  };
73
80
  }
81
+ /**
82
+ * `settings` with the current factory, router, batcher, migrator and vaults read from CloakConfig's registry, so a
83
+ * router added on-chain is used without a new release. Keeps the configured deployment if the registry can't be read.
84
+ */
85
+ export async function withLiveDeployment(settings) {
86
+ const pub = createPublicClient({ chain: settings.chain, transport: http(settings.rpcUrl, { retryCount: 3, timeout: 15_000 }) });
87
+ const deployment = await liveDeployment(pub, settings.deployment).catch(() => settings.deployment);
88
+ const moved = deployment.factory.toLowerCase() !== settings.deployment.factory.toLowerCase();
89
+ return { ...settings, deployment, launchesFromBlock: moved ? deployment.factoryBlock : settings.launchesFromBlock };
90
+ }
package/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
- import { loadSettings } from "./config.js";
2
+ import { loadSettings, withLiveDeployment } from "./config.js";
3
3
  import { createCloakServer } from "./server.js";
4
4
  /**
5
5
  * The hosted MCP endpoint as a web-standard handler (Request in, Response out), for any serverless runtime.
@@ -39,6 +39,8 @@ function limited(ip) {
39
39
  return h.n > REQUESTS_PER_MINUTE ? Math.ceil((h.start + 60_000 - now) / 1000) : 0;
40
40
  }
41
41
  let settings;
42
+ let settingsAt = 0;
43
+ const SETTINGS_TTL = 10 * 60_000; // re-read the registry every 10 minutes
42
44
  /** `settings` defaults to Elysium testnet, without a key (tests pass their own). */
43
45
  export async function handleMcpRequest(req, opts = {}) {
44
46
  if (req.method === "OPTIONS")
@@ -75,7 +77,11 @@ export async function handleMcpRequest(req, opts = {}) {
75
77
  headers.delete("transfer-encoding");
76
78
  const forward = new Request(req.url, { method: "POST", headers });
77
79
  // never a key here, whatever the environment says
78
- const s = opts.settings ?? (settings ??= loadSettings({ CLOAK_NETWORK: "testnet", CLOAK_RPC_URL: process.env.CLOAK_RPC_URL, CLOAK_RELAYER_URL: process.env.CLOAK_RELAYER_URL }));
80
+ if (!opts.settings && (!settings || Date.now() - settingsAt > SETTINGS_TTL)) {
81
+ settingsAt = Date.now();
82
+ settings = withLiveDeployment(loadSettings({ CLOAK_NETWORK: "testnet", CLOAK_RPC_URL: process.env.CLOAK_RPC_URL, CLOAK_RELAYER_URL: process.env.CLOAK_RELAYER_URL }));
83
+ }
84
+ const s = opts.settings ?? (await settings);
79
85
  const server = createCloakServer(s, { hosted: true });
80
86
  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
81
87
  try {
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
  // Cloak's MCP server over stdio: `npx @cloakely/mcp`. Configuration is environment variables only (see README).
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { createCloakServer, loadSettings, VERSION } from "./server.js";
5
+ import { withLiveDeployment } from "./config.js";
5
6
  try {
6
- const settings = loadSettings();
7
+ const settings = await withLiveDeployment(loadSettings());
7
8
  const server = createCloakServer(settings);
8
9
  await server.connect(new StdioServerTransport());
9
10
  // stdout carries the protocol, so anything for humans goes to stderr
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.2.0";
6
+ export declare const VERSION = "0.3.0";
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,15 +4,15 @@ 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, cloakPairAbi, createProver, loadProver, privateRouterAbi, relayerSubmitter, shieldedPoolAbi, } from "@cloakely/sdk";
7
+ import { Cloak, CloakPrivate, cloakAuctionAbi, cloakPairAbi, cloakVaultsAbi, createProver, loadProver, privateRouterAbi, relayerSubmitter, shieldedPoolAbi, } 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.2.0";
11
+ export const VERSION = "0.3.0";
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
- 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 pays out tokens and refunds (the keeper usually pushes claims already), and enable_private_trading opens a private vault for the token if nobody has yet.
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
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. Amounts are in whole units ("0.5" HYPE), and send, sell and withdraw accept "max".
18
18
 
@@ -98,8 +98,9 @@ export function createCloakServer(settings, opts = {}) {
98
98
  if (!wallet)
99
99
  throw new Error("No wallet configured: set CLOAK_KEY_FILE (or CLOAK_PRIVATE_KEY) to use private balances.");
100
100
  privP ??= (async () => {
101
- const keys = await CloakPrivate.keysFromWallet(wallet, { chainId: settings.chain.id, router: d.router, verify: true });
102
- const p = new CloakPrivate(pub, d.router, keys, prover, submit, d.routerBlock, d.batcher, d.migrator);
101
+ const keys = await CloakPrivate.keysFromWallet(wallet, { chainId: settings.chain.id, verify: true });
102
+ // notes are scanned from the permanent vaults' first block, whichever router is current
103
+ const p = new CloakPrivate(pub, d.router, keys, prover, submit, d.vaultsBlock ?? d.routerBlock, d.batcher, d.migrator);
103
104
  for (const a of d.assets ?? [])
104
105
  p.addAsset(a);
105
106
  return p;
@@ -130,6 +131,9 @@ export function createCloakServer(settings, opts = {}) {
130
131
  symbols.set(k, await pub.readContract({ address: token, abi: erc20Abi, functionName: "symbol" }).catch(() => "?"));
131
132
  return symbols.get(k);
132
133
  }
134
+ async function decimalsOf(token) {
135
+ return Number(await pub.readContract({ address: token, abi: erc20Abi, functionName: "decimals" }).catch(() => 18));
136
+ }
133
137
  /** Every asset with a private vault: HYPE, USDC-like assets, and every launch token with a private market. */
134
138
  async function vaults() {
135
139
  const c = { address: d.router, abi: privateRouterAbi };
@@ -137,14 +141,20 @@ export function createCloakServer(settings, opts = {}) {
137
141
  const out = [{ kind: "hype", symbol: "HYPE", token: d.whype, pool: d.hypePool, decimals: 18 }];
138
142
  for (const a of d.assets ?? [])
139
143
  out.push({ kind: "asset", symbol: a.symbol, token: a.token, pool: a.pool, decimals: a.decimals });
140
- const toks = await Promise.all(Array.from({ length: Number(n) - 1 }, (_, i) => pub.readContract({ ...c, functionName: "tokens", args: [BigInt(i + 1)] })));
144
+ const toks = await Promise.all(Array.from({ length: Number(n) }, (_, i) => pub.readContract({ ...c, functionName: "tokens", args: [BigInt(i)] })));
141
145
  for (const token of toks) {
146
+ if (out.some((a) => a.token.toLowerCase() === token.toLowerCase()))
147
+ continue; // HYPE, or an asset already listed
142
148
  const [pool, pair, symbol] = await Promise.all([
143
149
  pub.readContract({ ...c, functionName: "poolOf", args: [token] }),
144
150
  pub.readContract({ ...c, functionName: "pairOf", args: [token] }),
145
151
  symbolOf(token),
146
152
  ]);
147
- out.push({ kind: "token", symbol, token, pool, pair, decimals: 18 });
153
+ // a vault without a launch pair holds an existing token (USDC): an asset, not a market
154
+ if (pair === zeroAddress)
155
+ out.push({ kind: "asset", symbol, token, pool, decimals: await decimalsOf(token) });
156
+ else
157
+ out.push({ kind: "token", symbol, token, pool, pair, decimals: 18 });
148
158
  }
149
159
  return out;
150
160
  }
@@ -479,29 +489,55 @@ export function createCloakServer(settings, opts = {}) {
479
489
  bids: bids.map((b) => ({ id: b.id, deposit: hype(b.deposit), opened: b.revealed, claimed: b.claimed, ...(s.settled ? { tokensWon: formatEther(b.tokens), refund: hype(b.refund), penalty: hype(b.penalty) } : {}) })),
480
490
  };
481
491
  }));
482
- server.registerTool("claim", { title: "Claim tokens and refunds", description: "After bids open, settle the launch if nobody has yet, then claim the wallet's tokens and refunds. Cloak's keeper usually pushes claims already.", inputSchema: { auction: addr, dry_run: dry }, annotations: WRITE }, safe(async ({ auction, dry_run }) => {
492
+ server.registerTool("claim", {
493
+ title: "Claim tokens and refunds",
494
+ description: "After bids open, settle the launch if nobody has yet, then claim the wallet's tokens and refunds. Won tokens go straight into the private balance (privately: true, the default) when the launch supports it, so later trades can't be linked to the bid; refunds go to the wallet. Cloak's keeper pushes unclaimed bids to wallets publicly about an hour after bidding closes.",
495
+ inputSchema: { auction: addr, privately: z.boolean().optional().describe("Claim won tokens into the private balance (default true where supported)"), dry_run: dry },
496
+ annotations: WRITE,
497
+ }, safe(async ({ auction, privately, dry_run }) => {
483
498
  const { account } = needWallet();
484
499
  let s = await cloak.getAuction(auction);
485
500
  if (s.phase === "commit" || s.phase === "reveal")
486
501
  throw new Error(`${s.tokenSymbol} can't settle yet: bids are ${s.phase === "commit" ? "still being placed" : "still opening"} until ${iso(s.phase === "commit" ? s.commitEnd : s.revealEnd)}.`);
487
502
  const bids = await cloak.bidsOf(s.address, account.address, await bidsFrom(s.address));
488
503
  const open = bids.filter((b) => !b.claimed);
504
+ // private claims: launches from this deployment's factory, once their private vault is open
505
+ const canPrivate = async () => privately !== false && !s.failed && (await bidsFrom(s.address)) === settings.launchesFromBlock && (await privateTradingOn(s.token));
489
506
  if (isDry(dry_run))
490
- return { dryRun: true, wouldSettle: !s.settled, wouldClaim: open.map((b) => b.id) };
507
+ return { dryRun: true, wouldSettle: !s.settled, wouldClaim: open.map((b) => b.id), privately: s.settled ? await canPrivate() : privately !== false };
491
508
  return exclusive(async () => {
492
509
  const txs = [];
493
510
  if (!s.settled) {
494
511
  txs.push(link(await cloak.settle(s.address)));
495
512
  s = await cloak.getAuction(s.address);
496
513
  }
497
- if (open.length)
498
- txs.push(link(await cloak.claim(s.address, open.map((b) => b.id))));
514
+ const priv8 = await canPrivate();
515
+ const won = [];
516
+ let publicWhy;
517
+ if (priv8) {
518
+ const p = await priv();
519
+ for (const b of open.filter((x) => x.tokens > 0n)) {
520
+ // a deposit limit can refuse a large claim; that bid is then claimed to the wallet instead
521
+ const ok = await pub
522
+ .simulateContract({ address: s.address, abi: cloakAuctionAbi, functionName: "claimPrivate", args: [b.id, 1n, 1n, "0x"], account: account.address })
523
+ .then(() => true, (e) => ((publicWhy = explain(e)), false));
524
+ if (!ok)
525
+ continue;
526
+ txs.push(link((await p.claimPrivately(wallet, s.address, b.id)).txHash));
527
+ won.push(b);
528
+ }
529
+ }
530
+ const rest = open.filter((b) => !won.includes(b));
531
+ if (rest.length)
532
+ txs.push(link(await cloak.claim(s.address, rest.map((b) => b.id))));
499
533
  const after = await cloak.bidsOf(s.address, account.address, await bidsFrom(s.address));
500
534
  return {
501
535
  launch: s.tokenSymbol,
502
536
  settled: s.settled,
503
537
  ...(s.failed ? { failed: "No demand was revealed, so every bid was refunded in full." } : { clearingPrice: `${formatEther(s.clearingPrice)} HYPE per token` }),
504
538
  claimed: open.length ? open.map((b) => b.id) : "nothing left to claim (already paid out)",
539
+ ...(won.length ? { claimedPrivately: won.map((b) => b.id), privateBalance: "won tokens are in the private balance (private_balance)" } : {}),
540
+ ...(publicWhy ? { claimedToWallet: `some won tokens went to the wallet instead of the private balance: ${publicWhy}` } : {}),
505
541
  tokensWon: formatEther(after.reduce((a, b) => a + b.tokens, 0n)),
506
542
  refunds: hype(after.reduce((a, b) => a + b.refund, 0n)),
507
543
  txs,
@@ -515,17 +551,14 @@ export function createCloakServer(settings, opts = {}) {
515
551
  throw new Error(`${s.tokenSymbol} needs to settle successfully first (phase: ${s.phase}).`);
516
552
  if (await privateTradingOn(s.token))
517
553
  return { alreadyOn: true, token: s.token };
518
- const all = await cloak.listLaunches();
519
- const idx = all.findIndex((l) => l.auction.toLowerCase() === s.address.toLowerCase());
520
- if (idx < 0)
554
+ const l = (await cloak.listLaunches()).find((x) => x.auction.toLowerCase() === s.address.toLowerCase());
555
+ if (!l)
521
556
  throw new Error("This launch isn't from a Cloak factory, so it can't get a private vault.");
522
- // earlier deployments' launches go through the router's list of earlier factories, in the order they're listed
523
- const legacy = !!all[idx].legacy;
524
- const index = BigInt(legacy ? idx - all.filter((l) => !l.legacy).length : idx);
557
+ const vaults = d.vaults ?? (await pub.readContract({ address: d.router, abi: privateRouterAbi, functionName: "vaults" }));
525
558
  if (isDry(dry_run))
526
- return { dryRun: true, wouldEnable: s.tokenSymbol, launchIndex: index, earlierDeployment: legacy };
559
+ return { dryRun: true, wouldEnable: s.tokenSymbol, factory: l.factory, launchIndex: l.index };
527
560
  return exclusive(async () => {
528
- const h = await wallet.writeContract({ address: d.router, abi: privateRouterAbi, functionName: legacy ? "createLegacyPool" : "createPool", args: [index] });
561
+ const h = await wallet.writeContract({ address: vaults, abi: cloakVaultsAbi, functionName: "createPool", args: [l.factory, l.index] });
529
562
  await receipt(h);
530
563
  return { enabled: true, token: s.token, vault: await pub.readContract({ address: d.router, abi: privateRouterAbi, functionName: "poolOf", args: [s.token] }), tx: link(h) };
531
564
  });
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.2.0",
4
+ "version": "0.3.0",
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.3.0"
50
+ "@cloakely/sdk": "^0.4.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "22.10.2",