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
+ [![PyPI](https://img.shields.io/pypi/v/boltwork-mcp)](https://pypi.org/project/boltwork-mcp/)
38
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+ [![API](https://img.shields.io/badge/API-parsebit.fly.dev-green)](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
+ [![PyPI](https://img.shields.io/pypi/v/boltwork-mcp)](https://pypi.org/project/boltwork-mcp/)
8
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
+ [![API](https://img.shields.io/badge/API-parsebit.fly.dev-green)](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
+ ]
@@ -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()