@blockula/mcp 0.0.0-stage โ†’ 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jack Dishman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # @blockula/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ๐Ÿง› MCP server that lets any coding agent use **Blockula**, a post-quantum blockchain. Agents can hold a wallet, send FANG, claim `.boo` names, look things up in plain English, and start or summon chains.
4
+
5
+ ```bash
6
+ claude mcp add blockula -- npx -y @blockula/mcp
7
+ ```
8
+
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`.
12
+
13
+ **Spending a user's money safely:**
14
+ 1. `create_wallet` gives you a `key_id`. Hand it to the user.
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.
16
+ 3. `use_account("theirname.boo")`, then `send`.
17
+
18
+ | Env | Default | |
19
+ |---|---|---|
20
+ | `BLOCKULA_RPC` | local node if running, else the public testnet | node URL |
21
+ | `BLOCKULA_HOME` | `~/.blockula` | where the agent key is stored (mode 600) |
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`) |
24
+ | `BLOCKULA_BIN` | `blockula` | node binary used by `start_dev_node` / `summon_chain` |
25
+
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.
27
+
28
+ โš ๏ธ Early and unaudited: test chains only.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ // ๐Ÿง› Blockula MCP server. Add to Claude Code with:
3
+ // claude mcp add blockula -- npx -y @blockula/mcp
4
+ //
5
+ // Env:
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.
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { Client, Wallet, formatFang, parseFang } from "@blockula/sdk";
15
+ import { spawn, execFileSync } from "node:child_process";
16
+ import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { z } from "zod";
20
+ const LOCAL_RPC = "http://localhost:7070";
21
+ const TESTNET_RPC = "https://blockula-testnet.fly.dev";
22
+ const HOME = process.env.BLOCKULA_HOME ?? join(homedir(), ".blockula");
23
+ const WALLET_FILE = join(HOME, "agent-wallet.json");
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;
28
+ const BIN = process.env.BLOCKULA_BIN ?? "blockula";
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 });
59
+ const text = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });
60
+ function readWalletFile() {
61
+ return existsSync(WALLET_FILE) ? JSON.parse(readFileSync(WALLET_FILE, "utf8")) : null;
62
+ }
63
+ function loadWallet() {
64
+ const f = readWalletFile();
65
+ if (!f)
66
+ return null;
67
+ const w = Wallet.fromSeed(f.seed, client);
68
+ if (f.account)
69
+ w.account = f.account;
70
+ return w;
71
+ }
72
+ function requireWallet() {
73
+ const w = loadWallet();
74
+ if (!w)
75
+ throw new Error("No agent wallet yet โ€” call create_wallet first.");
76
+ return w;
77
+ }
78
+ function tool(name, description, shape, run) {
79
+ server.registerTool(name, { description, inputSchema: shape }, (async (args) => {
80
+ try {
81
+ return text(await run(args));
82
+ }
83
+ catch (e) {
84
+ const msg = e.message;
85
+ const hint = /fetch failed|ECONNREFUSED/.test(msg) ? ` (no node at ${RPC} โ€” call start_dev_node)` : "";
86
+ return { ...text(`Error: ${msg}${hint}`), isError: true };
87
+ }
88
+ }));
89
+ }
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";
108
+ mkdirSync(HOME, { recursive: true });
109
+ const log = openSync(join(HOME, "dev-node.log"), "a");
110
+ let missing = false;
111
+ const child = spawn(BIN, ["dev", "--port", port, ...(reset ? ["--reset"] : [])], { detached: true, stdio: ["ignore", log, log] });
112
+ child.on("error", (e) => {
113
+ missing = e.code === "ENOENT";
114
+ });
115
+ child.unref();
116
+ for (let i = 0; i < 40 && !missing; i++) {
117
+ await new Promise((r) => setTimeout(r, 250));
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.`);
125
+ }
126
+ throw new Error(`node didn't start. See ${join(HOME, "dev-node.log")}`);
127
+ });
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 }) => {
129
+ const out = execFileSync(BIN, ["summon", chain_id, ...(faucet ? [] : ["--no-faucet"])], { encoding: "utf8" });
130
+ return out.trim();
131
+ });
132
+ tool("create_wallet", "Create this agent's post-quantum (ML-DSA-65) wallet, stored locally. Returns the existing one if it already exists.", {}, async () => {
133
+ let w = loadWallet();
134
+ const existed = !!w;
135
+ if (!w) {
136
+ w = Wallet.generate(client);
137
+ mkdirSync(HOME, { recursive: true });
138
+ writeFileSync(WALLET_FILE, JSON.stringify({ address: w.address, key_id: w.keyId, seed: w.seedHex }, null, 2), { mode: 0o600 });
139
+ }
140
+ return {
141
+ key_id: w.keyId,
142
+ own_address: w.address,
143
+ acting_for: w.account,
144
+ existed,
145
+ stored_at: WALLET_FILE,
146
+ next_steps: "To spend from the user's account: ask them to open the Blockula web wallet โ†’ Devices & agents โ†’ 'Give an agent a budget' and paste this key_id. Then call use_account with their .boo name. (Or use faucet to fund this key's own address for testing.)",
147
+ };
148
+ });
149
+ tool("use_account", "Act for a user's account (name or address) that has authorized this agent's key โ€” usually as a session key with a spending limit. Pass 'self' to go back to the key's own address.", { account: z.string() }, async ({ account }) => {
150
+ const f = readWalletFile();
151
+ if (!f)
152
+ throw new Error("No agent wallet yet โ€” call create_wallet first.");
153
+ const w = loadWallet();
154
+ if (account === "self") {
155
+ delete f.account;
156
+ }
157
+ else {
158
+ const addr = await client.resolve(account);
159
+ const { accounts } = await client.keyAccounts(w.keyId);
160
+ const grant = accounts.find((a) => a.account.address === addr);
161
+ if (!grant)
162
+ throw new Error(`${account} hasn't authorized this key yet. Ask the user to add key_id ${w.keyId} in their wallet (Devices & agents).`);
163
+ f.account = addr;
164
+ writeFileSync(WALLET_FILE, JSON.stringify(f, null, 2), { mode: 0o600 });
165
+ return { acting_for: grant.account.name ?? addr, role: grant.role, label: grant.label };
166
+ }
167
+ writeFileSync(WALLET_FILE, JSON.stringify(f, null, 2), { mode: 0o600 });
168
+ return { acting_for: w.address };
169
+ });
170
+ tool("wallet_info", "This agent's address, balance, .boo names and recent activity.", {}, async () => {
171
+ const w = requireWallet();
172
+ const a = await client.account(w.account).catch(() => null);
173
+ const mine = a?.keys?.find((k) => k.key_id === w.keyId);
174
+ return { key_id: w.keyId, acting_for: a?.account?.name ?? w.account, my_permissions: mine ?? "not authorized on this account", address: w.account, balance: a?.balance_display ?? "0 FANG", names: a?.names ?? [], recent: (a?.recent_txs ?? []).slice(0, 10).map((t) => `${t.summary} (${t.id.slice(0, 12)}โ€ฆ)`) };
175
+ });
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 }) => {
177
+ const target = to ?? requireWallet().account;
178
+ const tx = await client.faucet(target);
179
+ return { summary: tx.summary, tx_id: tx.id, explorer: `${RPC}/#/tx/${tx.id}` };
180
+ });
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 }) => {
182
+ const drops = parseFang(amount);
183
+ if (drops > MAX_SEND)
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
+ }
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 });
192
+ return { summary: tx.summary, fee: tx.fee_display, tx_id: tx.id, block: tx.height, explorer: `${RPC}/#/tx/${tx.id}` };
193
+ });
194
+ tool("check_name", "Check whether a .boo name is available and what it costs.", { name: z.string() }, ({ name }) => client.name(name));
195
+ tool("register_name", "Claim a .boo name for this agent's wallet (3โ€“32 chars, a-z 0-9 -). Shorter names cost more.", { name: z.string() }, async ({ name }) => {
196
+ const tx = await requireWallet().registerName(name);
197
+ return { summary: tx.summary, tx_id: tx.id, explorer: `${RPC}/#/tx/${tx.id}` };
198
+ });
199
+ tool("lookup", "Look up anything โ€” a .boo name, boo1โ€ฆ address, block number/hash or tx id โ€” and get a plain-English explanation.", { query: z.string() }, async ({ query }) => {
200
+ const hit = await client.search(query);
201
+ const data = hit.type === "block" ? await client.block(hit.id) : hit.type === "tx" ? await client.tx(hit.id) : hit.type === "account" ? await client.account(hit.id) : await client.name(hit.id);
202
+ return { type: hit.type, explorer: `${RPC}/#/${hit.type}/${hit.id}`, ...data };
203
+ });
204
+ tool("latest_blocks", "The most recent blocks, summarized.", { limit: z.number().int().min(1).max(50).default(10) }, async ({ limit }) => {
205
+ const blocks = await client.blocks(limit);
206
+ return blocks.map((b) => `${b.summary} ยท ${b.hash.slice(0, 12)}โ€ฆ`);
207
+ });
208
+ await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@blockula/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "blockula-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "@blockula/sdk": "^0.2.0",
21
+ "@modelcontextprotocol/sdk": "^1.32.1",
22
+ "zod": "^4.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "*",
26
+ "typescript": "*"
27
+ },
28
+ "author": "Jack Dishman",
29
+ "keywords": [
30
+ "blockula",
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "post-quantum",
34
+ "blockchain",
35
+ "agents"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }