boltwork-mcp 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,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boltwork-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Boltwork — AI services via Bitcoin Lightning. PDF summarisation, code review, translation, and more.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Squidboy30/boltwork-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/Squidboy30/boltwork-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/Squidboy30/boltwork-mcp/issues
|
|
8
|
+
Project-URL: API, https://parsebit.fly.dev
|
|
9
|
+
Author-email: Cracked Minds <hello@crackedminds.co.uk>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: agents,ai,bitcoin,code-review,l402,lightning,mcp,model-context-protocol,pdf,summarisation,translation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: mcp>=1.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Provides-Extra: nwc
|
|
27
|
+
Requires-Dist: pynostr>=0.6.0; extra == 'nwc'
|
|
28
|
+
Requires-Dist: websockets>=12.0; extra == 'nwc'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# boltwork-mcp
|
|
32
|
+
|
|
33
|
+
**MCP server for Boltwork — AI services that pay for themselves via Bitcoin Lightning.**
|
|
34
|
+
|
|
35
|
+
Give your AI agent PDF summarisation, code review, translation, web extraction, document comparison, and persistent memory — all paid autonomously in sats. No API keys. No subscriptions. No accounts.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/boltwork-mcp/)
|
|
38
|
+
[](https://www.python.org/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://parsebit.fly.dev)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What this is
|
|
45
|
+
|
|
46
|
+
[Boltwork](https://parsebit.fly.dev) is a pay-per-call AI services API that uses the [L402 protocol](https://github.com/lightninglabs/L402) — your agent makes a request, receives a Lightning invoice, pays it automatically, and gets the result back. No human involved.
|
|
47
|
+
|
|
48
|
+
This package wraps Boltwork as an [MCP server](https://modelcontextprotocol.io) so any MCP-compatible AI (Claude, Cursor, Windsurf, etc.) can use it as a tool — with payments handled transparently in the background.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install boltwork-mcp
|
|
54
|
+
|
|
55
|
+
# If using NWC (Nostr Wallet Connect):
|
|
56
|
+
pip install "boltwork-mcp[nwc]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or use directly with `uvx` — no install needed:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uvx boltwork-mcp
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Setup
|
|
66
|
+
|
|
67
|
+
### 1. Get a Lightning wallet
|
|
68
|
+
|
|
69
|
+
You need a Lightning wallet that supports either:
|
|
70
|
+
|
|
71
|
+
**Option A — NWC (recommended, easiest)**
|
|
72
|
+
- [Alby](https://getalby.com) — browser extension, free, gives you an NWC connection string
|
|
73
|
+
- [Mutiny Wallet](https://mutinywallet.com) — self-custodial mobile wallet
|
|
74
|
+
|
|
75
|
+
**Option B — Phoenixd**
|
|
76
|
+
- [Phoenixd](https://phoenix.acinq.co/server) — self-hosted Lightning node, simple REST API
|
|
77
|
+
|
|
78
|
+
### 2. Add to your MCP config
|
|
79
|
+
|
|
80
|
+
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"boltwork": {
|
|
86
|
+
"command": "uvx",
|
|
87
|
+
"args": ["boltwork-mcp"],
|
|
88
|
+
"env": {
|
|
89
|
+
"NWC_CONNECTION_STRING": "nostr+walletconnect://your-connection-string-here"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Cursor** — edit `.cursor/mcp.json` in your project:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"boltwork": {
|
|
102
|
+
"command": "uvx",
|
|
103
|
+
"args": ["boltwork-mcp"],
|
|
104
|
+
"env": {
|
|
105
|
+
"NWC_CONNECTION_STRING": "nostr+walletconnect://your-connection-string-here"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Using Phoenixd instead of NWC:**
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"boltwork": {
|
|
118
|
+
"command": "uvx",
|
|
119
|
+
"args": ["boltwork-mcp"],
|
|
120
|
+
"env": {
|
|
121
|
+
"PHOENIXD_URL": "http://localhost:9740",
|
|
122
|
+
"PHOENIXD_PASSWORD": "your-phoenixd-password"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Restart your AI and start using tools
|
|
130
|
+
|
|
131
|
+
That's it. Your agent now has access to all Boltwork tools and will pay invoices automatically when it uses them.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Available tools
|
|
136
|
+
|
|
137
|
+
| Tool | What it does | Cost |
|
|
138
|
+
|------|-------------|------|
|
|
139
|
+
| `summarise_pdf` | Summarise a PDF from URL | 500 sats |
|
|
140
|
+
| `review_code` | Review code for bugs, security, quality | 2000 sats |
|
|
141
|
+
| `review_code_url` | Review code from GitHub/GitLab URL | 2000 sats |
|
|
142
|
+
| `summarise_webpage` | Summarise any web page | 100 sats |
|
|
143
|
+
| `extract_data` | Extract structured data from PDF | 200 sats |
|
|
144
|
+
| `translate` | Translate text or document to 24 languages | 150 sats |
|
|
145
|
+
| `extract_tables` | Extract tables from PDF as structured JSON | 300 sats |
|
|
146
|
+
| `compare_documents` | Diff two PDFs | 500 sats |
|
|
147
|
+
| `explain_code` | Explain code in plain English | 500 sats |
|
|
148
|
+
| `memory_store` | Store persistent key-value memory | 10 sats |
|
|
149
|
+
| `memory_retrieve` | Read stored memory | 5 sats |
|
|
150
|
+
| `memory_delete` | Delete a memory key | free |
|
|
151
|
+
| `run_workflow` | Chain services in a single pipeline | 1000 sats |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Example prompts
|
|
156
|
+
|
|
157
|
+
Once configured, just talk to your AI naturally:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
"Summarise this research paper: https://arxiv.org/pdf/2301.00000"
|
|
161
|
+
|
|
162
|
+
"Review the code at https://github.com/me/repo/blob/main/app.py"
|
|
163
|
+
|
|
164
|
+
"Translate this contract to Spanish: https://example.com/contract.pdf"
|
|
165
|
+
|
|
166
|
+
"Compare these two versions of our terms of service:
|
|
167
|
+
v1: https://example.com/tos-v1.pdf
|
|
168
|
+
v2: https://example.com/tos-v2.pdf"
|
|
169
|
+
|
|
170
|
+
"Remember that the last file I asked you to review was app.py
|
|
171
|
+
and the score was 7/10"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Your agent handles the payment automatically — you just see the result.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Workflow pipelines
|
|
179
|
+
|
|
180
|
+
Chain multiple services in one call with `run_workflow`:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
"Fetch the Bitcoin whitepaper, translate the summary to French,
|
|
184
|
+
and store the translation in my agent memory"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The `$from` syntax passes outputs between steps:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"steps": [
|
|
192
|
+
{"service": "pdf", "input": {"url": "https://bitcoin.org/bitcoin.pdf"}},
|
|
193
|
+
{"service": "translate", "input": {"text": {"$from": 0}, "target_language": "french"}}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Environment variables
|
|
201
|
+
|
|
202
|
+
| Variable | Required | Description |
|
|
203
|
+
|----------|----------|-------------|
|
|
204
|
+
| `NWC_CONNECTION_STRING` | One of these two | Nostr Wallet Connect string from Alby/Mutiny |
|
|
205
|
+
| `PHOENIXD_URL` | One of these two | Phoenixd base URL e.g. `http://localhost:9740` |
|
|
206
|
+
| `PHOENIXD_PASSWORD` | With Phoenixd | Phoenixd HTTP Basic auth password |
|
|
207
|
+
| `BOLTWORK_GATEWAY` | Optional | Override gateway URL (default: `https://parsebit-lnd.fly.dev`) |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Pricing
|
|
212
|
+
|
|
213
|
+
All prices are in satoshis (sats). At current rates, 1000 sats ≈ $0.60 — but this varies with Bitcoin's price.
|
|
214
|
+
|
|
215
|
+
There is no subscription, no monthly fee, no minimum spend. You pay exactly for what your agent uses, nothing more.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## How L402 works
|
|
220
|
+
|
|
221
|
+
1. Agent calls a Boltwork tool
|
|
222
|
+
2. `boltwork-mcp` sends the request to `parsebit-lnd.fly.dev`
|
|
223
|
+
3. Receives HTTP 402 with a Lightning invoice (e.g. 500 sats for PDF summarisation)
|
|
224
|
+
4. Pays the invoice via your configured wallet (NWC or Phoenixd)
|
|
225
|
+
5. Retries the request with the payment proof
|
|
226
|
+
6. Returns the result to your agent
|
|
227
|
+
|
|
228
|
+
The whole flow takes 1-3 seconds. Your agent sees only the final result.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Try before you pay
|
|
233
|
+
|
|
234
|
+
Boltwork has free trial endpoints — no Lightning wallet needed:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
curl -X POST https://parsebit.fly.dev/trial/review \
|
|
238
|
+
-H "Content-Type: application/json" \
|
|
239
|
+
-d '{"code": "def add(a, b): return a + b"}'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Links
|
|
245
|
+
|
|
246
|
+
- [Boltwork API](https://parsebit.fly.dev) — the service this MCP server wraps
|
|
247
|
+
- [Agent spec](https://parsebit.fly.dev/agent-spec.md) — full API reference
|
|
248
|
+
- [L402 Index](https://402index.io) — directory of L402 services
|
|
249
|
+
- [MCP documentation](https://modelcontextprotocol.io) — learn about MCP
|
|
250
|
+
- [Cracked Minds](https://crackedminds.co.uk) — the team behind Boltwork
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# boltwork-mcp
|
|
2
|
+
|
|
3
|
+
**MCP server for Boltwork — AI services that pay for themselves via Bitcoin Lightning.**
|
|
4
|
+
|
|
5
|
+
Give your AI agent PDF summarisation, code review, translation, web extraction, document comparison, and persistent memory — all paid autonomously in sats. No API keys. No subscriptions. No accounts.
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/boltwork-mcp/)
|
|
8
|
+
[](https://www.python.org/)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://parsebit.fly.dev)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this is
|
|
15
|
+
|
|
16
|
+
[Boltwork](https://parsebit.fly.dev) is a pay-per-call AI services API that uses the [L402 protocol](https://github.com/lightninglabs/L402) — your agent makes a request, receives a Lightning invoice, pays it automatically, and gets the result back. No human involved.
|
|
17
|
+
|
|
18
|
+
This package wraps Boltwork as an [MCP server](https://modelcontextprotocol.io) so any MCP-compatible AI (Claude, Cursor, Windsurf, etc.) can use it as a tool — with payments handled transparently in the background.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install boltwork-mcp
|
|
24
|
+
|
|
25
|
+
# If using NWC (Nostr Wallet Connect):
|
|
26
|
+
pip install "boltwork-mcp[nwc]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or use directly with `uvx` — no install needed:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uvx boltwork-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
### 1. Get a Lightning wallet
|
|
38
|
+
|
|
39
|
+
You need a Lightning wallet that supports either:
|
|
40
|
+
|
|
41
|
+
**Option A — NWC (recommended, easiest)**
|
|
42
|
+
- [Alby](https://getalby.com) — browser extension, free, gives you an NWC connection string
|
|
43
|
+
- [Mutiny Wallet](https://mutinywallet.com) — self-custodial mobile wallet
|
|
44
|
+
|
|
45
|
+
**Option B — Phoenixd**
|
|
46
|
+
- [Phoenixd](https://phoenix.acinq.co/server) — self-hosted Lightning node, simple REST API
|
|
47
|
+
|
|
48
|
+
### 2. Add to your MCP config
|
|
49
|
+
|
|
50
|
+
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"boltwork": {
|
|
56
|
+
"command": "uvx",
|
|
57
|
+
"args": ["boltwork-mcp"],
|
|
58
|
+
"env": {
|
|
59
|
+
"NWC_CONNECTION_STRING": "nostr+walletconnect://your-connection-string-here"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Cursor** — edit `.cursor/mcp.json` in your project:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"boltwork": {
|
|
72
|
+
"command": "uvx",
|
|
73
|
+
"args": ["boltwork-mcp"],
|
|
74
|
+
"env": {
|
|
75
|
+
"NWC_CONNECTION_STRING": "nostr+walletconnect://your-connection-string-here"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Using Phoenixd instead of NWC:**
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"boltwork": {
|
|
88
|
+
"command": "uvx",
|
|
89
|
+
"args": ["boltwork-mcp"],
|
|
90
|
+
"env": {
|
|
91
|
+
"PHOENIXD_URL": "http://localhost:9740",
|
|
92
|
+
"PHOENIXD_PASSWORD": "your-phoenixd-password"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Restart your AI and start using tools
|
|
100
|
+
|
|
101
|
+
That's it. Your agent now has access to all Boltwork tools and will pay invoices automatically when it uses them.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Available tools
|
|
106
|
+
|
|
107
|
+
| Tool | What it does | Cost |
|
|
108
|
+
|------|-------------|------|
|
|
109
|
+
| `summarise_pdf` | Summarise a PDF from URL | 500 sats |
|
|
110
|
+
| `review_code` | Review code for bugs, security, quality | 2000 sats |
|
|
111
|
+
| `review_code_url` | Review code from GitHub/GitLab URL | 2000 sats |
|
|
112
|
+
| `summarise_webpage` | Summarise any web page | 100 sats |
|
|
113
|
+
| `extract_data` | Extract structured data from PDF | 200 sats |
|
|
114
|
+
| `translate` | Translate text or document to 24 languages | 150 sats |
|
|
115
|
+
| `extract_tables` | Extract tables from PDF as structured JSON | 300 sats |
|
|
116
|
+
| `compare_documents` | Diff two PDFs | 500 sats |
|
|
117
|
+
| `explain_code` | Explain code in plain English | 500 sats |
|
|
118
|
+
| `memory_store` | Store persistent key-value memory | 10 sats |
|
|
119
|
+
| `memory_retrieve` | Read stored memory | 5 sats |
|
|
120
|
+
| `memory_delete` | Delete a memory key | free |
|
|
121
|
+
| `run_workflow` | Chain services in a single pipeline | 1000 sats |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Example prompts
|
|
126
|
+
|
|
127
|
+
Once configured, just talk to your AI naturally:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
"Summarise this research paper: https://arxiv.org/pdf/2301.00000"
|
|
131
|
+
|
|
132
|
+
"Review the code at https://github.com/me/repo/blob/main/app.py"
|
|
133
|
+
|
|
134
|
+
"Translate this contract to Spanish: https://example.com/contract.pdf"
|
|
135
|
+
|
|
136
|
+
"Compare these two versions of our terms of service:
|
|
137
|
+
v1: https://example.com/tos-v1.pdf
|
|
138
|
+
v2: https://example.com/tos-v2.pdf"
|
|
139
|
+
|
|
140
|
+
"Remember that the last file I asked you to review was app.py
|
|
141
|
+
and the score was 7/10"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Your agent handles the payment automatically — you just see the result.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Workflow pipelines
|
|
149
|
+
|
|
150
|
+
Chain multiple services in one call with `run_workflow`:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
"Fetch the Bitcoin whitepaper, translate the summary to French,
|
|
154
|
+
and store the translation in my agent memory"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The `$from` syntax passes outputs between steps:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"steps": [
|
|
162
|
+
{"service": "pdf", "input": {"url": "https://bitcoin.org/bitcoin.pdf"}},
|
|
163
|
+
{"service": "translate", "input": {"text": {"$from": 0}, "target_language": "french"}}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Environment variables
|
|
171
|
+
|
|
172
|
+
| Variable | Required | Description |
|
|
173
|
+
|----------|----------|-------------|
|
|
174
|
+
| `NWC_CONNECTION_STRING` | One of these two | Nostr Wallet Connect string from Alby/Mutiny |
|
|
175
|
+
| `PHOENIXD_URL` | One of these two | Phoenixd base URL e.g. `http://localhost:9740` |
|
|
176
|
+
| `PHOENIXD_PASSWORD` | With Phoenixd | Phoenixd HTTP Basic auth password |
|
|
177
|
+
| `BOLTWORK_GATEWAY` | Optional | Override gateway URL (default: `https://parsebit-lnd.fly.dev`) |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Pricing
|
|
182
|
+
|
|
183
|
+
All prices are in satoshis (sats). At current rates, 1000 sats ≈ $0.60 — but this varies with Bitcoin's price.
|
|
184
|
+
|
|
185
|
+
There is no subscription, no monthly fee, no minimum spend. You pay exactly for what your agent uses, nothing more.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## How L402 works
|
|
190
|
+
|
|
191
|
+
1. Agent calls a Boltwork tool
|
|
192
|
+
2. `boltwork-mcp` sends the request to `parsebit-lnd.fly.dev`
|
|
193
|
+
3. Receives HTTP 402 with a Lightning invoice (e.g. 500 sats for PDF summarisation)
|
|
194
|
+
4. Pays the invoice via your configured wallet (NWC or Phoenixd)
|
|
195
|
+
5. Retries the request with the payment proof
|
|
196
|
+
6. Returns the result to your agent
|
|
197
|
+
|
|
198
|
+
The whole flow takes 1-3 seconds. Your agent sees only the final result.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Try before you pay
|
|
203
|
+
|
|
204
|
+
Boltwork has free trial endpoints — no Lightning wallet needed:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
curl -X POST https://parsebit.fly.dev/trial/review \
|
|
208
|
+
-H "Content-Type: application/json" \
|
|
209
|
+
-d '{"code": "def add(a, b): return a + b"}'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Links
|
|
215
|
+
|
|
216
|
+
- [Boltwork API](https://parsebit.fly.dev) — the service this MCP server wraps
|
|
217
|
+
- [Agent spec](https://parsebit.fly.dev/agent-spec.md) — full API reference
|
|
218
|
+
- [L402 Index](https://402index.io) — directory of L402 services
|
|
219
|
+
- [MCP documentation](https://modelcontextprotocol.io) — learn about MCP
|
|
220
|
+
- [Cracked Minds](https://crackedminds.co.uk) — the team behind Boltwork
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "boltwork-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for Boltwork — AI services via Bitcoin Lightning. PDF summarisation, code review, translation, and more."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Cracked Minds", email = "hello@crackedminds.co.uk" }
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"mcp", "model-context-protocol", "lightning", "bitcoin", "l402",
|
|
16
|
+
"ai", "pdf", "summarisation", "code-review", "translation", "agents"
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.11"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"mcp>=1.0.0",
|
|
31
|
+
"httpx>=0.27.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
nwc = [
|
|
36
|
+
"pynostr>=0.6.0",
|
|
37
|
+
"websockets>=12.0",
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=8.0",
|
|
41
|
+
"pytest-asyncio>=0.23",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/Squidboy30/boltwork-mcp"
|
|
46
|
+
Repository = "https://github.com/Squidboy30/boltwork-mcp"
|
|
47
|
+
Issues = "https://github.com/Squidboy30/boltwork-mcp/issues"
|
|
48
|
+
API = "https://parsebit.fly.dev"
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
boltwork-mcp = "boltwork_mcp.server:run"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/boltwork_mcp"]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.sdist]
|
|
57
|
+
include = [
|
|
58
|
+
"src/",
|
|
59
|
+
"README.md",
|
|
60
|
+
"LICENSE",
|
|
61
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Boltwork MCP - L402 Payment Handler
|
|
3
|
+
=====================================
|
|
4
|
+
Handles the full L402 payment flow transparently.
|
|
5
|
+
|
|
6
|
+
Supported wallet backends:
|
|
7
|
+
- NWC (Nostr Wallet Connect) - works with Alby, Mutiny, etc.
|
|
8
|
+
- Phoenixd - self-hosted Lightning node
|
|
9
|
+
|
|
10
|
+
Flow:
|
|
11
|
+
1. Request hits L402 gateway → 402 response
|
|
12
|
+
2. Extract macaroon + invoice from WWW-Authenticate header
|
|
13
|
+
3. Pay invoice via configured wallet backend
|
|
14
|
+
4. Return Authorization header value for retry
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import json
|
|
20
|
+
import asyncio
|
|
21
|
+
import httpx
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# WWW-Authenticate header parsing
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def parse_402(www_authenticate: str) -> tuple[str, str]:
|
|
30
|
+
"""
|
|
31
|
+
Parse the WWW-Authenticate header from a 402 response.
|
|
32
|
+
Returns (macaroon, invoice).
|
|
33
|
+
|
|
34
|
+
Header format:
|
|
35
|
+
L402 macaroon="<base64>", invoice="<bolt11>"
|
|
36
|
+
"""
|
|
37
|
+
macaroon_match = re.search(r'macaroon="([^"]+)"', www_authenticate)
|
|
38
|
+
invoice_match = re.search(r'invoice="([^"]+)"', www_authenticate)
|
|
39
|
+
|
|
40
|
+
if not macaroon_match or not invoice_match:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Could not parse L402 header: {www_authenticate[:200]}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return macaroon_match.group(1), invoice_match.group(1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# NWC payment backend
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
async def pay_invoice_nwc(invoice: str, nwc_string: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Pay a Lightning invoice via Nostr Wallet Connect.
|
|
55
|
+
Returns the payment preimage as a hex string.
|
|
56
|
+
|
|
57
|
+
NWC connection string format:
|
|
58
|
+
nostr+walletconnect://<pubkey>?relay=<relay_url>&secret=<secret>
|
|
59
|
+
|
|
60
|
+
Uses the pynostr-based NWC flow via a lightweight websocket exchange.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
from pynostr.key import PrivateKey
|
|
64
|
+
from pynostr.encrypted_dm import EncryptedDirectMessage
|
|
65
|
+
import websockets
|
|
66
|
+
import uuid
|
|
67
|
+
import time
|
|
68
|
+
except ImportError:
|
|
69
|
+
raise ImportError(
|
|
70
|
+
"NWC requires: pip install pynostr websockets\n"
|
|
71
|
+
"Or install the full package: pip install boltwork-mcp[nwc]"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Parse NWC connection string
|
|
75
|
+
# nostr+walletconnect://<wallet_pubkey>?relay=<url>&secret=<hex_secret>
|
|
76
|
+
match = re.match(
|
|
77
|
+
r"nostr\+walletconnect://([0-9a-fA-F]+)\?.*relay=([^&]+).*secret=([0-9a-fA-F]+)",
|
|
78
|
+
nwc_string
|
|
79
|
+
)
|
|
80
|
+
if not match:
|
|
81
|
+
raise ValueError("Invalid NWC connection string format")
|
|
82
|
+
|
|
83
|
+
wallet_pubkey_hex = match.group(1)
|
|
84
|
+
relay_url = match.group(2).rstrip("/")
|
|
85
|
+
secret_hex = match.group(3)
|
|
86
|
+
|
|
87
|
+
client_privkey = PrivateKey(bytes.fromhex(secret_hex))
|
|
88
|
+
client_pubkey = client_privkey.public_key.hex()
|
|
89
|
+
|
|
90
|
+
# Build pay_invoice request
|
|
91
|
+
request_id = str(uuid.uuid4())
|
|
92
|
+
payload = json.dumps({
|
|
93
|
+
"id": request_id,
|
|
94
|
+
"method": "pay_invoice",
|
|
95
|
+
"params": {"invoice": invoice},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
# Encrypt the request to the wallet pubkey
|
|
99
|
+
dm = EncryptedDirectMessage(
|
|
100
|
+
recipient_pubkey=wallet_pubkey_hex,
|
|
101
|
+
cleartext_content=payload,
|
|
102
|
+
)
|
|
103
|
+
dm.encrypt(client_privkey.hex())
|
|
104
|
+
|
|
105
|
+
event = dm.to_event()
|
|
106
|
+
event.sign(client_privkey.hex())
|
|
107
|
+
|
|
108
|
+
timeout = 30.0
|
|
109
|
+
deadline = asyncio.get_event_loop().time() + timeout
|
|
110
|
+
preimage = None
|
|
111
|
+
|
|
112
|
+
async with websockets.connect(relay_url) as ws:
|
|
113
|
+
# Subscribe to responses addressed to us from the wallet
|
|
114
|
+
sub_id = str(uuid.uuid4())[:8]
|
|
115
|
+
sub_msg = json.dumps([
|
|
116
|
+
"REQ", sub_id,
|
|
117
|
+
{"kinds": [23195], "#p": [client_pubkey], "since": int(time.time()) - 5}
|
|
118
|
+
])
|
|
119
|
+
await ws.send(sub_msg)
|
|
120
|
+
|
|
121
|
+
# Send the payment request
|
|
122
|
+
await ws.send(json.dumps(["EVENT", event.to_dict()]))
|
|
123
|
+
|
|
124
|
+
# Wait for response
|
|
125
|
+
while asyncio.get_event_loop().time() < deadline:
|
|
126
|
+
try:
|
|
127
|
+
raw = await asyncio.wait_for(ws.recv(), timeout=5.0)
|
|
128
|
+
msg = json.loads(raw)
|
|
129
|
+
if msg[0] == "EVENT" and msg[1] == sub_id:
|
|
130
|
+
ev = msg[2]
|
|
131
|
+
# Decrypt response
|
|
132
|
+
dm_resp = EncryptedDirectMessage.from_event_dict(ev)
|
|
133
|
+
dm_resp.decrypt(client_privkey.hex(), public_key_hex=wallet_pubkey_hex)
|
|
134
|
+
resp = json.loads(dm_resp.cleartext_content)
|
|
135
|
+
if resp.get("result_type") == "pay_invoice":
|
|
136
|
+
if "error" in resp:
|
|
137
|
+
raise RuntimeError(f"NWC payment failed: {resp['error']}")
|
|
138
|
+
preimage = resp["result"]["preimage"]
|
|
139
|
+
break
|
|
140
|
+
except asyncio.TimeoutError:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if not preimage:
|
|
144
|
+
raise TimeoutError("NWC payment timed out after 30s")
|
|
145
|
+
|
|
146
|
+
return preimage
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Phoenixd payment backend
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
async def pay_invoice_phoenixd(invoice: str, phoenixd_url: str, phoenixd_password: str) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Pay a Lightning invoice via Phoenixd REST API.
|
|
156
|
+
Returns the payment preimage as a hex string.
|
|
157
|
+
|
|
158
|
+
phoenixd_url: e.g. http://localhost:9740
|
|
159
|
+
phoenixd_password: the HTTP Basic auth password from phoenixd config
|
|
160
|
+
"""
|
|
161
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
162
|
+
response = await client.post(
|
|
163
|
+
f"{phoenixd_url}/payinvoice",
|
|
164
|
+
data={"invoice": invoice},
|
|
165
|
+
auth=("", phoenixd_password),
|
|
166
|
+
)
|
|
167
|
+
if response.status_code != 200:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"Phoenixd payment failed: HTTP {response.status_code} — {response.text[:200]}"
|
|
170
|
+
)
|
|
171
|
+
data = response.json()
|
|
172
|
+
if "preimage" not in data:
|
|
173
|
+
raise RuntimeError(f"Phoenixd response missing preimage: {data}")
|
|
174
|
+
return data["preimage"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Main payment dispatcher
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
async def pay_invoice(invoice: str) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Pay a Lightning invoice using the configured wallet backend.
|
|
184
|
+
Returns the preimage as a hex string.
|
|
185
|
+
|
|
186
|
+
Reads configuration from environment variables:
|
|
187
|
+
NWC_CONNECTION_STRING — use NWC backend
|
|
188
|
+
PHOENIXD_URL — use Phoenixd backend (also needs PHOENIXD_PASSWORD)
|
|
189
|
+
PHOENIXD_PASSWORD — Phoenixd HTTP Basic auth password
|
|
190
|
+
"""
|
|
191
|
+
nwc_string = os.environ.get("NWC_CONNECTION_STRING", "").strip()
|
|
192
|
+
phoenixd_url = os.environ.get("PHOENIXD_URL", "").strip()
|
|
193
|
+
phoenixd_password = os.environ.get("PHOENIXD_PASSWORD", "").strip()
|
|
194
|
+
|
|
195
|
+
if nwc_string:
|
|
196
|
+
return await pay_invoice_nwc(invoice, nwc_string)
|
|
197
|
+
elif phoenixd_url and phoenixd_password:
|
|
198
|
+
return await pay_invoice_phoenixd(invoice, phoenixd_url, phoenixd_password)
|
|
199
|
+
else:
|
|
200
|
+
raise RuntimeError(
|
|
201
|
+
"No wallet configured. Set one of:\n"
|
|
202
|
+
" NWC_CONNECTION_STRING=nostr+walletconnect://...\n"
|
|
203
|
+
" PHOENIXD_URL=http://localhost:9740 + PHOENIXD_PASSWORD=..."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Full L402 request helper — use this from tool handlers
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
async def l402_request(
|
|
212
|
+
method: str,
|
|
213
|
+
url: str,
|
|
214
|
+
gateway_url: str,
|
|
215
|
+
json_body: Optional[dict] = None,
|
|
216
|
+
files: Optional[dict] = None,
|
|
217
|
+
) -> dict:
|
|
218
|
+
"""
|
|
219
|
+
Make an L402-authenticated request.
|
|
220
|
+
|
|
221
|
+
1. Sends request to gateway_url (the L402 gateway)
|
|
222
|
+
2. If 402, pays the invoice and retries with credentials
|
|
223
|
+
3. Returns the parsed JSON response
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
method: HTTP method ("GET", "POST")
|
|
227
|
+
url: The logical endpoint path (e.g. "/summarise/url")
|
|
228
|
+
gateway_url: Full URL to the L402 gateway endpoint
|
|
229
|
+
json_body: JSON request body (for POST)
|
|
230
|
+
files: Multipart files (for file upload)
|
|
231
|
+
"""
|
|
232
|
+
async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
|
|
233
|
+
|
|
234
|
+
# First attempt — expect either 200 or 402
|
|
235
|
+
kwargs = {}
|
|
236
|
+
if json_body is not None:
|
|
237
|
+
kwargs["json"] = json_body
|
|
238
|
+
if files is not None:
|
|
239
|
+
kwargs["files"] = files
|
|
240
|
+
|
|
241
|
+
response = await client.request(method, gateway_url, **kwargs)
|
|
242
|
+
|
|
243
|
+
if response.status_code == 200:
|
|
244
|
+
return response.json()
|
|
245
|
+
|
|
246
|
+
if response.status_code != 402:
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
f"Unexpected HTTP {response.status_code} from {gateway_url}: "
|
|
249
|
+
f"{response.text[:300]}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Parse 402 and pay
|
|
253
|
+
www_auth = response.headers.get("WWW-Authenticate", "")
|
|
254
|
+
if not www_auth:
|
|
255
|
+
raise RuntimeError("Got 402 but no WWW-Authenticate header")
|
|
256
|
+
|
|
257
|
+
macaroon, invoice = parse_402(www_auth)
|
|
258
|
+
preimage = await pay_invoice(invoice)
|
|
259
|
+
|
|
260
|
+
# Retry with L402 credentials
|
|
261
|
+
auth_header = f"L402 {macaroon}:{preimage}"
|
|
262
|
+
response2 = await client.request(
|
|
263
|
+
method, gateway_url,
|
|
264
|
+
headers={"Authorization": auth_header},
|
|
265
|
+
**kwargs,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if response2.status_code != 200:
|
|
269
|
+
raise RuntimeError(
|
|
270
|
+
f"L402 retry failed: HTTP {response2.status_code} — "
|
|
271
|
+
f"{response2.text[:300]}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return response2.json()
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Boltwork MCP Server
|
|
3
|
+
====================
|
|
4
|
+
Exposes all Boltwork AI services as MCP tools.
|
|
5
|
+
Handles L402 Lightning payments transparently.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
pip install boltwork-mcp
|
|
9
|
+
|
|
10
|
+
Then add to claude_desktop_config.json or .cursor/mcp.json:
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"boltwork": {
|
|
14
|
+
"command": "uvx",
|
|
15
|
+
"args": ["boltwork-mcp"],
|
|
16
|
+
"env": {
|
|
17
|
+
"NWC_CONNECTION_STRING": "nostr+walletconnect://..."
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import asyncio
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from mcp.server import Server
|
|
29
|
+
from mcp.server.stdio import stdio_server
|
|
30
|
+
from mcp import types
|
|
31
|
+
|
|
32
|
+
from boltwork_mcp.payment import l402_request
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Configuration
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
GATEWAY = os.environ.get("BOLTWORK_GATEWAY", "https://parsebit-lnd.fly.dev")
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# MCP Server
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
app = Server("boltwork")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.list_tools()
|
|
48
|
+
async def list_tools() -> list[types.Tool]:
|
|
49
|
+
return [
|
|
50
|
+
|
|
51
|
+
# ── Summarisation ────────────────────────────────────────────────
|
|
52
|
+
types.Tool(
|
|
53
|
+
name="summarise_pdf",
|
|
54
|
+
description=(
|
|
55
|
+
"Summarise a PDF document from a URL. Returns a structured summary "
|
|
56
|
+
"including title, key points, sentiment, topics, and word count. "
|
|
57
|
+
"Costs 500 sats via Lightning. "
|
|
58
|
+
"Use for: research papers, reports, contracts, any PDF."
|
|
59
|
+
),
|
|
60
|
+
inputSchema={
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"url": {"type": "string", "description": "URL of the PDF to summarise"},
|
|
64
|
+
"max_pages": {"type": "integer", "description": "Max pages to process (default 20)", "default": 20},
|
|
65
|
+
},
|
|
66
|
+
"required": ["url"],
|
|
67
|
+
},
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
# ── Code Review ──────────────────────────────────────────────────
|
|
71
|
+
types.Tool(
|
|
72
|
+
name="review_code",
|
|
73
|
+
description=(
|
|
74
|
+
"Review source code and return a structured analysis covering bugs, "
|
|
75
|
+
"security issues, code quality, strengths, and recommended actions. "
|
|
76
|
+
"Returns an overall score 1-10. "
|
|
77
|
+
"Costs 2000 sats via Lightning. "
|
|
78
|
+
"Supports: Python, JavaScript, TypeScript, Go, Rust, Java, C/C++, "
|
|
79
|
+
"C#, Ruby, PHP, Swift, Kotlin, Scala, Shell, SQL, Terraform, and more."
|
|
80
|
+
),
|
|
81
|
+
inputSchema={
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"code": {"type": "string", "description": "The source code to review"},
|
|
85
|
+
"language": {"type": "string", "description": "Language hint (optional, auto-detected if omitted)"},
|
|
86
|
+
"filename": {"type": "string", "description": "Filename hint for language detection (optional)"},
|
|
87
|
+
},
|
|
88
|
+
"required": ["code"],
|
|
89
|
+
},
|
|
90
|
+
),
|
|
91
|
+
|
|
92
|
+
types.Tool(
|
|
93
|
+
name="review_code_url",
|
|
94
|
+
description=(
|
|
95
|
+
"Review code fetched from a URL. Supports GitHub and GitLab blob URLs "
|
|
96
|
+
"(auto-converted to raw). "
|
|
97
|
+
"Costs 2000 sats via Lightning."
|
|
98
|
+
),
|
|
99
|
+
inputSchema={
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"url": {"type": "string", "description": "URL to the code file (GitHub/GitLab/raw)"},
|
|
103
|
+
"language": {"type": "string", "description": "Language hint (optional)"},
|
|
104
|
+
},
|
|
105
|
+
"required": ["url"],
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
|
|
109
|
+
# ── Web Extraction ───────────────────────────────────────────────
|
|
110
|
+
types.Tool(
|
|
111
|
+
name="summarise_webpage",
|
|
112
|
+
description=(
|
|
113
|
+
"Summarise any web page by URL. Returns title, summary, key points, "
|
|
114
|
+
"content type, sentiment, and topics. "
|
|
115
|
+
"Costs 100 sats via Lightning. "
|
|
116
|
+
"Use for: articles, blogs, documentation, product pages, news."
|
|
117
|
+
),
|
|
118
|
+
inputSchema={
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"url": {"type": "string", "description": "URL of the web page to summarise"},
|
|
122
|
+
},
|
|
123
|
+
"required": ["url"],
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
|
|
127
|
+
# ── Data Extraction ──────────────────────────────────────────────
|
|
128
|
+
types.Tool(
|
|
129
|
+
name="extract_data",
|
|
130
|
+
description=(
|
|
131
|
+
"Extract structured data from a PDF document. Returns document type, "
|
|
132
|
+
"dates, parties, amounts, line items, and reference numbers. "
|
|
133
|
+
"Costs 200 sats via Lightning. "
|
|
134
|
+
"Use for: invoices, contracts, receipts, forms."
|
|
135
|
+
),
|
|
136
|
+
inputSchema={
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"url": {"type": "string", "description": "URL of the PDF"},
|
|
140
|
+
"max_pages": {"type": "integer", "description": "Max pages to process (default 20)", "default": 20},
|
|
141
|
+
},
|
|
142
|
+
"required": ["url"],
|
|
143
|
+
},
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
# ── Translation ──────────────────────────────────────────────────
|
|
147
|
+
types.Tool(
|
|
148
|
+
name="translate",
|
|
149
|
+
description=(
|
|
150
|
+
"Translate text or a document URL to any of 24 supported languages. "
|
|
151
|
+
"Detects source language automatically. "
|
|
152
|
+
"Costs 150 sats via Lightning. "
|
|
153
|
+
"Supported languages: Spanish, French, German, Italian, Portuguese, "
|
|
154
|
+
"Dutch, Russian, Japanese, Chinese, Korean, Arabic, Hindi, Turkish, "
|
|
155
|
+
"Polish, Swedish, Danish, Norwegian, Finnish, Czech, Romanian, "
|
|
156
|
+
"Hungarian, Greek, Hebrew, Thai."
|
|
157
|
+
),
|
|
158
|
+
inputSchema={
|
|
159
|
+
"type": "object",
|
|
160
|
+
"properties": {
|
|
161
|
+
"text": {"type": "string", "description": "Text to translate (provide either text or url)"},
|
|
162
|
+
"url": {"type": "string", "description": "URL of document to translate (provide either text or url)"},
|
|
163
|
+
"target_language": {"type": "string", "description": "Target language (e.g. 'spanish', 'french', 'japanese')"},
|
|
164
|
+
},
|
|
165
|
+
"required": ["target_language"],
|
|
166
|
+
},
|
|
167
|
+
),
|
|
168
|
+
|
|
169
|
+
# ── Analysis ─────────────────────────────────────────────────────
|
|
170
|
+
types.Tool(
|
|
171
|
+
name="extract_tables",
|
|
172
|
+
description=(
|
|
173
|
+
"Extract all tables from a PDF as structured JSON. "
|
|
174
|
+
"Returns table count, headers, rows, and a summary. "
|
|
175
|
+
"Costs 300 sats via Lightning. "
|
|
176
|
+
"Use for: financial reports, research data, invoices with line items."
|
|
177
|
+
),
|
|
178
|
+
inputSchema={
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"url": {"type": "string", "description": "URL of the PDF"},
|
|
182
|
+
"max_pages": {"type": "integer", "description": "Max pages to process (default 20)", "default": 20},
|
|
183
|
+
},
|
|
184
|
+
"required": ["url"],
|
|
185
|
+
},
|
|
186
|
+
),
|
|
187
|
+
|
|
188
|
+
types.Tool(
|
|
189
|
+
name="compare_documents",
|
|
190
|
+
description=(
|
|
191
|
+
"Compare two PDF documents and return a structured diff. "
|
|
192
|
+
"Identifies additions, removals, modifications, and overall similarity. "
|
|
193
|
+
"Costs 500 sats via Lightning. "
|
|
194
|
+
"Use for: contract versions, policy updates, paper revisions."
|
|
195
|
+
),
|
|
196
|
+
inputSchema={
|
|
197
|
+
"type": "object",
|
|
198
|
+
"properties": {
|
|
199
|
+
"url_a": {"type": "string", "description": "URL of the first PDF (original)"},
|
|
200
|
+
"url_b": {"type": "string", "description": "URL of the second PDF (revised)"},
|
|
201
|
+
"max_pages": {"type": "integer", "description": "Max pages per document (default 20)", "default": 20},
|
|
202
|
+
},
|
|
203
|
+
"required": ["url_a", "url_b"],
|
|
204
|
+
},
|
|
205
|
+
),
|
|
206
|
+
|
|
207
|
+
types.Tool(
|
|
208
|
+
name="explain_code",
|
|
209
|
+
description=(
|
|
210
|
+
"Explain what code does in plain English. Unlike code review (which finds "
|
|
211
|
+
"problems), this explains purpose and behaviour to a non-programmer. "
|
|
212
|
+
"Costs 500 sats via Lightning. "
|
|
213
|
+
"Use for: understanding inherited code, due diligence, onboarding docs."
|
|
214
|
+
),
|
|
215
|
+
inputSchema={
|
|
216
|
+
"type": "object",
|
|
217
|
+
"properties": {
|
|
218
|
+
"code": {"type": "string", "description": "Source code to explain (provide either code or url)"},
|
|
219
|
+
"url": {"type": "string", "description": "URL of code file to explain (provide either code or url)"},
|
|
220
|
+
"language": {"type": "string", "description": "Language hint (optional)"},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
),
|
|
224
|
+
|
|
225
|
+
# ── Agent Memory ─────────────────────────────────────────────────
|
|
226
|
+
types.Tool(
|
|
227
|
+
name="memory_store",
|
|
228
|
+
description=(
|
|
229
|
+
"Store persistent key-value memory for your agent. "
|
|
230
|
+
"Data persists across sessions, keyed by agent_id. "
|
|
231
|
+
"Up to 100 keys per agent, 10 keys per write call. "
|
|
232
|
+
"Costs 10 sats via Lightning."
|
|
233
|
+
),
|
|
234
|
+
inputSchema={
|
|
235
|
+
"type": "object",
|
|
236
|
+
"properties": {
|
|
237
|
+
"agent_id": {"type": "string", "description": "Stable identifier for your agent"},
|
|
238
|
+
"entries": {"type": "object", "description": "Key-value pairs to store (max 10 keys, values must be JSON-serialisable)"},
|
|
239
|
+
},
|
|
240
|
+
"required": ["agent_id", "entries"],
|
|
241
|
+
},
|
|
242
|
+
),
|
|
243
|
+
|
|
244
|
+
types.Tool(
|
|
245
|
+
name="memory_retrieve",
|
|
246
|
+
description=(
|
|
247
|
+
"Retrieve stored memory for your agent. "
|
|
248
|
+
"Returns all keys or a specific subset. "
|
|
249
|
+
"Costs 5 sats via Lightning."
|
|
250
|
+
),
|
|
251
|
+
inputSchema={
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {
|
|
254
|
+
"agent_id": {"type": "string", "description": "Agent identifier"},
|
|
255
|
+
"keys": {"type": "array", "description": "Specific keys to fetch (omit to return all)", "items": {"type": "string"}},
|
|
256
|
+
},
|
|
257
|
+
"required": ["agent_id"],
|
|
258
|
+
},
|
|
259
|
+
),
|
|
260
|
+
|
|
261
|
+
types.Tool(
|
|
262
|
+
name="memory_delete",
|
|
263
|
+
description="Delete a single key from your agent's memory store. Free.",
|
|
264
|
+
inputSchema={
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": {
|
|
267
|
+
"agent_id": {"type": "string", "description": "Agent identifier"},
|
|
268
|
+
"key": {"type": "string", "description": "Key to delete"},
|
|
269
|
+
},
|
|
270
|
+
"required": ["agent_id", "key"],
|
|
271
|
+
},
|
|
272
|
+
),
|
|
273
|
+
|
|
274
|
+
# ── Workflow Pipelines ────────────────────────────────────────────
|
|
275
|
+
types.Tool(
|
|
276
|
+
name="run_workflow",
|
|
277
|
+
description=(
|
|
278
|
+
"Chain multiple Boltwork services in a single call. "
|
|
279
|
+
"Pay once, describe a pipeline of up to 5 steps, get the final result "
|
|
280
|
+
"plus all intermediate outputs. "
|
|
281
|
+
"Use {\"$from\": N} in any input value to pass the primary output of "
|
|
282
|
+
"step N into the current step. "
|
|
283
|
+
"Costs 1000 sats via Lightning. "
|
|
284
|
+
"Supported services: webpage, pdf, summarise, translate, data, "
|
|
285
|
+
"tables, explain, review, compare. "
|
|
286
|
+
"Example: fetch a webpage, translate the summary to French — "
|
|
287
|
+
"steps: [{service: webpage, input: {url: ...}}, "
|
|
288
|
+
"{service: translate, input: {text: {$from: 0}, target_language: french}}]"
|
|
289
|
+
),
|
|
290
|
+
inputSchema={
|
|
291
|
+
"type": "object",
|
|
292
|
+
"properties": {
|
|
293
|
+
"steps": {
|
|
294
|
+
"type": "array",
|
|
295
|
+
"description": "Pipeline steps, each with 'service' and 'input'",
|
|
296
|
+
"items": {
|
|
297
|
+
"type": "object",
|
|
298
|
+
"properties": {
|
|
299
|
+
"service": {"type": "string"},
|
|
300
|
+
"input": {"type": "object"},
|
|
301
|
+
},
|
|
302
|
+
"required": ["service", "input"],
|
|
303
|
+
},
|
|
304
|
+
"maxItems": 5,
|
|
305
|
+
},
|
|
306
|
+
"label": {"type": "string", "description": "Optional label for this pipeline"},
|
|
307
|
+
},
|
|
308
|
+
"required": ["steps"],
|
|
309
|
+
},
|
|
310
|
+
),
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
# Tool call handlers
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
@app.call_tool()
|
|
319
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
|
|
320
|
+
try:
|
|
321
|
+
result = await _dispatch(name, arguments)
|
|
322
|
+
import json
|
|
323
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
324
|
+
except Exception as e:
|
|
325
|
+
return [types.TextContent(type="text", text=f"Error: {e}")]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def _dispatch(name: str, args: dict) -> dict:
|
|
329
|
+
match name:
|
|
330
|
+
|
|
331
|
+
case "summarise_pdf":
|
|
332
|
+
return await l402_request(
|
|
333
|
+
"POST", "/summarise/url",
|
|
334
|
+
f"{GATEWAY}/summarise/url",
|
|
335
|
+
json_body={"url": args["url"], "max_pages": args.get("max_pages", 20)},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
case "review_code":
|
|
339
|
+
return await l402_request(
|
|
340
|
+
"POST", "/review/code",
|
|
341
|
+
f"{GATEWAY}/review/code",
|
|
342
|
+
json_body={
|
|
343
|
+
"code": args["code"],
|
|
344
|
+
"language": args.get("language"),
|
|
345
|
+
"filename": args.get("filename"),
|
|
346
|
+
},
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
case "review_code_url":
|
|
350
|
+
return await l402_request(
|
|
351
|
+
"POST", "/review/url",
|
|
352
|
+
f"{GATEWAY}/review/url",
|
|
353
|
+
json_body={"url": args["url"], "language": args.get("language")},
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
case "summarise_webpage":
|
|
357
|
+
return await l402_request(
|
|
358
|
+
"POST", "/extract/webpage",
|
|
359
|
+
f"{GATEWAY}/extract/webpage",
|
|
360
|
+
json_body={"url": args["url"]},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
case "extract_data":
|
|
364
|
+
return await l402_request(
|
|
365
|
+
"POST", "/extract/data",
|
|
366
|
+
f"{GATEWAY}/extract/data",
|
|
367
|
+
json_body={"url": args["url"], "max_pages": args.get("max_pages", 20)},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
case "translate":
|
|
371
|
+
body = {"target_language": args["target_language"]}
|
|
372
|
+
if "text" in args:
|
|
373
|
+
body["text"] = args["text"]
|
|
374
|
+
if "url" in args:
|
|
375
|
+
body["url"] = args["url"]
|
|
376
|
+
return await l402_request(
|
|
377
|
+
"POST", "/translate",
|
|
378
|
+
f"{GATEWAY}/translate",
|
|
379
|
+
json_body=body,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
case "extract_tables":
|
|
383
|
+
return await l402_request(
|
|
384
|
+
"POST", "/analyse/tables",
|
|
385
|
+
f"{GATEWAY}/analyse/tables",
|
|
386
|
+
json_body={"url": args["url"], "max_pages": args.get("max_pages", 20)},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
case "compare_documents":
|
|
390
|
+
return await l402_request(
|
|
391
|
+
"POST", "/analyse/compare",
|
|
392
|
+
f"{GATEWAY}/analyse/compare",
|
|
393
|
+
json_body={
|
|
394
|
+
"url_a": args["url_a"],
|
|
395
|
+
"url_b": args["url_b"],
|
|
396
|
+
"max_pages": args.get("max_pages", 20),
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
case "explain_code":
|
|
401
|
+
body = {}
|
|
402
|
+
if "code" in args:
|
|
403
|
+
body["code"] = args["code"]
|
|
404
|
+
if "url" in args:
|
|
405
|
+
body["url"] = args["url"]
|
|
406
|
+
if "language" in args:
|
|
407
|
+
body["language"] = args["language"]
|
|
408
|
+
return await l402_request(
|
|
409
|
+
"POST", "/analyse/explain",
|
|
410
|
+
f"{GATEWAY}/analyse/explain",
|
|
411
|
+
json_body=body,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
case "memory_store":
|
|
415
|
+
return await l402_request(
|
|
416
|
+
"POST", "/memory/store",
|
|
417
|
+
f"{GATEWAY}/memory/store",
|
|
418
|
+
json_body={"agent_id": args["agent_id"], "entries": args["entries"]},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
case "memory_retrieve":
|
|
422
|
+
body = {"agent_id": args["agent_id"]}
|
|
423
|
+
if "keys" in args:
|
|
424
|
+
body["keys"] = args["keys"]
|
|
425
|
+
return await l402_request(
|
|
426
|
+
"POST", "/memory/retrieve",
|
|
427
|
+
f"{GATEWAY}/memory/retrieve",
|
|
428
|
+
json_body=body,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
case "memory_delete":
|
|
432
|
+
return await l402_request(
|
|
433
|
+
"POST", "/memory/delete",
|
|
434
|
+
f"{GATEWAY}/memory/delete",
|
|
435
|
+
json_body={"agent_id": args["agent_id"], "key": args["key"]},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
case "run_workflow":
|
|
439
|
+
return await l402_request(
|
|
440
|
+
"POST", "/workflow/run",
|
|
441
|
+
f"{GATEWAY}/workflow/run",
|
|
442
|
+
json_body={"steps": args["steps"], "label": args.get("label")},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
case _:
|
|
446
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
# Entry point
|
|
451
|
+
# ---------------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
async def main():
|
|
454
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
455
|
+
await app.run(
|
|
456
|
+
read_stream,
|
|
457
|
+
write_stream,
|
|
458
|
+
app.create_initialization_options(),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def run():
|
|
463
|
+
asyncio.run(main())
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
if __name__ == "__main__":
|
|
467
|
+
run()
|