@blockula/mcp 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/dist/index.js +82 -29
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -6,20 +6,23 @@
6
6
  claude mcp add blockula -- npx -y @blockula/mcp
7
7
  ```
8
8
 
9
- **Tools:** `status`, `start_dev_node`, `summon_chain`, `create_wallet`, `use_account`, `wallet_info`, `faucet`, `send`, `check_name`, `register_name`, `lookup`, `latest_blocks`.
9
+ It works with zero setup: if no local node is running, it connects to the public testnet at `https://blockula-testnet.fly.dev`. Call `status` to see which node you're on.
10
+
11
+ **Tools:** `status`, `tx_status`, `start_dev_node`, `summon_chain`, `create_wallet`, `use_account`, `wallet_info`, `faucet`, `send`, `check_name`, `register_name`, `lookup`, `latest_blocks`.
10
12
 
11
13
  **Spending a user's money safely:**
12
14
  1. `create_wallet` gives you a `key_id`. Hand it to the user.
13
- 2. The user grants it a budget in the Blockula web wallet (Devices & agents → Give an agent a budget). The chain enforces the limit and expiry.
15
+ 2. The user grants it a budget in the Blockula web wallet at https://blockula-wallet.vercel.app (Devices & agents → Give an agent a budget). The chain enforces the limit and expiry.
14
16
  3. `use_account("theirname.boo")`, then `send`.
15
17
 
16
18
  | Env | Default | |
17
19
  |---|---|---|
18
- | `BLOCKULA_RPC` | `http://localhost:7070` | node URL |
20
+ | `BLOCKULA_RPC` | local node if running, else the public testnet | node URL |
19
21
  | `BLOCKULA_HOME` | `~/.blockula` | where the agent key is stored (mode 600) |
20
22
  | `BLOCKULA_MAX_SEND` | `100` | max FANG per `send` call |
23
+ | `BLOCKULA_MAX_DAILY` | `100` | max FANG sent per UTC day across all calls (persisted in `BLOCKULA_HOME`) |
21
24
  | `BLOCKULA_BIN` | `blockula` | node binary used by `start_dev_node` / `summon_chain` |
22
25
 
23
- `start_dev_node` and `summon_chain` need the `blockula` node binary on your PATH. All other tools work against any reachable node.
26
+ `start_dev_node` and `summon_chain` need the `blockula` node binary (build it from the Blockula repo with `cargo build --release`). Every other tool works against the public testnet without it.
24
27
 
25
28
  ⚠️ Early and unaudited: test chains only.
package/dist/index.js CHANGED
@@ -3,25 +3,59 @@
3
3
  // claude mcp add blockula -- npx -y @blockula/mcp
4
4
  //
5
5
  // Env:
6
- // BLOCKULA_RPC node URL (default http://localhost:7070)
7
- // BLOCKULA_HOME where the agent wallet lives (default ~/.blockula)
8
- // BLOCKULA_MAX_SEND max FANG per send() call (default 100) — a guardrail until on-chain session keys land
9
- // BLOCKULA_BIN path to the `blockula` binary (default: found on PATH)
6
+ // BLOCKULA_RPC node URL (default: a local node on :7070 if one is running, else the public testnet)
7
+ // BLOCKULA_HOME where the agent wallet lives (default ~/.blockula)
8
+ // BLOCKULA_MAX_SEND max FANG per send() call (default 100)
9
+ // BLOCKULA_MAX_DAILY max FANG this MCP sends per UTC day, across calls (default 100)
10
+ // BLOCKULA_BIN path to the `blockula` binary (default: found on PATH)
11
+ // When acting for a user's account via a session key, the chain also enforces that key's limit.
10
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { Client, Wallet, parseFang } from "@blockula/sdk";
14
+ import { Client, Wallet, formatFang, parseFang } from "@blockula/sdk";
13
15
  import { spawn, execFileSync } from "node:child_process";
14
16
  import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
15
17
  import { homedir } from "node:os";
16
18
  import { join } from "node:path";
17
19
  import { z } from "zod";
18
- const RPC = process.env.BLOCKULA_RPC ?? "http://localhost:7070";
20
+ const LOCAL_RPC = "http://localhost:7070";
21
+ const TESTNET_RPC = "https://blockula-testnet.fly.dev";
19
22
  const HOME = process.env.BLOCKULA_HOME ?? join(homedir(), ".blockula");
20
23
  const WALLET_FILE = join(HOME, "agent-wallet.json");
21
24
  const MAX_SEND = parseFang(process.env.BLOCKULA_MAX_SEND ?? "100");
