@blockula/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/README.md +1 -1
- package/dist/index.js +65 -5
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ claude mcp add blockula -- npx -y @blockula/mcp
|
|
|
8
8
|
|
|
9
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
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`.
|
|
11
|
+
**Tools:** `status`, `tx_status`, `install_node`, `list_hunts`, `link_github`, `fund_hunt`, `start_dev_node`, `summon_chain`, `create_wallet`, `use_account`, `wallet_info`, `faucet`, `send`, `check_name`, `register_name`, `lookup`, `latest_blocks`.
|
|
12
12
|
|
|
13
13
|
**Spending a user's money safely:**
|
|
14
14
|
1. `create_wallet` gives you a `key_id`. Hand it to the user.
|
package/dist/index.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
// BLOCKULA_HOME where the agent wallet lives (default ~/.blockula)
|
|
8
8
|
// BLOCKULA_MAX_SEND max FANG per send() call (default 100)
|
|
9
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:
|
|
10
|
+
// BLOCKULA_BIN path to the `blockula` binary (default: ~/.blockula/bin/blockula from install_node, else PATH)
|
|
11
11
|
// When acting for a user's account via a session key, the chain also enforces that key's limit.
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
14
|
import { Client, Wallet, formatFang, parseFang } from "@blockula/sdk";
|
|
15
15
|
import { spawn, execFileSync } from "node:child_process";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { homedir, tmpdir } from "node:os";
|
|
18
19
|
import { join } from "node:path";
|
|
19
20
|
import { z } from "zod";
|
|
20
21
|
const LOCAL_RPC = "http://localhost:7070";
|
|
@@ -25,7 +26,19 @@ const MAX_SEND = parseFang(process.env.BLOCKULA_MAX_SEND ?? "100");
|
|
|
25
26
|
const MAX_DAILY = parseFang(process.env.BLOCKULA_MAX_DAILY ?? "100");
|
|
26
27
|
const SPEND_FILE = join(HOME, "agent-spend.json");
|
|
27
28
|
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
28
|
-
const
|
|
29
|
+
const REPO = "jackdishman/blockula";
|
|
30
|
+
const INSTALLED_BIN = join(HOME, "bin", "blockula");
|
|
31
|
+
let BIN = process.env.BLOCKULA_BIN ?? (existsSync(INSTALLED_BIN) ? INSTALLED_BIN : "blockula");
|
|
32
|
+
function releaseTarget() {
|
|
33
|
+
const key = `${process.platform}-${process.arch}`;
|
|
34
|
+
return { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu" }[key] ?? null;
|
|
35
|
+
}
|
|
36
|
+
async function download(url) {
|
|
37
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
throw new Error(`download failed (${res.status}): ${url}`);
|
|
40
|
+
return Buffer.from(await res.arrayBuffer());
|
|
41
|
+
}
|
|
29
42
|
async function reachable(url) {
|
|
30
43
|
try {
|
|
31
44
|
await new Client(url).status();
|
|
@@ -121,10 +134,36 @@ tool("start_dev_node", "Start a local Blockula dev chain (faucet + built-in expl
|
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
136
|
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.
|
|
137
|
+
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. Call install_node to download the prebuilt node (checksum-verified), then start_dev_node again.`);
|
|
125
138
|
}
|
|
126
139
|
throw new Error(`node didn't start. See ${join(HOME, "dev-node.log")}`);
|
|
127
140
|
});
|
|
141
|
+
tool("install_node", "Download the prebuilt `blockula` node + CLI for this machine from the official GitHub release, verify its SHA-256 checksum, and install it to ~/.blockula/bin. Needed for start_dev_node and summon_chain; nothing else needs it.", { version: z.string().regex(/^(latest|v\d+\.\d+\.\d+)$/).default("latest") }, async ({ version }) => {
|
|
142
|
+
const target = releaseTarget();
|
|
143
|
+
if (!target)
|
|
144
|
+
throw new Error(`No prebuilt node for ${process.platform}/${process.arch}. Build from source: https://github.com/${REPO}`);
|
|
145
|
+
const base = version === "latest" ? `https://github.com/${REPO}/releases/latest/download` : `https://github.com/${REPO}/releases/download/${version}`;
|
|
146
|
+
const asset = `blockula-${target}.tar.gz`;
|
|
147
|
+
const [tarball, sums] = await Promise.all([download(`${base}/${asset}`), download(`${base}/checksums.txt`)]);
|
|
148
|
+
const expected = sums.toString("utf8").split("\n").map((l) => l.trim().split(/\s+/)).find(([, f]) => f === asset)?.[0];
|
|
149
|
+
const actual = createHash("sha256").update(tarball).digest("hex");
|
|
150
|
+
if (!expected || expected !== actual)
|
|
151
|
+
throw new Error(`checksum mismatch for ${asset}; not installing`);
|
|
152
|
+
const tmp = mkdtempSync(join(tmpdir(), "blockula-"));
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(join(tmp, asset), tarball);
|
|
155
|
+
execFileSync("tar", ["-xzf", join(tmp, asset), "-C", tmp]);
|
|
156
|
+
mkdirSync(join(HOME, "bin"), { recursive: true });
|
|
157
|
+
copyFileSync(join(tmp, `blockula-${target}`, "blockula"), INSTALLED_BIN);
|
|
158
|
+
chmodSync(INSTALLED_BIN, 0o755);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
BIN = INSTALLED_BIN;
|
|
164
|
+
const installed = execFileSync(INSTALLED_BIN, ["--version"], { encoding: "utf8" }).trim();
|
|
165
|
+
return { installed, path: INSTALLED_BIN, sha256: actual, next: "start_dev_node runs a local chain; summon_chain creates your own network" };
|
|
166
|
+
});
|
|
128
167
|
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
168
|
const out = execFileSync(BIN, ["summon", chain_id, ...(faucet ? [] : ["--no-faucet"])], { encoding: "utf8" });
|
|
130
169
|
return out.trim();
|
|
@@ -205,4 +244,25 @@ tool("latest_blocks", "The most recent blocks, summarized.", { limit: z.number()
|
|
|
205
244
|
const blocks = await client.blocks(limit);
|
|
206
245
|
return blocks.map((b) => `${b.summary} · ${b.hash.slice(0, 12)}…`);
|
|
207
246
|
});
|
|
247
|
+
tool("list_hunts", "Open bounties (Hunts) on GitHub issues, with FANG escrowed on-chain. Each links to an issue with acceptance criteria: implement it, open a PR, and the bounty is awarded on-chain when it merges.", { status: z.enum(["open", "awarded", "cancelled", "expired"]).default("open") }, async ({ status }) => {
|
|
248
|
+
const res = await client.hunts(status);
|
|
249
|
+
const hunts = (Array.isArray(res) ? res : res.hunts ?? []);
|
|
250
|
+
return hunts.map((h) => ({ hunt_id: h.id, bounty: h.amount_display, title: h.title, issue: h.issue_url, funders: h.funders?.length ?? 0, explorer: `${RPC}/#/hunt/${h.id}` }));
|
|
251
|
+
});
|
|
252
|
+
tool("link_github", "Link a GitHub handle to the account this agent acts for, so Hunt awards show up on its profile. First create a PUBLIC gist (as that GitHub user) containing the account's boo1 address, then pass the gist id here.", { handle: z.string().regex(/^[A-Za-z0-9-]{1,39}$/), gist_id: z.string().regex(/^[0-9a-f]{20,40}$/) }, async ({ handle, gist_id }) => {
|
|
253
|
+
const tx = await requireWallet().linkGithub(handle, gist_id);
|
|
254
|
+
const check = await client.github(handle).catch(() => null);
|
|
255
|
+
return { summary: tx.summary, verified: check?.verified ?? false, explorer: `${RPC}/#/tx/${tx.id}` };
|
|
256
|
+
});
|
|
257
|
+
tool("fund_hunt", "Add FANG to an open Hunt's bounty (crowdfunding). Refunded to you if the hunt is cancelled.", { hunt_id: z.number().int().min(0), amount: z.string() }, async ({ hunt_id, amount }) => {
|
|
258
|
+
const drops = parseFang(amount);
|
|
259
|
+
if (drops > MAX_SEND)
|
|
260
|
+
throw new Error(`amount exceeds this agent's per-send limit (BLOCKULA_MAX_SEND=${process.env.BLOCKULA_MAX_SEND ?? "100"} FANG)`);
|
|
261
|
+
const today = spentToday();
|
|
262
|
+
if (today.spent + drops > MAX_DAILY)
|
|
263
|
+
throw new Error(`this would exceed the agent's daily limit (BLOCKULA_MAX_DAILY=${process.env.BLOCKULA_MAX_DAILY ?? "100"} FANG)`);
|
|
264
|
+
const tx = await requireWallet().fundHunt(hunt_id, amount);
|
|
265
|
+
writeFileSync(SPEND_FILE, JSON.stringify({ day: today.day, spent: (today.spent + drops + BigInt(tx.fee)).toString() }), { mode: 0o600 });
|
|
266
|
+
return { summary: tx.summary, explorer: `${RPC}/#/hunt/${hunt_id}` };
|
|
267
|
+
});
|
|
208
268
|
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockula/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.
|
|
20
|
+
"@blockula/sdk": "^0.3.0",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
22
22
|
"zod": "^4.0.0"
|
|
23
23
|
},
|
|
@@ -39,5 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/jackdishman/blockula.git",
|
|
46
|
+
"directory": "mcp"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/jackdishman/blockula#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/jackdishman/blockula/issues"
|
|
42
51
|
}
|
|
43
52
|
}
|