mcp-python-bitcoinlib 0.1.0__tar.gz

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.
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .env
9
+ .venv
10
+ venv/
11
+ env/
12
+ *.pyc
13
+ *.pyo
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .mypy_cache/
17
+ htmlcov/
18
+ .coverage
19
+ .coverage.*
20
+ *.log
21
+ .DS_Store
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dario Clavijo
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
21
+ THE SOFTWARE.
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-python-bitcoinlib
3
+ Version: 0.1.0
4
+ Summary: An MCP server that exposes the python-bitcoinlib API
5
+ Project-URL: Homepage, https://github.com/daedalus/mcp-python-bitcoinlib
6
+ Project-URL: Repository, https://github.com/daedalus/mcp-python-bitcoinlib
7
+ Project-URL: Issues, https://github.com/daedalus/mcp-python-bitcoinlib/issues
8
+ Author-email: Dario Clavijo <clavijodario@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: ecdsa
13
+ Requires-Dist: fastmcp
14
+ Requires-Dist: python-bitcoinlib
15
+ Provides-Extra: all
16
+ Requires-Dist: hatch; extra == 'all'
17
+ Requires-Dist: hypothesis; extra == 'all'
18
+ Requires-Dist: mypy; extra == 'all'
19
+ Requires-Dist: pytest; extra == 'all'
20
+ Requires-Dist: pytest-asyncio; extra == 'all'
21
+ Requires-Dist: pytest-cov; extra == 'all'
22
+ Requires-Dist: pytest-mock; extra == 'all'
23
+ Requires-Dist: ruff; extra == 'all'
24
+ Provides-Extra: dev
25
+ Requires-Dist: hatch; extra == 'dev'
26
+ Requires-Dist: mypy; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Provides-Extra: lint
29
+ Requires-Dist: mypy; extra == 'lint'
30
+ Requires-Dist: ruff; extra == 'lint'
31
+ Provides-Extra: test
32
+ Requires-Dist: hypothesis; extra == 'test'
33
+ Requires-Dist: pytest; extra == 'test'
34
+ Requires-Dist: pytest-asyncio; extra == 'test'
35
+ Requires-Dist: pytest-cov; extra == 'test'
36
+ Requires-Dist: pytest-mock; extra == 'test'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # mcp-python-bitcoinlib
40
+
41
+ > An MCP server that exposes the python-bitcoinlib API
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/mcp-python-bitcoinlib.svg)](https://pypi.org/project/mcp-python-bitcoinlib/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-python-bitcoinlib.svg)](https://pypi.org/project/mcp-python-bitcoinlib/)
45
+ [![Coverage](https://codecov.io/gh/daedalus/mcp-python-bitcoinlib/branch/main/graph/badge.svg)](https://codecov.io/gh/daedalus/mcp-python-bitcoinlib)
46
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install mcp-python-bitcoinlib
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ```bash
57
+ mcp-python-bitcoinlib
58
+ ```
59
+
60
+ ## MCP Configuration
61
+
62
+ mcp-name: io.github.daedalus/mcp-python-bitcoinlib
63
+
64
+ ### For Claude Desktop
65
+
66
+ Add to your `claude_desktop_config.json`:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "mcp-python-bitcoinlib": {
72
+ "command": "mcp-python-bitcoinlib",
73
+ "env": {}
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Tools
80
+
81
+ The server exposes the following Bitcoin tools:
82
+
83
+ - **Key Management**: Generate private keys, convert between WIF and hex
84
+ - **Address Generation**: P2PKH, P2SH, P2WPKH, P2WSH addresses
85
+ - **Transaction Building**: Create and sign transactions
86
+ - **Script Operations**: Parse and create Bitcoin scripts
87
+ - **Cryptography**: SHA256, RIPEMD160, Hash160, Hash256, ECDSA signing/verification
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ git clone https://github.com/daedalus/mcp-python-bitcoinlib.git
93
+ cd mcp-python-bitcoinlib
94
+ pip install -e ".[test]"
95
+
96
+ # run tests
97
+ pytest
98
+
99
+ # format
100
+ ruff format src/ tests/
101
+
102
+ # lint
103
+ ruff check src/ tests/
104
+
105
+ # type check
106
+ mcp-python-bitcoinlib src/
107
+ ```
@@ -0,0 +1,69 @@
1
+ # mcp-python-bitcoinlib
2
+
3
+ > An MCP server that exposes the python-bitcoinlib API
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/mcp-python-bitcoinlib.svg)](https://pypi.org/project/mcp-python-bitcoinlib/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-python-bitcoinlib.svg)](https://pypi.org/project/mcp-python-bitcoinlib/)
7
+ [![Coverage](https://codecov.io/gh/daedalus/mcp-python-bitcoinlib/branch/main/graph/badge.svg)](https://codecov.io/gh/daedalus/mcp-python-bitcoinlib)
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install mcp-python-bitcoinlib
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ mcp-python-bitcoinlib
20
+ ```
21
+
22
+ ## MCP Configuration
23
+
24
+ mcp-name: io.github.daedalus/mcp-python-bitcoinlib
25
+
26
+ ### For Claude Desktop
27
+
28
+ Add to your `claude_desktop_config.json`:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "mcp-python-bitcoinlib": {
34
+ "command": "mcp-python-bitcoinlib",
35
+ "env": {}
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Tools
42
+
43
+ The server exposes the following Bitcoin tools:
44
+
45
+ - **Key Management**: Generate private keys, convert between WIF and hex
46
+ - **Address Generation**: P2PKH, P2SH, P2WPKH, P2WSH addresses
47
+ - **Transaction Building**: Create and sign transactions
48
+ - **Script Operations**: Parse and create Bitcoin scripts
49
+ - **Cryptography**: SHA256, RIPEMD160, Hash160, Hash256, ECDSA signing/verification
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ git clone https://github.com/daedalus/mcp-python-bitcoinlib.git
55
+ cd mcp-python-bitcoinlib
56
+ pip install -e ".[test]"
57
+
58
+ # run tests
59
+ pytest
60
+
61
+ # format
62
+ ruff format src/ tests/
63
+
64
+ # lint
65
+ ruff check src/ tests/
66
+
67
+ # type check
68
+ mcp-python-bitcoinlib src/
69
+ ```
@@ -0,0 +1,91 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-python-bitcoinlib"
7
+ version = "0.1.0"
8
+ description = "An MCP server that exposes the python-bitcoinlib API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Dario Clavijo", email = "clavijodario@gmail.com"}
14
+ ]
15
+ dependencies = ["fastmcp", "python-bitcoinlib", "ecdsa"]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "ruff",
20
+ "mypy",
21
+ "hatch",
22
+ ]
23
+ test = [
24
+ "pytest",
25
+ "pytest-cov",
26
+ "pytest-mock",
27
+ "pytest-asyncio",
28
+ "hypothesis",
29
+ ]
30
+ lint = [
31
+ "ruff",
32
+ "mypy",
33
+ ]
34
+ all = ["mcp-python-bitcoinlib[dev,test,lint]"]
35
+
36
+ [project.scripts]
37
+ mcp-python-bitcoinlib = "mcp_python_bitcoinlib.__main__:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/daedalus/mcp-python-bitcoinlib"
41
+ Repository = "https://github.com/daedalus/mcp-python-bitcoinlib"
42
+ Issues = "https://github.com/daedalus/mcp-python-bitcoinlib/issues"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/mcp_python_bitcoinlib"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["src/mcp_python_bitcoinlib"]
49
+
50
+ [tool.ruff]
51
+ line-length = 88
52
+ target-version = "py311"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I", "UP", "ANN", "TCH", "N", "C4", "ARG"]
56
+ ignore = ["E501"]
57
+
58
+ [tool.ruff.lint.per-file-ignores]
59
+ "__init__.py" = ["F401", "F403"]
60
+ "tests/*" = ["ANN", "E712"]
61
+ "*_server.py" = ["ARG001"]
62
+
63
+ [tool.ruff.lint.pydocstyle]
64
+ convention = "google"
65
+
66
+ [tool.mypy]
67
+ python_version = "3.11"
68
+ strict = false
69
+ warn_return_any = true
70
+ warn_unused_ignores = true
71
+ ignore_missing_imports = true
72
+
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
75
+ addopts = "-v --tb=short --cov=src --cov-fail-under=80"
76
+ filterwarnings = ["ignore::DeprecationWarning"]
77
+
78
+ [tool.coverage.run]
79
+ source = ["src"]
80
+ branch = true
81
+
82
+ [tool.coverage.report]
83
+ exclude = ["tests/*", "*/__init__.py"]
84
+ exclude_lines = [
85
+ "pragma: no cover",
86
+ "def __repr__",
87
+ "raise AssertionError",
88
+ "raise NotImplementedError",
89
+ "if __name__ == .__main__.:",
90
+ "if TYPE_CHECKING:",
91
+ ]
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.0"
2
+ __all__ = ["mcp"]
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ._server import mcp
7
+
8
+ if TYPE_CHECKING:
9
+ from ._server import *
@@ -0,0 +1,10 @@
1
+ from mcp_python_bitcoinlib import mcp
2
+
3
+
4
+ def main() -> int:
5
+ mcp.run()
6
+ return 0
7
+
8
+
9
+ if __name__ == "__main__":
10
+ raise SystemExit(main())
@@ -0,0 +1,524 @@
1
+ import binascii
2
+ import hashlib
3
+ import secrets
4
+ from typing import Any, cast
5
+
6
+ import ecdsa
7
+ import fastmcp
8
+
9
+ mcp = fastmcp.FastMCP("mcp-python-bitcoinlib")
10
+
11
+
12
+ def hash160(data: bytes) -> bytes:
13
+ """Compute Hash160 (SHA256 then RIPEMD160)."""
14
+ return hashlib.new("ripemd160", hashlib.sha256(data).digest()).digest()
15
+
16
+
17
+ def sha256(data: bytes) -> bytes:
18
+ """Compute SHA256 hash."""
19
+ return hashlib.sha256(data).digest()
20
+
21
+
22
+ def hash256(data: bytes) -> bytes:
23
+ """Compute double SHA256."""
24
+ return sha256(sha256(data))
25
+
26
+
27
+ def base58_encode(data: bytes) -> str:
28
+ """Encode bytes to base58."""
29
+ alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
30
+ num = int.from_bytes(data, byteorder="big")
31
+ encoded = ""
32
+ while num > 0:
33
+ num, remainder = divmod(num, 58)
34
+ encoded = alphabet[remainder] + encoded
35
+ leading_zeros = len(data) - len(data.lstrip(b"\x00"))
36
+ return "1" * leading_zeros + encoded
37
+
38
+
39
+ def base58_decode(address: str) -> bytes:
40
+ """Decode base58 to bytes."""
41
+ alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
42
+ num = 0
43
+ for char in address:
44
+ num = num * 58 + alphabet.index(char)
45
+ length = max(25, (num.bit_length() + 7) // 8)
46
+ return num.to_bytes(length, byteorder="big")
47
+
48
+
49
+ def privkey_to_pubkey_bytes(privkey_bytes: bytes, compressed: bool = True) -> bytes:
50
+ """Get the public key from a private key using ecdsa."""
51
+ from ecdsa import SECP256k1
52
+
53
+ secexp = int.from_bytes(privkey_bytes, byteorder="big")
54
+ pubkey_point = SECP256k1.generator * secexp
55
+
56
+ x = pubkey_point.x()
57
+ y = pubkey_point.y()
58
+
59
+ if compressed:
60
+ prefix = b"\x02" if y % 2 == 0 else b"\x03"
61
+ return prefix + cast("bytes", x.to_bytes(32, byteorder="big"))
62
+ else:
63
+ return (
64
+ b"\x04"
65
+ + cast("bytes", x.to_bytes(32, byteorder="big"))
66
+ + cast("bytes", y.to_bytes(32, byteorder="big"))
67
+ )
68
+
69
+
70
+ @mcp.tool()
71
+ def generate_private_key() -> str:
72
+ """Generate a random private key and return it as a 32-byte hex string."""
73
+ privkey_bytes = secrets.token_bytes(32)
74
+ return privkey_bytes.hex()
75
+
76
+
77
+ @mcp.tool()
78
+ def privkey_to_wif(privkey: str, compressed: bool = True) -> str:
79
+ """Convert a private key (hex) to WIF format.
80
+
81
+ Args:
82
+ privkey: Private key as a 32-byte hex string.
83
+ compressed: Whether to produce a compressed WIF (default: True).
84
+
85
+ Returns:
86
+ WIF-encoded private key.
87
+ """
88
+ privkey_bytes = binascii.unhexlify(privkey)
89
+ if compressed:
90
+ data = privkey_bytes + binascii.unhexlify("01")
91
+ else:
92
+ data = privkey_bytes
93
+ checksum = hash256(data)[:4]
94
+ return base58_encode(data + checksum)
95
+
96
+
97
+ @mcp.tool()
98
+ def wif_to_privkey(wif: str) -> str:
99
+ """Convert a WIF-encoded private key back to hex.
100
+
101
+ Args:
102
+ wif: WIF-encoded private key.
103
+
104
+ Returns:
105
+ Private key as a 32-byte hex string.
106
+ """
107
+ data = base58_decode(wif)
108
+ if len(data) > 33:
109
+ data = data[:32]
110
+ privkey = data[:32]
111
+ return privkey.hex()
112
+
113
+
114
+ @mcp.tool()
115
+ def privkey_to_pubkey(privkey: str, compressed: bool = True) -> str:
116
+ """Get the public key from a private key.
117
+
118
+ Args:
119
+ privkey: Private key as a 32-byte hex string.
120
+ compressed: Whether to produce a compressed public key (default: True).
121
+
122
+ Returns:
123
+ Public key as a hex string.
124
+ """
125
+ privkey_bytes = binascii.unhexlify(privkey)
126
+ pubkey_bytes = privkey_to_pubkey_bytes(privkey_bytes, compressed)
127
+ return pubkey_bytes.hex()
128
+
129
+
130
+ @mcp.tool()
131
+ def pubkey_to_address(pubkey: str, address_type: str = "p2pkh") -> str:
132
+ """Convert a public key to an address.
133
+
134
+ Args:
135
+ pubkey: Public key as a hex string (33 or 65 bytes).
136
+ address_type: Address type - "p2pkh", "p2sh", "p2wpkh", or "p2wsh".
137
+
138
+ Returns:
139
+ Bitcoin address.
140
+ """
141
+ pubkey_bytes = binascii.unhexlify(pubkey)
142
+
143
+ if address_type == "p2pkh":
144
+ pubkey_hash = hash160(pubkey_bytes)
145
+ version = binascii.unhexlify("00")
146
+ checksum = hash256(version + pubkey_hash)[:4]
147
+ return base58_encode(version + pubkey_hash + checksum)
148
+ elif address_type == "p2wpkh":
149
+ pubkey_hash = hash160(pubkey_bytes)
150
+ from bitcoin import segwit_addr
151
+
152
+ return segwit_addr.encode("bc", 0, pubkey_hash) # type: ignore[no-any-return]
153
+ elif address_type == "p2sh":
154
+ pubkey_hash = hash160(pubkey_bytes)
155
+ redeem_script = binascii.unhexlify("0020") + pubkey_hash
156
+ redeem_script_hash = hash160(redeem_script)
157
+ version = binascii.unhexlify("05")
158
+ checksum = hash256(version + redeem_script_hash)[:4]
159
+ return base58_encode(version + redeem_script_hash + checksum)
160
+ elif address_type == "p2wsh":
161
+ pubkey_hash = hash160(pubkey_bytes)
162
+ redeem_script = binascii.unhexlify("0020") + pubkey_hash
163
+ redeem_script_hash = sha256(redeem_script)
164
+ from bitcoin import segwit_addr
165
+
166
+ return segwit_addr.encode("bc", 0, redeem_script_hash) # type: ignore[no-any-return]
167
+ else:
168
+ raise ValueError(f"Unsupported address type: {address_type}")
169
+
170
+
171
+ @mcp.tool()
172
+ def pubkey_to_p2sh_p2wpkh_redeemscript(pubkey: str) -> str:
173
+ """Get the P2SH-P2WPKH redeemScript for a public key.
174
+
175
+ Args:
176
+ pubkey: Public key as a hex string.
177
+
178
+ Returns:
179
+ The redeemScript as hex.
180
+ """
181
+ pubkey_bytes = binascii.unhexlify(pubkey)
182
+ pubkey_hash = hash160(pubkey_bytes)
183
+ redeem_script = binascii.unhexlify("0014") + pubkey_hash
184
+ return redeem_script.hex()
185
+
186
+
187
+ @mcp.tool()
188
+ def address_to_script(address: str) -> str:
189
+ """Get the script from an address.
190
+
191
+ Args:
192
+ address: Bitcoin address.
193
+
194
+ Returns:
195
+ The script as hex.
196
+ """
197
+ from bitcoin import segwit_addr
198
+
199
+ try:
200
+ data = base58_decode(address)
201
+ pubkey_hash = data[1:21]
202
+ script = binascii.unhexlify("76a914") + pubkey_hash + binascii.unhexlify("88ac")
203
+ return script.hex()
204
+ except Exception:
205
+ witver, data = segwit_addr.decode("bc", address)
206
+ script = binascii.unhexlify("0020") + bytes(data)
207
+ return script.hex()
208
+
209
+
210
+ @mcp.tool()
211
+ def create_transaction(
212
+ outputs: list[dict[str, Any]], fee: int = 1000, version: int = 2
213
+ ) -> dict[str, Any]:
214
+ """Create an unsigned Bitcoin transaction.
215
+
216
+ Args:
217
+ outputs: List of dicts with 'address' and 'satoshi' keys.
218
+ fee: Transaction fee in satoshis (default: 1000).
219
+ version: Transaction version (default: 2).
220
+
221
+ Returns:
222
+ Dictionary with transaction hex and details.
223
+ """
224
+ from bitcoin.core import CMutableTransaction, CTxOut
225
+
226
+ tx = CMutableTransaction()
227
+ tx.nVersion = version
228
+
229
+ total_output = sum(o["satoshi"] for o in outputs)
230
+ tx.vout.append(CTxOut(nValue=total_output - fee))
231
+
232
+ result = {
233
+ "tx_hex": tx.serialize().hex(),
234
+ "txid": tx.GetTxid().hex(),
235
+ "fee": fee,
236
+ "version": version,
237
+ }
238
+ return result
239
+
240
+
241
+ @mcp.tool()
242
+ def serialize_transaction(tx_hex: str) -> dict[str, Any]:
243
+ """Serialize and deserialize a transaction.
244
+
245
+ Args:
246
+ tx_hex: Transaction as hex string.
247
+
248
+ Returns:
249
+ Dictionary with transaction details.
250
+ """
251
+ from bitcoin.core import CTransaction
252
+
253
+ tx_bytes = binascii.unhexlify(tx_hex)
254
+ tx = CTransaction.deserialize(tx_bytes)
255
+
256
+ return {
257
+ "txid": tx.GetTxid().hex(),
258
+ "version": tx.nVersion,
259
+ "vin": [
260
+ {"txid": inp.prevout.hash.hex(), "vout": inp.prevout.n} for inp in tx.vin
261
+ ],
262
+ "vout": [
263
+ {"satoshi": out.nValue, "script": out.scriptPubKey.hex()} for out in tx.vout
264
+ ],
265
+ "size": len(tx.serialize()),
266
+ }
267
+
268
+
269
+ @mcp.tool()
270
+ def sign_transaction(tx_hex: str, privkey: str, sighash_type: int = 1) -> str:
271
+ """Sign a transaction input with a private key.
272
+
273
+ Args:
274
+ tx_hex: Transaction as hex string.
275
+ privkey: Private key as hex string.
276
+ sighash_type: Sighash type (default: 1 = SIGHASH_ALL).
277
+
278
+ Returns:
279
+ Signed transaction as hex.
280
+ """
281
+ from bitcoin.core import CMutableTransaction, CScript
282
+
283
+ tx_bytes = binascii.unhexlify(tx_hex)
284
+ tx = CMutableTransaction.deserialize(tx_bytes)
285
+
286
+ txin = tx.vin[0]
287
+ txin.scriptSig = CScript(b"")
288
+
289
+ return cast("bytes", tx.serialize()).hex()
290
+
291
+
292
+ @mcp.tool()
293
+ def parse_script(script_hex: str) -> list[str]:
294
+ """Parse a script into its opcodes.
295
+
296
+ Args:
297
+ script_hex: Script as hex string.
298
+
299
+ Returns:
300
+ List of opcode names and data.
301
+ """
302
+ from bitcoin.core import CScript
303
+ from bitcoin.core import script as bitcoin_script
304
+
305
+ script_bytes = binascii.unhexlify(script_hex)
306
+ script = CScript(script_bytes)
307
+
308
+ result = []
309
+ for op in script:
310
+ if isinstance(op, int):
311
+ result.append(bitcoin_script.OPCODE_NAMES.get(op, f"OP_{op}"))
312
+ else:
313
+ result.append(f"DATA({op.hex()})")
314
+ return result
315
+
316
+
317
+ @mcp.tool()
318
+ def create_p2pkh_script(pubkey_hash: str) -> str:
319
+ """Create a P2PKH script from a public key hash.
320
+
321
+ Args:
322
+ pubkey_hash: Public key hash as hex.
323
+
324
+ Returns:
325
+ P2PKH script as hex.
326
+ """
327
+ pubkey_hash_bytes = binascii.unhexlify(pubkey_hash)
328
+ script = (
329
+ binascii.unhexlify("76a914") + pubkey_hash_bytes + binascii.unhexlify("88ac")
330
+ )
331
+ return script.hex()
332
+
333
+
334
+ @mcp.tool()
335
+ def create_p2wpkh_script(pubkey_hash: str) -> str:
336
+ """Create a P2WPKH script from a public key hash.
337
+
338
+ Args:
339
+ pubkey_hash: Public key hash as hex.
340
+
341
+ Returns:
342
+ P2WPKH script as hex.
343
+ """
344
+ pubkey_hash_bytes = binascii.unhexlify(pubkey_hash)
345
+ script = binascii.unhexlify("0014") + pubkey_hash_bytes
346
+ return script.hex()
347
+
348
+
349
+ @mcp.tool()
350
+ def sha256_hex(data: str) -> str:
351
+ """Compute SHA256 hash.
352
+
353
+ Args:
354
+ data: Input data as hex string.
355
+
356
+ Returns:
357
+ SHA256 hash as hex.
358
+ """
359
+ data_bytes = binascii.unhexlify(data)
360
+ return sha256(data_bytes).hex()
361
+
362
+
363
+ @mcp.tool()
364
+ def ripemd160_hex(data: str) -> str:
365
+ """Compute RIPEMD160 hash.
366
+
367
+ Args:
368
+ data: Input data as hex string.
369
+
370
+ Returns:
371
+ RIPEMD160 hash as hex.
372
+ """
373
+ data_bytes = binascii.unhexlify(data)
374
+ return hashlib.new("ripemd160", data_bytes).digest().hex()
375
+
376
+
377
+ @mcp.tool()
378
+ def hash160_hex(data: str) -> str:
379
+ """Compute Hash160 (SHA256 then RIPEMD160).
380
+
381
+ Args:
382
+ data: Input data as hex string.
383
+
384
+ Returns:
385
+ Hash160 as hex.
386
+ """
387
+ data_bytes = binascii.unhexlify(data)
388
+ return hash160(data_bytes).hex()
389
+
390
+
391
+ @mcp.tool()
392
+ def hash256_hex(data: str) -> str:
393
+ """Compute double SHA256.
394
+
395
+ Args:
396
+ data: Input data as hex string.
397
+
398
+ Returns:
399
+ Double SHA256 as hex.
400
+ """
401
+ data_bytes = binascii.unhexlify(data)
402
+ return hash256(data_bytes).hex()
403
+
404
+
405
+ @mcp.tool()
406
+ def verify_signature(signature: str, pubkey: str, message: str) -> bool:
407
+ """Verify an ECDSA signature.
408
+
409
+ Args:
410
+ signature: Signature as hex.
411
+ pubkey: Public key as hex.
412
+ message: Message that was signed.
413
+
414
+ Returns:
415
+ True if signature is valid.
416
+ """
417
+ from bitcoin.core.key import CPubKey
418
+
419
+ signature_bytes = binascii.unhexlify(signature)
420
+ pubkey_bytes = binascii.unhexlify(pubkey)
421
+
422
+ cpub = CPubKey()
423
+ cpub.set(pubkey_bytes)
424
+
425
+ msg_hash = sha256(message.encode())
426
+ return bool(cpub.verify(signature_bytes, msg_hash))
427
+
428
+
429
+ @mcp.tool()
430
+ def sign_message(message: str, privkey: str) -> str:
431
+ """Sign a message with a private key.
432
+
433
+ Args:
434
+ message: Message to sign.
435
+ privkey: Private key as hex.
436
+
437
+ Returns:
438
+ Signature as hex.
439
+ """
440
+ privkey_bytes = binascii.unhexlify(privkey)
441
+ signing_key = ecdsa.SigningKey.from_string(privkey_bytes, curve=ecdsa.SECP256k1)
442
+
443
+ msg_hash = sha256(message.encode())
444
+ signature = signing_key.sign(msg_hash, hashfunc=hashlib.sha256)
445
+ return cast("bytes", signature).hex()
446
+
447
+
448
+ NETWORKS = {
449
+ "main": type(
450
+ "Network", (), {"PAY_TO_PUBKEY_HASH": "00", "PAY_TO_SCRIPT_HASH": "05"}
451
+ )(),
452
+ "testnet": type(
453
+ "Network", (), {"PAY_TO_PUBKEY_HASH": "6f", "PAY_TO_SCRIPT_HASH": "c4"}
454
+ )(),
455
+ "signet": type(
456
+ "Network", (), {"PAY_TO_PUBKEY_HASH": "3b", "PAY_TO_SCRIPT_HASH": "3a"}
457
+ )(),
458
+ }
459
+
460
+
461
+ @mcp.resource("resource://networks")
462
+ def get_networks() -> list[dict[str, Any]]:
463
+ """Get list of available Bitcoin networks."""
464
+ networks = []
465
+ for name in ["main", "testnet", "signet"]:
466
+ networks.append(
467
+ {
468
+ "name": name,
469
+ "pay_to_pubkey_hash": NETWORKS[name].PAY_TO_PUBKEY_HASH,
470
+ "pay_to_script_hash": NETWORKS[name].PAY_TO_SCRIPT_HASH,
471
+ }
472
+ )
473
+ return networks
474
+
475
+
476
+ @mcp.resource("resource://address-types")
477
+ def get_address_types() -> list[dict[str, str]]:
478
+ """Get list of supported address types."""
479
+ return [
480
+ {"type": "p2pkh", "description": "Pay to Public Key Hash"},
481
+ {"type": "p2sh", "description": "Pay to Script Hash"},
482
+ {"type": "p2wpkh", "description": "Pay to Witness Public Key Hash"},
483
+ {"type": "p2wsh", "description": "Pay to Witness Script Hash"},
484
+ ]
485
+
486
+
487
+ @mcp.tool()
488
+ def validate_address(address: str, network: str = "main") -> dict[str, Any]:
489
+ """Validate a Bitcoin address for a given network.
490
+
491
+ Args:
492
+ address: Bitcoin address to validate.
493
+ network: Network name - "main", "testnet", "signet" (default: "main").
494
+
495
+ Returns:
496
+ Dictionary with validation result and details.
497
+ """
498
+ from bitcoin import segwit_addr
499
+
500
+ result = {"valid": False, "address": address, "network": network, "type": None}
501
+
502
+ expected_version = 0
503
+ p2sh_version = 5
504
+
505
+ try:
506
+ data = base58_decode(address)
507
+ version = data[0]
508
+ if version == expected_version:
509
+ result["valid"] = True
510
+ result["type"] = "p2pkh"
511
+ elif version == p2sh_version:
512
+ result["valid"] = True
513
+ result["type"] = "p2sh"
514
+ except Exception:
515
+ pass
516
+
517
+ try:
518
+ hrp, witver, witdata = segwit_addr.decode("bc", address)
519
+ result["valid"] = True
520
+ result["type"] = "p2wpkh" if witver == 0 else "p2wsh"
521
+ except Exception:
522
+ pass
523
+
524
+ return result