25
+ const MAX_DAILY = parseFang(process.env.BLOCKULA_MAX_DAILY ?? "100");
26
+ const SPEND_FILE = join(HOME, "agent-spend.json");
27
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
22
28
  const BIN = process.env.BLOCKULA_BIN ?? "blockula";
23
- const client = new Client(RPC);
24
- const server = new McpServer({ name: "blockula", version: "0.1.0" });
29
+ async function reachable(url) {
30
+ try {
31
+ await new Client(url).status();
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ // No configuration needed: use a local node if one is running, otherwise the public testnet.
39
+ let RPC = process.env.BLOCKULA_RPC ?? ((await reachable(LOCAL_RPC)) ? LOCAL_RPC : TESTNET_RPC);
40
+ let client = new Client(RPC);
41
+ function useRpc(url) {
42
+ RPC = url;
43
+ client = new Client(url);
44
+ }
45
+ /** Running total of FANG sent today (UTC), persisted so the cap holds across calls and restarts. */
46
+ function spentToday() {
47
+ const day = new Date().toISOString().slice(0, 10);
48
+ try {
49
+ const f = JSON.parse(readFileSync(SPEND_FILE, "utf8"));
50
+ if (f.day === day)
51
+ return { day, spent: BigInt(f.spent) };
52
+ }
53
+ catch {
54
+ /* no spend yet */
55
+ }
56
+ return { day, spent: 0n };
57
+ }
58
+ const server = new McpServer({ name: "blockula", version: VERSION });
25
59
  const text = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });
