@blockula/mcp 0.0.0-stage โ†’ 0.1.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,25 @@
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
+ **Tools:** `status`, `start_dev_node`, `summon_chain`, `create_wallet`, `use_account`, `wallet_info`, `faucet`, `send`, `check_name`, `register_name`, `lookup`, `latest_blocks`.
10
+
11
+ **Spending a user's money safely:**
12
+ 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.
14
+ 3. `use_account("theirname.boo")`, then `send`.
15
+
16
+ | Env | Default | |
17
+ |---|---|---|
18
+ | `BLOCKULA_RPC` | `http://localhost:7070` | node URL |
19
+ | `BLOCKULA_HOME` | `~/.blockula` | where the agent key is stored (mode 600) |
20
+ | `BLOCKULA_MAX_SEND` | `100` | max FANG per `send` call |
21
+ | `BLOCKULA_BIN` | `blockula` | node binary used by `start_dev_node` / `summon_chain` |
22
+
23
+ `start_dev_node` and `summon_chain` need the `blockula` node binary on your PATH. All other tools work against any reachable node.
24
+
25
+ โš ๏ธ 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,155 @@
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 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)
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { Client, Wallet, parseFang } from "@blockula/sdk";
13
+ import { spawn, execFileSync } from "node:child_process";
14
+ import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { z } from "zod";
18
+ const RPC = process.env.BLOCKULA_RPC ?? "http://localhost:7070";
19
+ const HOME = process.env.BLOCKULA_HOME ?? join(homedir(), ".blockula");
20
+ const WALLET_FILE = join(HOME, "agent-wallet.json");
21
+ const MAX_SEND = parseFang(process.env.BLOCKULA_MAX_SEND ?? "100");
22
+ const BIN = process.env.BLOCKULA_BIN ?? "blockula";
23
+ const client = new Client(RPC);
24
+ const server = new McpServer({ name: "blockula", version: "0.1.0" });
25
+ const text = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });
26
+ function readWalletFile() {
27
+ return existsSync(WALLET_FILE) ? JSON.parse(readFileSync(WALLET_FILE, "utf8")) : null;
28
+ }
29
+ function loadWallet() {
30
+ const f = readWalletFile();
31
+ if (!f)
32
+ return null;
33
+ const w = Wallet.fromSeed(f.seed, client);
34
+ if (f.account)
35
+ w.account = f.account;
36
+ return w;
37
+ }
38
+ function requireWallet() {
39
+ const w = loadWallet();
40
+ if (!w)
41
+ throw new Error("No agent wallet yet โ€” call create_wallet first.");
42
+ return w;
43
+ }
44
+ async function nodeUp() {
45
+ try {
46
+ await client.status();
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ function tool(name, description, shape, run) {
54
+ server.registerTool(name, { description, inputSchema: shape }, (async (args) => {
55
+ try {
56
+ return text(await run(args));
57
+ }
58
+ catch (e) {
59
+ const msg = e.message;
60
+ const hint = /fetch failed|ECONNREFUSED/.test(msg) ? ` (no node at ${RPC} โ€” call start_dev_node)` : "";
61
+ return { ...text(`Error: ${msg}${hint}`), isError: true };
62
+ }
63
+ }));
64
+ }
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";
70
+ mkdirSync(HOME, { recursive: true });
71
+ const log = openSync(join(HOME, "dev-node.log"), "a");
72
+ const child = spawn(BIN, ["dev", "--port", port, ...(reset ? ["--reset"] : [])], { detached: true, stdio: ["ignore", log, log] });
73
+ child.on("error", () => { });
74
+ child.unref();
75
+ for (let i = 0; i < 40; i++) {
76
+ 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")}`;
79
+ }
80
+ throw new Error(`node didn't start โ€” is \`${BIN}\` installed? See ${join(HOME, "dev-node.log")}`);
81
+ });
82
+ 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
+ const out = execFileSync(BIN, ["summon", chain_id, ...(faucet ? [] : ["--no-faucet"])], { encoding: "utf8" });
84
+ return out.trim();
85
+ });
86
+ tool("create_wallet", "Create this agent's post-quantum (ML-DSA-65) wallet, stored locally. Returns the existing one if it already exists.", {}, async () => {
87
+ let w = loadWallet();
88
+ const existed = !!w;
89
+ if (!w) {
90
+ w = Wallet.generate(client);
91
+ mkdirSync(HOME, { recursive: true });
92
+ writeFileSync(WALLET_FILE, JSON.stringify({ address: w.address, key_id: w.keyId, seed: w.seedHex }, null, 2), { mode: 0o600 });
93
+ }
94
+ return {
95
+ key_id: w.keyId,
96
+ own_address: w.address,
97
+ acting_for: w.account,
98
+ existed,
99
+ stored_at: WALLET_FILE,
100
+ 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.)",
101
+ };
102
+ });
103
+ 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 }) => {
104
+ const f = readWalletFile();
105
+ if (!f)
106
+ throw new Error("No agent wallet yet โ€” call create_wallet first.");
107
+ const w = loadWallet();
108
+ if (account === "self") {
109
+ delete f.account;
110
+ }
111
+ else {
112
+ const addr = await client.resolve(account);
113
+ const { accounts } = await client.keyAccounts(w.keyId);
114
+ const grant = accounts.find((a) => a.account.address === addr);
115
+ if (!grant)
116
+ throw new Error(`${account} hasn't authorized this key yet. Ask the user to add key_id ${w.keyId} in their wallet (Devices & agents).`);
117
+ f.account = addr;
118
+ writeFileSync(WALLET_FILE, JSON.stringify(f, null, 2), { mode: 0o600 });
119
+ return { acting_for: grant.account.name ?? addr, role: grant.role, label: grant.label };
120
+ }
121
+ writeFileSync(WALLET_FILE, JSON.stringify(f, null, 2), { mode: 0o600 });
122
+ return { acting_for: w.address };
123
+ });
124
+ tool("wallet_info", "This agent's address, balance, .boo names and recent activity.", {}, async () => {
125
+ const w = requireWallet();
126
+ const a = await client.account(w.account).catch(() => null);
127
+ const mine = a?.keys?.find((k) => k.key_id === w.keyId);
128
+ 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)}โ€ฆ)`) };
129
+ });
130
+ 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
+ const target = to ?? requireWallet().account;
132
+ const tx = await client.faucet(target);
133
+ return tx.summary;
134
+ });
135
+ 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)
137
+ throw new Error(`amount exceeds this agent's per-send limit (BLOCKULA_MAX_SEND=${process.env.BLOCKULA_MAX_SEND ?? "100"} FANG)`);
138
+ const tx = await requireWallet().send(to, amount, { memo });
139
+ return { summary: tx.summary, fee: tx.fee_display, tx_id: tx.id, block: tx.height, explorer: `${RPC}/#/tx/${tx.id}` };
140
+ });
141
+ tool("check_name", "Check whether a .boo name is available and what it costs.", { name: z.string() }, ({ name }) => client.name(name));
142
+ 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 }) => {
143
+ const tx = await requireWallet().registerName(name);
144
+ return { summary: tx.summary, tx_id: tx.id, explorer: `${RPC}/#/tx/${tx.id}` };
145
+ });
146
+ 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 }) => {
147
+ const hit = await client.search(query);
148
+ 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);
149
+ return { type: hit.type, explorer: `${RPC}/#/${hit.type}/${hit.id}`, ...data };
150
+ });
151
+ tool("latest_blocks", "The most recent blocks, summarized.", { limit: z.number().int().min(1).max(50).default(10) }, async ({ limit }) => {
152
+ const blocks = await client.blocks(limit);
153
+ return blocks.map((b) => `${b.summary} ยท ${b.hash.slice(0, 12)}โ€ฆ`);
154
+ });
155
+ 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.1.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.1.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
+ }