@cloakely/mcp 0.2.0 → 0.4.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/README.md +16 -10
- package/dist/config.d.ts +7 -0
- package/dist/config.js +26 -4
- package/dist/http.js +8 -2
- package/dist/index.js +12 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +57 -24
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +41 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@ An agent that trades on chain leaks its strategy with every transaction, and any
|
|
|
6
6
|
|
|
7
7
|
**Testnet only for now** (Elysium testnet, chain `99801`). Play money only.
|
|
8
8
|
|
|
9
|
+
**The easy way:** tell your agent
|
|
10
|
+
|
|
11
|
+
> Read https://cloak.boblabs.tech/agent.md and set me up to trade privately.
|
|
12
|
+
|
|
13
|
+
That page is written for agents. The agent sets up its own wallet and the connection, and asks you only for what it can't do itself (free testnet HYPE from the faucet). It never needs your own wallet's key.
|
|
14
|
+
|
|
9
15
|
There are two ways in:
|
|
10
16
|
|
|
11
17
|
| | Hosted link | Local server (this package) |
|
|
@@ -25,16 +31,18 @@ Its tools: `cloak_info`, `list_launches`, `get_launch`, `quote_bid`, `quote_swap
|
|
|
25
31
|
|
|
26
32
|
## Set up the local server
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
Make the agent's own wallet. It's saved in `~/.cloak-agent-key`, readable by you only, and the key is never printed:
|
|
29
35
|
|
|
30
36
|
```sh
|
|
31
|
-
|
|
37
|
+
npx -y @cloakely/mcp setup
|
|
32
38
|
```
|
|
33
39
|
|
|
40
|
+
It prints the wallet's address. Fund it with free testnet HYPE from the [Elysium faucet](https://elysium.kinetiq.xyz/testnet-faucet). Running it again keeps the same wallet.
|
|
41
|
+
|
|
34
42
|
**Claude Code:**
|
|
35
43
|
|
|
36
44
|
```sh
|
|
37
|
-
claude mcp add cloak --
|
|
45
|
+
claude mcp add cloak -- npx -y @cloakely/mcp
|
|
38
46
|
```
|
|
39
47
|
|
|
40
48
|
**Claude Desktop, Cursor and other clients** (`claude_desktop_config.json`, `.cursor/mcp.json`, and so on):
|
|
@@ -42,16 +50,14 @@ claude mcp add cloak --env CLOAK_KEY_FILE=$HOME/.cloak-agent-key -- npx -y @cloa
|
|
|
42
50
|
```json
|
|
43
51
|
{
|
|
44
52
|
"mcpServers": {
|
|
45
|
-
"cloak": {
|
|
46
|
-
"command": "npx",
|
|
47
|
-
"args": ["-y", "@cloakely/mcp"],
|
|
48
|
-
"env": { "CLOAK_KEY_FILE": "/Users/you/.cloak-agent-key" }
|
|
49
|
-
}
|
|
53
|
+
"cloak": { "command": "npx", "args": ["-y", "@cloakely/mcp"] }
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
The server uses `~/.cloak-agent-key` when nothing else is set. To use another wallet, set `CLOAK_KEY_FILE` (or `CLOAK_PRIVATE_KEY`) in the server's `env`.
|
|
59
|
+
|
|
60
|
+
Without a key (no `~/.cloak-agent-key` and nothing set), the server is **read only**: the agent can browse launches and quote bids and swaps, but can't sign anything.
|
|
55
61
|
|
|
56
62
|
Then ask, for example:
|
|
57
63
|
- "What Cloak launches are open right now?"
|
|
@@ -98,7 +104,7 @@ Amounts are in whole units (`"0.5"` HYPE). `private_send`, `withdraw` and a `pri
|
|
|
98
104
|
|
|
99
105
|
| Variable | Default | |
|
|
100
106
|
|---|---|---|
|
|
101
|
-
| `CLOAK_KEY_FILE` | | File holding the agent wallet's private key
|
|
107
|
+
| `CLOAK_KEY_FILE` | `~/.cloak-agent-key` if it exists | File holding the agent wallet's private key |
|
|
102
108
|
| `CLOAK_PRIVATE_KEY` | | The key itself, if a file isn't practical |
|
|
103
109
|
| `CLOAK_NETWORK` | `testnet` | `testnet`, or `custom` with `CLOAK_DEPLOYMENT_FILE` |
|
|
104
110
|
| `CLOAK_MAX_HYPE_PER_TX` | `1` | HYPE value per transaction |
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Chain, type Hex } from "viem";
|
|
2
2
|
import { type CloakDeployment } from "@cloakely/sdk";
|
|
3
|
+
/** The agent wallet's key file that `setup` creates and the server uses when no key is configured. */
|
|
4
|
+
export declare const defaultKeyFile: () => string;
|
|
3
5
|
/** Where the server points, and what it may do. Everything comes from environment variables (see README). */
|
|
4
6
|
export interface Settings {
|
|
5
7
|
network: string;
|
|
@@ -23,3 +25,8 @@ export interface Limits {
|
|
|
23
25
|
launchesPerDay: number;
|
|
24
26
|
}
|
|
25
27
|
export declare function loadSettings(env?: NodeJS.ProcessEnv): Settings;
|
|
28
|
+
/**
|
|
29
|
+
* `settings` with the current factory, router, batcher, migrator and vaults read from CloakConfig's registry, so a
|
|
30
|
+
* router added on-chain is used without a new release. Keeps the configured deployment if the registry can't be read.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withLiveDeployment(settings: Settings): Promise<Settings>;
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, 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
|
+
/** The agent wallet's key file that `setup` creates and the server uses when no key is configured. */
|
|
7
|
+
export const defaultKeyFile = () => join(homedir(), ".cloak-agent-key");
|
|
6
8
|
const NETWORKS = {
|
|
7
9
|
testnet: { chain: elysiumTestnet },
|
|
8
10
|
};
|
|
@@ -24,7 +26,14 @@ export function loadSettings(env = process.env) {
|
|
|
24
26
|
rpcUrl = f.rpcUrl;
|
|
25
27
|
chain = defineChain({ id: f.chainId, name: `Cloak custom (${f.chainId})`, nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } }, testnet: true });
|
|
26
28
|
launchesFromBlock = BigInt(f.launchesFromBlock ?? 0);
|
|
27
|
-
deployment = {
|
|
29
|
+
deployment = {
|
|
30
|
+
...f,
|
|
31
|
+
version: f.version ?? 0,
|
|
32
|
+
factoryBlock: launchesFromBlock,
|
|
33
|
+
routerBlock: BigInt(f.routerBlock),
|
|
34
|
+
vaultsBlock: f.vaultsBlock === undefined ? undefined : BigInt(f.vaultsBlock),
|
|
35
|
+
assets: f.assets?.map((a) => ({ ...a, fromBlock: BigInt(a.fromBlock) })),
|
|
36
|
+
};
|
|
28
37
|
}
|
|
29
38
|
else {
|
|
30
39
|
const n = NETWORKS[network];
|
|
@@ -42,6 +51,9 @@ export function loadSettings(env = process.env) {
|
|
|
42
51
|
let key = env.CLOAK_PRIVATE_KEY?.trim();
|
|
43
52
|
if (!key && env.CLOAK_KEY_FILE)
|
|
44
53
|
key = readFileSync(env.CLOAK_KEY_FILE, "utf8").trim();
|
|
54
|
+
// the wallet `npx @cloakely/mcp setup` made, when nothing else is configured
|
|
55
|
+
else if (!key && existsSync(defaultKeyFile()))
|
|
56
|
+
key = readFileSync(defaultKeyFile(), "utf8").trim();
|
|
45
57
|
if (key && !key.startsWith("0x"))
|
|
46
58
|
key = `0x${key}`;
|
|
47
59
|
if (key && !/^0x[0-9a-fA-F]{64}$/.test(key))
|
|
@@ -71,3 +83,13 @@ export function loadSettings(env = process.env) {
|
|
|
71
83
|
},
|
|
72
84
|
};
|
|
73
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* `settings` with the current factory, router, batcher, migrator and vaults read from CloakConfig's registry, so a
|
|
88
|
+
* router added on-chain is used without a new release. Keeps the configured deployment if the registry can't be read.
|
|
89
|
+
*/
|
|
90
|
+
export async function withLiveDeployment(settings) {
|
|
91
|
+
const pub = createPublicClient({ chain: settings.chain, transport: http(settings.rpcUrl, { retryCount: 3, timeout: 15_000 }) });
|
|
92
|
+
const deployment = await liveDeployment(pub, settings.deployment).catch(() => settings.deployment);
|
|
93
|
+
const moved = deployment.factory.toLowerCase() !== settings.deployment.factory.toLowerCase();
|
|
94
|
+
return { ...settings, deployment, launchesFromBlock: moved ? deployment.factoryBlock : settings.launchesFromBlock };
|
|
95
|
+
}
|
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
|
-
|
|
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
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Cloak's MCP server over stdio: `npx @cloakely/mcp`. Configuration is environment variables
|
|
2
|
+
// Cloak's MCP server over stdio: `npx @cloakely/mcp`. Configuration is environment variables (see README).
|
|
3
|
+
// `npx @cloakely/mcp setup` makes the agent's wallet in ~/.cloak-agent-key, which the server then uses by default.
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { createCloakServer, loadSettings, VERSION } from "./server.js";
|
|
6
|
+
import { withLiveDeployment } from "./config.js";
|
|
7
|
+
import { setup } from "./setup.js";
|
|
8
|
+
if (process.argv[2] === "setup") {
|
|
9
|
+
// make the agent's wallet and print what's left to do (see https://cloak.boblabs.tech/agent.md)
|
|
10
|
+
await setup().then(() => process.exit(0), (e) => {
|
|
11
|
+
console.error(`cloak-mcp setup: ${e.message}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
5
15
|
try {
|
|
6
|
-
const settings = loadSettings();
|
|
16
|
+
const settings = await withLiveDeployment(loadSettings());
|
|
7
17
|
const server = createCloakServer(settings);
|
|
8
18
|
await server.connect(new StdioServerTransport());
|
|
9
19
|
// 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.
|
|
6
|
+
export declare const VERSION = "0.4.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.
|
|
11
|
+
export const VERSION = "0.4.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
|
|
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,
|
|
102
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
|
@@ -383,14 +393,14 @@ export function createCloakServer(settings, opts = {}) {
|
|
|
383
393
|
}));
|
|
384
394
|
server.registerTool("connect_full_agent", { title: "Connect a full agent", description: "How to let an agent launch, bid and trade privately: the local Cloak MCP server, which signs on your own machine under spending limits.", inputSchema: {}, annotations: READ }, safe(async () => ({
|
|
385
395
|
why: "This hosted server never holds keys, so it can read and plan but not sign. Launching, bidding and private trading run in the local server, on the machine that holds the agent's wallet. Its proofs are made there too, so nobody else, Cloak included, can see or move the funds.",
|
|
396
|
+
easiest: "If you can run terminal commands, read https://cloak.boblabs.tech/agent.md and follow it: it sets everything up and says what to ask the person. If you can't (a chat app), tell the person to open that link in Claude Code, Claude Desktop or Cursor and ask the agent there to follow it.",
|
|
386
397
|
steps: [
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
'Other clients: { "mcpServers": { "cloak": { "command": "npx", "args": ["-y", "@cloakely/mcp"], "env": { "CLOAK_KEY_FILE": "/path/to/key" } } } }',
|
|
398
|
+
"npx -y @cloakely/mcp setup (makes the agent's own wallet in ~/.cloak-agent-key and prints its address; the key is never printed)",
|
|
399
|
+
"Claude Code: claude mcp add cloak -- npx -y @cloakely/mcp. Other clients: { \"mcpServers\": { \"cloak\": { \"command\": \"npx\", \"args\": [\"-y\", \"@cloakely/mcp\"] } } }",
|
|
400
|
+
"The person gets free testnet HYPE for the agent's address at https://elysium.kinetiq.xyz/testnet-faucet",
|
|
391
401
|
],
|
|
392
402
|
safety: "Spending limits per transaction and per day (1 HYPE and 10 HYPE by default, set with CLOAK_MAX_HYPE_PER_TX and CLOAK_MAX_HYPE_PER_DAY), dry_run on every action, testnet by default. Never paste a key into a chat or into this hosted server.",
|
|
393
|
-
docs: "https://
|
|
403
|
+
docs: "https://cloak.boblabs.tech/agent.md",
|
|
394
404
|
})));
|
|
395
405
|
return server;
|
|
396
406
|
}
|
|
@@ -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", {
|
|
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
|
-
|
|
498
|
-
|
|
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
|
|
519
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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/dist/setup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setup(): Promise<void>;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// `npx -y @cloakely/mcp setup`: make the agent's own wallet (once) and say what's left to do, in words an agent can pass
|
|
2
|
+
// on to a person. Written for agents following https://cloak.boblabs.tech/agent.md. The key is never printed.
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createPublicClient, formatEther, http } from "viem";
|
|
5
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
6
|
+
import { elysiumTestnet } from "@cloakely/sdk";
|
|
7
|
+
import { defaultKeyFile } from "./config.js";
|
|
8
|
+
const FAUCET = "https://elysium.kinetiq.xyz/testnet-faucet";
|
|
9
|
+
export async function setup() {
|
|
10
|
+
const file = process.env.CLOAK_KEY_FILE || defaultKeyFile();
|
|
11
|
+
let made = false;
|
|
12
|
+
if (!existsSync(file)) {
|
|
13
|
+
// owner-only from the start; `wx` refuses to overwrite a file that appeared in between
|
|
14
|
+
writeFileSync(file, `${generatePrivateKey()}\n`, { mode: 0o600, flag: "wx" });
|
|
15
|
+
made = true;
|
|
16
|
+
}
|
|
17
|
+
let key = readFileSync(file, "utf8").trim();
|
|
18
|
+
if (!key.startsWith("0x"))
|
|
19
|
+
key = `0x${key}`;
|
|
20
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(key))
|
|
21
|
+
throw new Error(`${file} doesn't hold a private key. Move it aside and run setup again.`);
|
|
22
|
+
const address = privateKeyToAccount(key).address;
|
|
23
|
+
const pub = createPublicClient({ chain: elysiumTestnet, transport: http(undefined, { timeout: 15_000 }) });
|
|
24
|
+
const balance = await pub.getBalance({ address }).catch(() => undefined);
|
|
25
|
+
const funded = balance !== undefined && balance > 0n;
|
|
26
|
+
const out = {
|
|
27
|
+
status: made ? "Made a new wallet for the agent." : "The agent's wallet already exists. Kept it.",
|
|
28
|
+
agentWallet: address,
|
|
29
|
+
keyFile: `${file} (readable by this user only, never printed and never sent anywhere)`,
|
|
30
|
+
network: "Elysium testnet (play money only)",
|
|
31
|
+
balance: balance === undefined ? "couldn't read it right now" : `${formatEther(balance)} HYPE`,
|
|
32
|
+
next: [
|
|
33
|
+
"Add the Cloak MCP server to this app if it isn't there yet. Claude Code: claude mcp add cloak -- npx -y @cloakely/mcp. Any other app: an MCP server with command npx and args -y @cloakely/mcp. It uses this wallet without any settings.",
|
|
34
|
+
funded
|
|
35
|
+
? "The wallet has HYPE, so it's ready."
|
|
36
|
+
: `Ask the person to get free testnet HYPE for ${address} at ${FAUCET}. Copy the address for them exactly. Agents can't use the faucet themselves.`,
|
|
37
|
+
"Tell the person to restart the app or start a new chat so the Cloak tools load, then call cloak_info to check.",
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
console.log(JSON.stringify(out, null, 2));
|
|
41
|
+
}
|
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.4.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.
|
|
50
|
+
"@cloakely/sdk": "^0.4.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "22.10.2",
|