26
60
  function readWalletFile() {
27
61
  return existsSync(WALLET_FILE) ? JSON.parse(readFileSync(WALLET_FILE, "utf8")) : null;
@@ -41,15 +75,6 @@ function requireWallet() {
41
75
  throw new Error("No agent wallet yet — call create_wallet first.");
42
76
  return w;
43
77
  }
44
- async function nodeUp() {
45
- try {
46
- await client.status();
47
- return true;
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
78
  function tool(name, description, shape, run) {
54
79
  server.registerTool(name, { description, inputSchema: shape }, (async (args) => {
55
80
  try {
@@ -62,22 +87,43 @@ function tool(name, description, shape, run) {
62
87
  }
63
88
  }));
64
89
  }
65
- tool("status", "Blockula network status: chain id, height, supply, fees burned, and the crypto in use.", {}, () => client.status());
66
- tool("start_dev_node", "Start a local Blockula dev chain (with faucet + built-in explorer) if one isn't already running. Requires the `blockula` binary.", { reset: z.boolean().optional().describe("wipe the dev chain first") }, async ({ reset }) => {
67
- if (!reset && (await nodeUp()))
68
- return `Already running at ${RPC} (explorer: ${RPC})`;
69
- const port = new URL(RPC).port || "7070";
90
+ tool("status", "Blockula network status: which node you're connected to, chain id, height, supply, fees burned, and the crypto in use.", {}, async () => ({
91
+ rpc: RPC,
92
+ network: RPC === TESTNET_RPC ? "public testnet" : RPC === LOCAL_RPC ? "local node" : "custom node",
93
+ explorer: RPC,
94
+ mcp_version: VERSION,
95
+ ...(await client.status()),
96
+ }));
97
+ tool("tx_status", "Check a transaction by id: pending, finalized (with block), and its plain-English summary.", { tx_id: z.string().regex(/^[0-9a-f]{64}$/) }, async ({ tx_id }) => {
98
+ const tx = await client.tx(tx_id);
99
+ return { status: tx.status, summary: tx.summary, block: tx.height, fee: tx.fee_display, explorer: `${RPC}/#/tx/${tx_id}` };
100
+ });
101
+ tool("start_dev_node", "Start a local Blockula dev chain (faucet + built-in explorer) and switch to it. Needs the `blockula` binary; without it, keep using the public testnet, which supports everything except running your own chain.", { reset: z.boolean().optional().describe("wipe the dev chain first") }, async ({ reset }) => {
102
+ const target = process.env.BLOCKULA_RPC ?? LOCAL_RPC;
103
+ if (!reset && (await reachable(target))) {
104
+ useRpc(target);
105
+ return `Already running at ${target} (explorer: ${target})`;
106
+ }
107
+ const port = new URL(target).port || "7070";
70
108
  mkdirSync(HOME, { recursive: true });
71
109
  const log = openSync(join(HOME, "dev-node.log"), "a");
110
+ let missing = false;
72
111
  const child = spawn(BIN, ["dev", "--port", port, ...(reset ? ["--reset"] : [])], { detached: true, stdio: ["ignore", log, log] });
73
- child.on("error", () => { });
112
+ child.on("error", (e) => {
113
+ missing = e.code === "ENOENT";
114
+ });
74
115
  child.unref();
75
- for (let i = 0; i < 40; i++) {
116
+ for (let i = 0; i < 40 && !missing; i++) {
76
117
  await new Promise((r) => setTimeout(r, 250));
77
- if (await nodeUp())
78
- return `🧛 Dev chain is up. Explorer: ${RPC} · logs: ${join(HOME, "dev-node.log")}`;
118
+ if (await reachable(target)) {
119
+ useRpc(target);
120
+ return `🧛 Dev chain is up. Explorer: ${target} · logs: ${join(HOME, "dev-node.log")}`;
121
+ }
122
+ }
123
+ if (missing) {
124
+ throw new Error(`The \`${BIN}\` node binary isn't installed, so a local chain can't start. You're connected to ${RPC}${RPC === TESTNET_RPC ? " (the public testnet)" : ""}, which works for wallets, the faucet, names and sends. To run your own chain, build the node from the Blockula repo (cargo build --release) and set BLOCKULA_BIN.`);
79
125
  }
80
- throw new Error(`node didn't start — is \`${BIN}\` installed? See ${join(HOME, "dev-node.log")}`);
126
+ throw new Error(`node didn't start. See ${join(HOME, "dev-node.log")}`);
81
127
  });
82
128
  tool("summon_chain", "Create a brand-new Blockula chain (fresh genesis + validator key) that you can run as your own network.", { chain_id: z.string().regex(/^[a-z0-9-]{3,40}$/), faucet: z.boolean().default(true) }, async ({ chain_id, faucet }) => {
83
129
  const out = execFileSync(BIN, ["summon", chain_id, ...(faucet ? [] : ["--no-faucet"])], { encoding: "utf8" });
@@ -130,12 +176,19 @@ tool("wallet_info", "This agent's address, balance, .boo names and recent activi
130
176
  tool("faucet", "Get 100 test FANG on a dev/test chain (to this agent's wallet, or any name/address).", { to: z.string().optional() }, async ({ to }) => {
131
177
  const target = to ?? requireWallet().account;
132
178
  const tx = await client.faucet(target);
133
- return tx.summary;
179
+ return { summary: tx.summary, tx_id: tx.id, explorer: `${RPC}/#/tx/${tx.id}` };
134
180
  });
135
181
  tool("send", "Send FANG to a .boo name or boo1… address. Amounts are in FANG, e.g. \"1.5\".", { to: z.string(), amount: z.string(), memo: z.string().max(256).optional() }, async ({ to, amount, memo }) => {
136
- if (parseFang(amount) > MAX_SEND)
182
+ const drops = parseFang(amount);
183
+ if (drops > MAX_SEND)
137
184
  throw new Error(`amount exceeds this agent's per-send limit (BLOCKULA_MAX_SEND=${process.env.BLOCKULA_MAX_SEND ?? "100"} FANG)`);
185
+ const today = spentToday();
186
+ if (today.spent + drops > MAX_DAILY) {
187
+ throw new Error(`this would exceed the agent's daily limit (BLOCKULA_MAX_DAILY=${process.env.BLOCKULA_MAX_DAILY ?? "100"} FANG; ${formatFang(today.spent)} already sent today)`);
188
+ }
138
189
  const tx = await requireWallet().send(to, amount, { memo });
190
+ mkdirSync(HOME, { recursive: true });
191
+ writeFileSync(SPEND_FILE, JSON.stringify({ day: today.day, spent: (today.spent + drops + BigInt(tx.fee)).toString() }), { mode: 0o600 });
139
192
  return { summary: tx.summary, fee: tx.fee_display, tx_id: tx.id, block: tx.height, explorer: `${RPC}/#/tx/${tx.id}` };
140
193
  });
141
194
  tool("check_name", "Check whether a .boo name is available and what it costs.", { name: z.string() }, ({ name }) => client.name(name));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockula/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "\ud83e\udddb MCP server for Blockula \u2014 lets any coding agent run a node, hold a post-quantum wallet, send FANG and claim .boo names",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@blockula/sdk": "^0.1.0",
20
+ "@blockula/sdk": "^0.2.0",
21
21
  "@modelcontextprotocol/sdk": "^1.32.1",
22
22
  "zod": "^4.0.0"
23
23
  },