@mrfentmen/btc-address-mcp 0.0.0-stage → 1.0.1
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 +32 -2
- package/dist/api.d.ts +14 -0
- package/dist/api.js +210 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +43 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Bitcoin Addresses
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Validate Bitcoin addresses offline with real base58check and bech32/bech32m checksums, and decode BIP-32 derivation paths, keyless.
|
|
4
|
+
|
|
5
|
+
Keyless — no signup, no API key, no environment variables.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @mrfentmen/btc-address-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your MCP client:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"btc-address-mcp": { "command": "npx", "args": ["-y", "@mrfentmen/btc-address-mcp"] }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `validate_btc_address` | Check an address's checksum and shape: P2PKH, P2SH, P2WPKH, P2WSH or P2TR, with network and hash. |
|
|
28
|
+
| `derivation_path_info` | Explain a BIP-32 derivation path level by level, including the purpose presets and hardened flags. |
|
|
29
|
+
| `list_address_types` | The address types this checker recognises, with their prefixes and version bytes. |
|
|
30
|
+
|
|
31
|
+
## Data source
|
|
32
|
+
|
|
33
|
+
Runs fully offline — all work happens inside the server with no API calls and no keys.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitcoin Addresses — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
export declare class SourceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export declare function clean(s: unknown): string;
|
|
7
|
+
export declare function bullet(label: string, value: unknown): string;
|
|
8
|
+
export declare function validateBtcAddress(a: {
|
|
9
|
+
address: string;
|
|
10
|
+
}): Promise<string>;
|
|
11
|
+
export declare function derivationPathInfo(a: {
|
|
12
|
+
path: string;
|
|
13
|
+
}): Promise<string>;
|
|
14
|
+
export declare function listAddressTypes(): Promise<string>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitcoin Addresses — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
export class SourceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export function clean(s) {
|
|
7
|
+
return String(s ?? "").replace(/\s+/g, " ").trim();
|
|
8
|
+
}
|
|
9
|
+
export function bullet(label, value) {
|
|
10
|
+
const v = value === undefined || value === null || value === "" ? "" : String(value);
|
|
11
|
+
return v ? `${label}: ${v}` : "";
|
|
12
|
+
}
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
15
|
+
const BECH32_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
16
|
+
function sha256(data) {
|
|
17
|
+
return createHash("sha256").update(data).digest();
|
|
18
|
+
}
|
|
19
|
+
function base58Decode(text) {
|
|
20
|
+
let num = 0n;
|
|
21
|
+
for (const ch of text) {
|
|
22
|
+
const index = B58.indexOf(ch);
|
|
23
|
+
if (index < 0)
|
|
24
|
+
throw new SourceError(`"${text}" is not valid base58`);
|
|
25
|
+
num = num * 58n + BigInt(index);
|
|
26
|
+
}
|
|
27
|
+
const bytes = [];
|
|
28
|
+
while (num > 0n) {
|
|
29
|
+
bytes.unshift(Number(num & 0xffn));
|
|
30
|
+
num >>= 8n;
|
|
31
|
+
}
|
|
32
|
+
for (const ch of text) {
|
|
33
|
+
if (ch !== "1")
|
|
34
|
+
break;
|
|
35
|
+
bytes.unshift(0);
|
|
36
|
+
}
|
|
37
|
+
return Buffer.from(bytes);
|
|
38
|
+
}
|
|
39
|
+
function bech32Polymod(values) {
|
|
40
|
+
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
41
|
+
let chk = 1;
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
const top = chk >> 25;
|
|
44
|
+
chk = ((chk & 0x1ffffff) << 5) ^ value;
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
if ((top >> i) & 1)
|
|
47
|
+
chk ^= GEN[i];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return chk;
|
|
51
|
+
}
|
|
52
|
+
function bech32HrpExpand(hrp) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const ch of hrp)
|
|
55
|
+
out.push(ch.charCodeAt(0) >> 5);
|
|
56
|
+
out.push(0);
|
|
57
|
+
for (const ch of hrp)
|
|
58
|
+
out.push(ch.charCodeAt(0) & 31);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function convertBits(data, from, to) {
|
|
62
|
+
let acc = 0;
|
|
63
|
+
let bits = 0;
|
|
64
|
+
const out = [];
|
|
65
|
+
const maxv = (1 << to) - 1;
|
|
66
|
+
for (const value of data) {
|
|
67
|
+
if (value < 0 || value >> from !== 0)
|
|
68
|
+
return null;
|
|
69
|
+
acc = (acc << from) | value;
|
|
70
|
+
bits += from;
|
|
71
|
+
while (bits >= to) {
|
|
72
|
+
bits -= to;
|
|
73
|
+
out.push((acc >> bits) & maxv);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (bits >= from)
|
|
77
|
+
return null;
|
|
78
|
+
if (((acc << (to - bits)) & maxv) !== 0)
|
|
79
|
+
return null;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function decodeBase58Address(address) {
|
|
83
|
+
const bytes = base58Decode(address);
|
|
84
|
+
if (bytes.length !== 25)
|
|
85
|
+
throw new SourceError("base58 addresses are 25 bytes; this one has the wrong length");
|
|
86
|
+
const payload = bytes.subarray(0, 21);
|
|
87
|
+
const checksum = bytes.subarray(21);
|
|
88
|
+
const expected = sha256(sha256(payload)).subarray(0, 4);
|
|
89
|
+
if (!checksum.equals(expected))
|
|
90
|
+
throw new SourceError("the base58 checksum does not match — the address is mistyped or corrupted");
|
|
91
|
+
const version = payload[0];
|
|
92
|
+
const hash = payload.subarray(1).toString("hex");
|
|
93
|
+
if (version === 0x00)
|
|
94
|
+
return { kind: "P2PKH (pay to public key hash)", network: "mainnet", detail: `hash160 ${hash}`, program: hash };
|
|
95
|
+
if (version === 0x05)
|
|
96
|
+
return { kind: "P2SH (pay to script hash)", network: "mainnet", detail: `script hash ${hash}`, program: hash };
|
|
97
|
+
if (version === 0x6f)
|
|
98
|
+
return { kind: "P2PKH (pay to public key hash)", network: "testnet", detail: `hash160 ${hash}`, program: hash };
|
|
99
|
+
if (version === 0xc4)
|
|
100
|
+
return { kind: "P2SH (pay to script hash)", network: "testnet", detail: `script hash ${hash}`, program: hash };
|
|
101
|
+
throw new SourceError(`unknown version byte 0x${version.toString(16).padStart(2, "0")}`);
|
|
102
|
+
}
|
|
103
|
+
function decodeBech32Address(address) {
|
|
104
|
+
if (address !== address.toLowerCase() && address !== address.toUpperCase()) {
|
|
105
|
+
throw new SourceError("bech32 addresses cannot mix upper and lower case");
|
|
106
|
+
}
|
|
107
|
+
const text = address.toLowerCase();
|
|
108
|
+
const separator = text.lastIndexOf("1");
|
|
109
|
+
if (separator < 1 || separator + 7 > text.length)
|
|
110
|
+
throw new SourceError("the bech32 separator is missing or misplaced");
|
|
111
|
+
const hrp = text.slice(0, separator);
|
|
112
|
+
const dataPart = text.slice(separator + 1);
|
|
113
|
+
const data = [];
|
|
114
|
+
for (const ch of dataPart) {
|
|
115
|
+
const value = BECH32_CHARS.indexOf(ch);
|
|
116
|
+
if (value < 0)
|
|
117
|
+
throw new SourceError(`"${ch}" is not a bech32 character`);
|
|
118
|
+
data.push(value);
|
|
119
|
+
}
|
|
120
|
+
const checksum = bech32Polymod([...bech32HrpExpand(hrp), ...data]);
|
|
121
|
+
const networks = { bc: "mainnet", tb: "testnet", bcrt: "regtest" };
|
|
122
|
+
if (!(hrp in networks))
|
|
123
|
+
throw new SourceError(`unknown bech32 prefix "${hrp}" (expected bc, tb or bcrt)`);
|
|
124
|
+
const version = data[0];
|
|
125
|
+
const constant = version === 0 ? 1 : 0x2bc830a3;
|
|
126
|
+
if (checksum !== constant) {
|
|
127
|
+
throw new SourceError(version === 0 ? "the bech32 checksum does not match" : "the bech32m checksum does not match");
|
|
128
|
+
}
|
|
129
|
+
const program = convertBits(data.slice(1, -6), 5, 8);
|
|
130
|
+
if (!program)
|
|
131
|
+
throw new SourceError("the witness program has invalid padding");
|
|
132
|
+
const hex = Buffer.from(program).toString("hex");
|
|
133
|
+
if (version === 0) {
|
|
134
|
+
if (program.length === 20)
|
|
135
|
+
return { kind: "P2WPKH (segwit v0, witness public key hash)", network: networks[hrp], detail: `witness program ${hex}`, program: hex };
|
|
136
|
+
if (program.length === 32)
|
|
137
|
+
return { kind: "P2WSH (segwit v0, witness script hash)", network: networks[hrp], detail: `witness program ${hex}`, program: hex };
|
|
138
|
+
throw new SourceError("segwit v0 programs must be 20 or 32 bytes");
|
|
139
|
+
}
|
|
140
|
+
if (version === 1 && program.length === 32) {
|
|
141
|
+
return { kind: "P2TR (taproot, segwit v1)", network: networks[hrp], detail: `witness program ${hex}`, program: hex };
|
|
142
|
+
}
|
|
143
|
+
if (version > 16)
|
|
144
|
+
throw new SourceError(`witness version ${version} is out of range (0-16)`);
|
|
145
|
+
return { kind: `segwit v${version}`, network: networks[hrp], detail: `witness program ${hex}`, program: hex };
|
|
146
|
+
}
|
|
147
|
+
export async function validateBtcAddress(a) {
|
|
148
|
+
const address = clean(a.address);
|
|
149
|
+
if (!address)
|
|
150
|
+
throw new SourceError("address is required");
|
|
151
|
+
if (address.length > 100)
|
|
152
|
+
throw new SourceError("address is far too long to be a Bitcoin address");
|
|
153
|
+
const decoded = /^(bc|tb|bcrt)1/i.test(address) ? decodeBech32Address(address) : decodeBase58Address(address);
|
|
154
|
+
return [
|
|
155
|
+
`Valid address.`,
|
|
156
|
+
"",
|
|
157
|
+
bullet("type", decoded.kind),
|
|
158
|
+
bullet("network", decoded.network),
|
|
159
|
+
bullet("detail", decoded.detail),
|
|
160
|
+
bullet("length", `${address.length} characters`),
|
|
161
|
+
]
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.join("\n");
|
|
164
|
+
}
|
|
165
|
+
export async function derivationPathInfo(a) {
|
|
166
|
+
const path = clean(a.path);
|
|
167
|
+
if (!/^m(\/\d+'?)*$/.test(path))
|
|
168
|
+
throw new SourceError(`path must look like m/84'/0'/0'/0/1 (got "${a.path}")`);
|
|
169
|
+
const parts = path.split("/").slice(1);
|
|
170
|
+
const purposes = {
|
|
171
|
+
"44": "BIP-44 legacy P2PKH addresses",
|
|
172
|
+
"49": "BIP-49 P2SH-wrapped segwit",
|
|
173
|
+
"84": "BIP-84 native segwit (bc1q...)",
|
|
174
|
+
"86": "BIP-86 taproot (bc1p...)",
|
|
175
|
+
};
|
|
176
|
+
const coins = { "0": "Bitcoin mainnet", "1": "Bitcoin testnet" };
|
|
177
|
+
const rows = parts.map((part, i) => {
|
|
178
|
+
const hardened = part.endsWith("'");
|
|
179
|
+
const value = Number(hardened ? part.slice(0, -1) : part);
|
|
180
|
+
if (!Number.isInteger(value) || value < 0 || value >= 2 ** 31)
|
|
181
|
+
throw new SourceError(`component ${i + 1} must be between 0 and 2^31-1`);
|
|
182
|
+
const role = i === 0 && purposes[String(value)] ? purposes[String(value)] :
|
|
183
|
+
i === 1 ? (coins[String(value)] ?? `coin type ${value}`) :
|
|
184
|
+
i === 2 ? "account" :
|
|
185
|
+
i === 3 ? (value === 0 ? "external chain (receiving)" : value === 1 ? "internal chain (change)" : `chain ${value}`) :
|
|
186
|
+
i === 4 ? "address index" : `level ${i}`;
|
|
187
|
+
return `${String(i + 1).padStart(2)}. ${part.padEnd(6)} ${hardened ? "hardened" : "normal "} ${role}`;
|
|
188
|
+
});
|
|
189
|
+
return [
|
|
190
|
+
`Derivation path ${path} (${parts.length} level(s), ${parts.filter((p) => p.endsWith("'")).length} hardened):`,
|
|
191
|
+
"",
|
|
192
|
+
...rows,
|
|
193
|
+
"",
|
|
194
|
+
"Hardened levels use the key's private half, so a watch-only wallet cannot derive them.",
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
197
|
+
export async function listAddressTypes() {
|
|
198
|
+
return [
|
|
199
|
+
"Bitcoin address types this checker recognises:",
|
|
200
|
+
"",
|
|
201
|
+
"prefix type version / witness version",
|
|
202
|
+
"1... P2PKH legacy 0x00 base58check",
|
|
203
|
+
"3... P2SH (often wrapped segwit) 0x05 base58check",
|
|
204
|
+
"m... / n... P2PKH testnet 0x6f base58check",
|
|
205
|
+
"2... P2SH testnet 0xc4 base58check",
|
|
206
|
+
"bc1q... P2WPKH / P2WSH witness v0, bech32",
|
|
207
|
+
"bc1p... P2TR taproot witness v1, bech32m",
|
|
208
|
+
"tb1... / bcrt1 the same on testnet and regtest bech32 / bech32m",
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
console.error("btc-address-mcp running on stdio");
|
|
9
|
+
}
|
|
10
|
+
main().catch((err) => {
|
|
11
|
+
console.error("Fatal error:", err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { derivationPathInfo, listAddressTypes, validateBtcAddress, SourceError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
function errorMessage(e) {
|
|
6
|
+
if (e instanceof SourceError || e instanceof Error)
|
|
7
|
+
return `Error: ${e.message}`;
|
|
8
|
+
return `Error: ${String(e)}`;
|
|
9
|
+
}
|
|
10
|
+
export function createServer() {
|
|
11
|
+
const server = new McpServer({ name: "btc-address-mcp", version: "1.0.0" });
|
|
12
|
+
server.tool("validate_btc_address", "Check an address's checksum and shape: P2PKH, P2SH, P2WPKH, P2WSH or P2TR, with network and hash.", {
|
|
13
|
+
address: z.string().describe('Bitcoin address to check')
|
|
14
|
+
}, async (a) => {
|
|
15
|
+
try {
|
|
16
|
+
return text(await validateBtcAddress(a));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
server.tool("derivation_path_info", "Explain a BIP-32 derivation path level by level, including the purpose presets and hardened flags.", {
|
|
23
|
+
path: z.string().describe("Path such as m/84'/0'/0'/0/1")
|
|
24
|
+
}, async (a) => {
|
|
25
|
+
try {
|
|
26
|
+
return text(await derivationPathInfo(a));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("list_address_types", "The address types this checker recognises, with their prefixes and version bytes.", {
|
|
33
|
+
// no arguments
|
|
34
|
+
}, async (a) => {
|
|
35
|
+
try {
|
|
36
|
+
return text(await listAddressTypes());
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return server;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrfentmen/btc-address-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Validate Bitcoin addresses offline with real base58check and bech32/bech32m checksums, and decode BIP-32 derivation paths, keyless.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/btc-address-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mrfentmen/awesome-servers.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"btc-address-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"bitcoin",
|
|
25
|
+
"address",
|
|
26
|
+
"bech32",
|
|
27
|
+
"base58",
|
|
28
|
+
"offline",
|
|
29
|
+
"keyless"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|