@blockrun/mcp 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -4
- package/dist/index.js +374 -322
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -66,6 +66,14 @@ Prompts and a worked example for these are in [`skills/image-prompting/SKILL.md`
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
+
## Prerequisites
|
|
70
|
+
|
|
71
|
+
- **Node.js ≥ 18** (`node -v`)
|
|
72
|
+
- **~$5 USDC** on Base or Solana (the server auto-creates a wallet on first run; see [Fund your wallet](#fund-your-wallet))
|
|
73
|
+
- **An MCP client**: Claude Code, Claude Desktop, Cursor, Windsurf, or ChatGPT Desktop
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
69
77
|
## Install
|
|
70
78
|
|
|
71
79
|
**Claude Code (recommended)**
|
|
@@ -93,6 +101,23 @@ ensures `-y` is passed to `npx`, not parsed by `claude mcp add`.
|
|
|
93
101
|
claude mcp add blockrun -s user --transport http https://mcp.blockrun.ai/mcp
|
|
94
102
|
```
|
|
95
103
|
|
|
104
|
+
**Cursor** — add to `~/.cursor/mcp.json` (macOS / Linux) or `%APPDATA%\Cursor\mcp.json` (Windows):
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"blockrun": {
|
|
109
|
+
"command": "npx",
|
|
110
|
+
"args": ["-y", "@blockrun/mcp@latest"]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Windsurf** — same JSON, in:
|
|
117
|
+
- macOS: `~/.codeium/windsurf/mcp_config.json`
|
|
118
|
+
- Linux: `~/.config/.codeium/windsurf/mcp_config.json`
|
|
119
|
+
- Windows: `%APPDATA%\Codeium\windsurf\mcp_config.json`
|
|
120
|
+
|
|
96
121
|
---
|
|
97
122
|
|
|
98
123
|
## Fund your wallet
|
|
@@ -118,7 +143,7 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
|
|
|
118
143
|
| `blockrun_music` | MiniMax music generation | per track |
|
|
119
144
|
| `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
|
|
120
145
|
| `blockrun_markets` | Polymarket (markets, candles, trades, orderbooks, leaderboards, smart-wallet PnL/clusters, UMA oracle), Kalshi, Limitless, Opinion, Predict.Fun, dFlow, Binance Futures, cross-platform match + search | $0.001–0.005/query |
|
|
121
|
-
| `
|
|
146
|
+
| `blockrun_surf` | Surf (asksurf.ai) — 84 endpoints: CEX market data, on-chain SQL (13 chains, 80+ ClickHouse tables), 100M+ labeled wallets, Polymarket + Kalshi side-by-side, social mindshare, news, search, Surf-1.5 chat with citations | $0.001–0.02/call |
|
|
122
147
|
| `blockrun_exa` | Neural web search (Exa) — research, competitors, papers, URL content | $0.01/query |
|
|
123
148
|
| `blockrun_search` | Grok Live Search — web + news with citations | ~$0.025 per source |
|
|
124
149
|
| `blockrun_dex` | Live DEX prices via DexScreener | free |
|
|
@@ -127,28 +152,127 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
|
|
|
127
152
|
|
|
128
153
|
---
|
|
129
154
|
|
|
155
|
+
## Tips for effective LLMs
|
|
156
|
+
|
|
157
|
+
> **For LLMs and agents invoking BlockRun MCP tools:**
|
|
158
|
+
|
|
159
|
+
- **CRITICAL: When any `blockrun_*` tool returns a payment / balance / 402 error, call `blockrun_wallet` *first* to check status, then `action:"setup"` to get funding instructions.** Don't retry the failing tool blindly — the wallet is empty.
|
|
160
|
+
- **CRITICAL: For `blockrun_markets` and `blockrun_surf`, the 80+ endpoint catalog lives in `skills/markets/SKILL.md` and `skills/surf/SKILL.md`, NOT in the tool description.** Browse the skill before guessing endpoint paths.
|
|
161
|
+
- **CRITICAL: `blockrun_chat routing:"smart"` (ClawRouter) only works on Base wallets.** On Solana, pass `mode:` or `model:` to pick a model directly.
|
|
162
|
+
- **CRITICAL: `blockrun_music` and `blockrun_video` are payment-on-completion async.** Failures or client-side timeouts do NOT charge. Don't retry-loop them — they may take 60–180s.
|
|
163
|
+
- **CRITICAL: Before spawning child agents, allocate per-agent budget:** `blockrun_wallet action:"delegate" agent_id:"X" agent_limit:1.00`. Pass `agent_id:"X"` to every downstream `blockrun_*` call — the child is auto-blocked when the budget hits zero.
|
|
164
|
+
- **Free tier first for drafts**: `blockrun_chat mode:"free"` (NVIDIA), `blockrun_dex`, `blockrun_price` (crypto / FX / commodity), and `blockrun_models` are all $0. Use them to scaffold before paying for premium models.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Key Use Cases
|
|
169
|
+
|
|
170
|
+
What kinds of questions can Claude (or any LLM agent) answer once BlockRun MCP is installed:
|
|
171
|
+
|
|
172
|
+
1. **Price reads / market data**
|
|
173
|
+
> *"What's BTC trading at? Compare with last week's average."* → `blockrun_price` (free) or `blockrun_surf` path:`market/price`
|
|
174
|
+
|
|
175
|
+
2. **Prediction-market consensus**
|
|
176
|
+
> *"What's Polymarket's odds for the next Fed rate decision?"* → `blockrun_markets` path:`polymarket/events` + filter
|
|
177
|
+
|
|
178
|
+
3. **On-chain forensics**
|
|
179
|
+
> *"This wallet (0xabc...) — what's it labeled as? What does it hold? When did it whale up?"* → `blockrun_surf` paths:`wallet/labels/batch`, `wallet/detail`, `wallet/net-worth`
|
|
180
|
+
|
|
181
|
+
4. **Cited research with sources**
|
|
182
|
+
> *"Find the 5 most-cited papers on speculative decoding from the last 90 days. Summarize the dominant approach."* → `blockrun_exa` action:`search` then `contents`
|
|
183
|
+
|
|
184
|
+
5. **Image generation with on-image text**
|
|
185
|
+
> *"Generate a poster announcing GPT-5.5 on BlockRun, retro-futuristic, with the headline 'NOW LIVE'."* → `blockrun_image` + the `image-prompting` skill 5-section framework
|
|
186
|
+
|
|
187
|
+
6. **Voice phone-out**
|
|
188
|
+
> *"Call +1-415-555-... and confirm the appointment on Friday at 3pm."* → `blockrun_phone` path:`voice/call`, body: `{ to, task, from }` (provision `from` first via `phone/numbers/buy`), then poll `voice/call/{call_id}`
|
|
189
|
+
|
|
190
|
+
7. **Multi-agent research with budget cap**
|
|
191
|
+
> *"Spawn 3 research agents on competing L1 narratives. Cap each at $0.50."* → `blockrun_wallet delegate × 3` → children call `blockrun_chat` + `blockrun_exa` with their `agent_id`
|
|
192
|
+
|
|
193
|
+
8. **Cross-chain SQL forensics**
|
|
194
|
+
> *"Top 10 tokens by DEX volume on Base in the last 24h."* → `blockrun_surf` path:`onchain/sql`, body: `{ sql: "SELECT..." }`
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
130
198
|
## Why not just use the APIs directly?
|
|
131
199
|
|
|
132
200
|
| | Direct APIs | BlockRun |
|
|
133
201
|
|---|---|---|
|
|
134
202
|
| Exa | Sign up, $20/mo minimum | $0.01/call, no subscription |
|
|
135
203
|
| Polymarket | Undocumented, rate-limited | $0.001/call, clean JSON |
|
|
136
|
-
|
|
|
137
|
-
| Multiple sources |
|
|
204
|
+
| Surf (asksurf.ai) | Account + monthly plan | $0.001/call, no Surf account, 84 endpoints |
|
|
205
|
+
| Multiple sources | 3 accounts, 3 API keys, 3 billing pages | 1 wallet |
|
|
138
206
|
|
|
139
207
|
One wallet. All sources. No dashboards.
|
|
140
208
|
|
|
141
209
|
---
|
|
142
210
|
|
|
211
|
+
## When NOT to use BlockRun MCP
|
|
212
|
+
|
|
213
|
+
BlockRun shines when you want **unified billing + many sources + LLM-readable errors**. It is not the right fit for:
|
|
214
|
+
|
|
215
|
+
- **High-volume single-API workloads (≥10k calls/day to one source).** Direct subscriptions amortize better past the break-even point — Polymarket's free public API plus your own caching beats $0.001 × 10k/day if you don't need cross-source aggregation.
|
|
216
|
+
- **Compliance-sensitive flows that need a fiat invoice / audit trail.** BlockRun settles in USDC; receipts are on-chain (Basescan / Solscan) but are not tax invoices. For enterprise procurement, contract directly with the upstream provider.
|
|
217
|
+
- **Latency-critical sub-100ms reads.** Each x402 call adds ~200–500ms of payment-signing + settlement overhead vs. a direct authenticated request. For HFT-style flows, run your own infra.
|
|
218
|
+
- **You only need one source forever.** If you'll only ever call Polymarket, or only ever Exa, save the indirection — sign up upstream and skip the wallet.
|
|
219
|
+
|
|
220
|
+
Use BlockRun when you want pay-per-call for *exploration*, *aggregation*, or *agent-driven* workloads where you can't predict which source you'll reach for next.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
143
224
|
## Multi-agent budget delegation
|
|
144
225
|
|
|
145
226
|
Delegate a spending budget to a child agent with `agent_id`. The child is auto-blocked when the budget runs out — useful for autonomous agents that shouldn't run up unbounded costs.
|
|
146
227
|
|
|
147
228
|
---
|
|
148
229
|
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
- **`Insufficient balance` / HTTP 402 after retry** → Run `blockrun_wallet action:"setup"`. Send USDC on Base (or Solana — see [Environment Variables](#environment-variables)).
|
|
233
|
+
- **`Smart routing (ClawRouter) is not available on Solana`** → Pass `model:` or `mode:` explicitly to `blockrun_chat`, or switch back to Base by unsetting `SOLANA_WALLET_KEY` and removing `~/.blockrun/.solana-session`.
|
|
234
|
+
- **`claude mcp list` doesn't show `blockrun`** → Check `node -v` (must be ≥18). Clear the npx cache: `rm -rf ~/.npm/_npx`. Re-run the install command from above.
|
|
235
|
+
- **`fetch failed` / timeout when checking wallet balance** → Base RPC transient outage. The tool already falls through 3 public RPCs; retry after 30s. Persistent failures usually = local proxy / firewall blocking outbound RPC.
|
|
236
|
+
- **`ENOENT: ~/.blockrun/.session`** → Expected on first run. The server auto-creates the wallet; check stderr for the `WALLET_CREATED` line confirming the address.
|
|
237
|
+
- **`Video generation timed out` (5-min cap)** → Upstream Seedance / xAI queue congestion. **No charge** (payment-on-completion). Retry, or pick a faster model (`bytedance/seedance-1.5-pro`).
|
|
238
|
+
- **`Music generation timed out` (200s cap)** → Same pattern. **No charge**. Retry; if it persists, the upstream model is rate-limited — try off-peak.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Environment Variables
|
|
243
|
+
|
|
244
|
+
| Variable / File | Default | Effect |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `~/.blockrun/.session` | auto-created on first run | EVM private key (0x...). File exists → use Base. |
|
|
247
|
+
| `~/.blockrun/.solana-session` | not created | Solana private key. File exists → switch to Solana. |
|
|
248
|
+
| `SOLANA_WALLET_KEY` | unset | Env-var override of `.solana-session`. Set → use Solana. |
|
|
249
|
+
|
|
250
|
+
Chain selection priority (see `src/utils/wallet.ts:24`):
|
|
251
|
+
|
|
252
|
+
1. `SOLANA_WALLET_KEY` env var present → Solana
|
|
253
|
+
2. `~/.blockrun/.solana-session` exists → Solana
|
|
254
|
+
3. Otherwise → Base (`~/.blockrun/.session` auto-created)
|
|
255
|
+
|
|
256
|
+
**Switching chains:**
|
|
257
|
+
|
|
258
|
+
- Base → Solana: `export SOLANA_WALLET_KEY=...`, or `echo "<secret>" > ~/.blockrun/.solana-session`
|
|
259
|
+
- Solana → Base: `unset SOLANA_WALLET_KEY && rm ~/.blockrun/.solana-session` (the existing `.session` is reused, so it's the same Base wallet)
|
|
260
|
+
|
|
261
|
+
The server also runs a non-blocking npm registry check at startup and prints an `Update available` notice to stderr when a newer `@blockrun/mcp` version exists. Upgrade by re-running the install command — no manual `npm update` needed.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
149
265
|
## How it works
|
|
150
266
|
|
|
151
|
-
Pay-per-call via [x402](https://x402.org) micropayments in USDC
|
|
267
|
+
Pay-per-call via [x402](https://x402.org) micropayments in USDC. Your wallet lives at `~/.blockrun/.session` (Base) or `~/.blockrun/.solana-session` (Solana). The private key never leaves your machine.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Contributing
|
|
272
|
+
|
|
273
|
+
PRs welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, the tool-vs-skill design rule, and how to add a new partner API.
|
|
274
|
+
|
|
275
|
+
Issues: [github.com/blockrunai/blockrun-mcp/issues](https://github.com/blockrunai/blockrun-mcp/issues)
|
|
152
276
|
|
|
153
277
|
---
|
|
154
278
|
|
package/dist/index.js
CHANGED
|
@@ -6,38 +6,78 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
|
|
8
8
|
// src/utils/wallet.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
9
12
|
import {
|
|
10
13
|
LLMClient,
|
|
11
14
|
ImageClient,
|
|
12
15
|
PriceClient,
|
|
16
|
+
SolanaLLMClient,
|
|
13
17
|
getOrCreateWallet,
|
|
18
|
+
loadSolanaWallet,
|
|
14
19
|
getPaymentLinks,
|
|
15
20
|
formatWalletCreatedMessage,
|
|
16
|
-
formatNeedsFundingMessage
|
|
21
|
+
formatNeedsFundingMessage,
|
|
22
|
+
SOLANA_WALLET_FILE_PATH
|
|
17
23
|
} from "@blockrun/llm";
|
|
18
|
-
var
|
|
24
|
+
var BLOCKRUN_DIR = path.join(os.homedir(), ".blockrun");
|
|
25
|
+
var CHAIN_PREFERENCE_FILES = [
|
|
26
|
+
path.join(BLOCKRUN_DIR, ".chain"),
|
|
27
|
+
path.join(BLOCKRUN_DIR, "payment-chain")
|
|
28
|
+
];
|
|
29
|
+
var _evmClient = null;
|
|
19
30
|
var _imageClient = null;
|
|
20
31
|
var _priceClient = null;
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
var _evmWalletInfo = null;
|
|
33
|
+
var _solanaClient = null;
|
|
34
|
+
function readChainPreference() {
|
|
35
|
+
for (const file of CHAIN_PREFERENCE_FILES) {
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(file)) continue;
|
|
38
|
+
const value = fs.readFileSync(file, "utf-8").trim().toLowerCase();
|
|
39
|
+
if (value === "base" || value === "solana") return value;
|
|
40
|
+
} catch {
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
|
-
return
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function getChain() {
|
|
46
|
+
const preferred = readChainPreference();
|
|
47
|
+
if (preferred) return preferred;
|
|
48
|
+
if (process.env.SOLANA_WALLET_KEY) return "solana";
|
|
49
|
+
try {
|
|
50
|
+
if (fs.existsSync(SOLANA_WALLET_FILE_PATH)) return "solana";
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
return "base";
|
|
54
|
+
}
|
|
55
|
+
function ensureEvmWallet() {
|
|
56
|
+
if (!_evmWalletInfo) {
|
|
57
|
+
_evmWalletInfo = getOrCreateWallet();
|
|
58
|
+
if (_evmWalletInfo.isNew) {
|
|
59
|
+
console.error(formatWalletCreatedMessage(_evmWalletInfo.address));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return _evmWalletInfo;
|
|
30
63
|
}
|
|
31
64
|
function getOrCreateWalletKey() {
|
|
32
|
-
const info =
|
|
65
|
+
const info = ensureEvmWallet();
|
|
33
66
|
return info.privateKey;
|
|
34
67
|
}
|
|
35
68
|
function getClient() {
|
|
36
|
-
if (
|
|
69
|
+
if (getChain() === "solana") {
|
|
70
|
+
if (!_solanaClient) {
|
|
71
|
+
const privateKey = process.env.SOLANA_WALLET_KEY || loadSolanaWallet() || void 0;
|
|
72
|
+
_solanaClient = new SolanaLLMClient(privateKey ? { privateKey } : void 0);
|
|
73
|
+
}
|
|
74
|
+
return _solanaClient;
|
|
75
|
+
}
|
|
76
|
+
if (!_evmClient) {
|
|
37
77
|
const privateKey = getOrCreateWalletKey();
|
|
38
|
-
|
|
78
|
+
_evmClient = new LLMClient({ privateKey });
|
|
39
79
|
}
|
|
40
|
-
return
|
|
80
|
+
return _evmClient;
|
|
41
81
|
}
|
|
42
82
|
function getImageClient() {
|
|
43
83
|
if (!_imageClient) {
|
|
@@ -53,8 +93,21 @@ function getPriceClient() {
|
|
|
53
93
|
}
|
|
54
94
|
return _priceClient;
|
|
55
95
|
}
|
|
56
|
-
function getWalletInfo() {
|
|
57
|
-
|
|
96
|
+
async function getWalletInfo() {
|
|
97
|
+
if (getChain() === "solana") {
|
|
98
|
+
const client = getClient();
|
|
99
|
+
const address = await client.getWalletAddress();
|
|
100
|
+
return {
|
|
101
|
+
address,
|
|
102
|
+
network: "Solana",
|
|
103
|
+
chainId: null,
|
|
104
|
+
currency: "USDC",
|
|
105
|
+
isNew: false,
|
|
106
|
+
explorerUrl: `https://solscan.io/account/${address}`,
|
|
107
|
+
fundingUrl: "https://sol.blockrun.ai"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const info = ensureEvmWallet();
|
|
58
111
|
const links = getPaymentLinks(info.address);
|
|
59
112
|
return {
|
|
60
113
|
address: info.address,
|
|
@@ -62,11 +115,19 @@ function getWalletInfo() {
|
|
|
62
115
|
chainId: 8453,
|
|
63
116
|
currency: "USDC",
|
|
64
117
|
isNew: info.isNew,
|
|
65
|
-
|
|
118
|
+
explorerUrl: links.basescan,
|
|
66
119
|
fundingUrl: links.blockrun
|
|
67
120
|
};
|
|
68
121
|
}
|
|
69
122
|
async function getUsdcBalance(address) {
|
|
123
|
+
if (getChain() === "solana") {
|
|
124
|
+
try {
|
|
125
|
+
const client = getClient();
|
|
126
|
+
return await client.getBalance();
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
70
131
|
const USDC_ADDRESS2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
71
132
|
const BASE_RPC_URLS = [
|
|
72
133
|
"https://mainnet.base.org",
|
|
@@ -101,14 +162,15 @@ import { z } from "zod";
|
|
|
101
162
|
// src/utils/qr.ts
|
|
102
163
|
import QRCode from "qrcode";
|
|
103
164
|
import open from "open";
|
|
104
|
-
import * as
|
|
165
|
+
import * as fs2 from "fs";
|
|
166
|
+
import sharp from "sharp";
|
|
105
167
|
|
|
106
168
|
// src/utils/constants.ts
|
|
107
|
-
import * as
|
|
108
|
-
import * as
|
|
109
|
-
var WALLET_DIR =
|
|
110
|
-
var WALLET_FILE =
|
|
111
|
-
var QR_FILE =
|
|
169
|
+
import * as path2 from "path";
|
|
170
|
+
import * as os2 from "os";
|
|
171
|
+
var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
|
|
172
|
+
var WALLET_FILE = path2.join(WALLET_DIR, ".session");
|
|
173
|
+
var QR_FILE = path2.join(WALLET_DIR, "qr.png");
|
|
112
174
|
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
113
175
|
var BASE_CHAIN_ID = "8453";
|
|
114
176
|
var MODEL_TIERS = {
|
|
@@ -123,22 +185,53 @@ var MODEL_TIERS = {
|
|
|
123
185
|
};
|
|
124
186
|
|
|
125
187
|
// src/utils/qr.ts
|
|
188
|
+
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
126
189
|
function getEip681Uri(address, amountUsdc = 1) {
|
|
127
190
|
const amountWei = Math.floor(amountUsdc * 1e6);
|
|
128
191
|
return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
|
|
129
192
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
function getSolanaPayUri(address, amountUsdc = 1) {
|
|
194
|
+
return `solana:${address}?spl-token=${SOLANA_USDC_MINT}&amount=${amountUsdc}&label=BlockRun`;
|
|
195
|
+
}
|
|
196
|
+
function buildSolanaLogoSvg(size) {
|
|
197
|
+
const half = size / 2;
|
|
198
|
+
return `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
199
|
+
<defs>
|
|
200
|
+
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
201
|
+
<stop offset="0%" style="stop-color:#9945FF"/>
|
|
202
|
+
<stop offset="100%" style="stop-color:#14F195"/>
|
|
203
|
+
</linearGradient>
|
|
204
|
+
<clipPath id="c"><circle cx="${half}" cy="${half}" r="${half}"/></clipPath>
|
|
205
|
+
</defs>
|
|
206
|
+
<circle cx="${half}" cy="${half}" r="${half}" fill="url(#g)" clip-path="url(#c)"/>
|
|
207
|
+
<text x="${half}" y="${half + 14}" font-size="40" font-weight="bold" fill="white"
|
|
208
|
+
font-family="Arial,sans-serif" text-anchor="middle">\u25CE</text>
|
|
209
|
+
</svg>`;
|
|
210
|
+
}
|
|
211
|
+
async function overlayLogo(qrBuf, chain, qrSize) {
|
|
212
|
+
if (chain !== "solana") return qrBuf;
|
|
213
|
+
const logoSize = Math.round(qrSize * 0.18);
|
|
214
|
+
const pad = Math.round(logoSize * 0.08);
|
|
215
|
+
const logoBuf = await sharp(Buffer.from(buildSolanaLogoSvg(logoSize))).resize(logoSize, logoSize).extend({ top: pad, bottom: pad, left: pad, right: pad, background: { r: 255, g: 255, b: 255, alpha: 1 } }).toBuffer();
|
|
216
|
+
const totalSize = logoSize + pad * 2;
|
|
217
|
+
const offset = Math.round((qrSize - totalSize) / 2);
|
|
218
|
+
return sharp(qrBuf).composite([{ input: logoBuf, left: offset, top: offset }]).toBuffer();
|
|
219
|
+
}
|
|
220
|
+
async function generateQrPng(address, chain = "base") {
|
|
221
|
+
const uri = chain === "solana" ? getSolanaPayUri(address) : getEip681Uri(address);
|
|
222
|
+
const qrSize = 400;
|
|
223
|
+
if (!fs2.existsSync(WALLET_DIR)) {
|
|
224
|
+
fs2.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
|
|
134
225
|
}
|
|
135
|
-
await QRCode.
|
|
226
|
+
const qrBuf = await QRCode.toBuffer(uri, {
|
|
136
227
|
type: "png",
|
|
137
|
-
width:
|
|
228
|
+
width: qrSize,
|
|
138
229
|
margin: 2,
|
|
139
230
|
errorCorrectionLevel: "H",
|
|
140
231
|
color: { dark: "#000000", light: "#FFFFFF" }
|
|
141
232
|
});
|
|
233
|
+
const finalBuf = await overlayLogo(qrBuf, chain, qrSize);
|
|
234
|
+
fs2.writeFileSync(QR_FILE, finalBuf);
|
|
142
235
|
return QR_FILE;
|
|
143
236
|
}
|
|
144
237
|
async function openQrInViewer(qrPath) {
|
|
@@ -149,6 +242,32 @@ async function openQrInViewer(qrPath) {
|
|
|
149
242
|
}
|
|
150
243
|
|
|
151
244
|
// src/utils/errors.ts
|
|
245
|
+
function extractErrorMessage(err) {
|
|
246
|
+
if (!err || typeof err !== "object") return String(err);
|
|
247
|
+
const e = err;
|
|
248
|
+
const base = typeof e.message === "string" ? e.message : String(err);
|
|
249
|
+
if (e.response === void 0 || e.response === null) return base;
|
|
250
|
+
try {
|
|
251
|
+
const body = e.response;
|
|
252
|
+
if (typeof body === "string") return body.trim() ? `${base} \u2014 ${body}` : base;
|
|
253
|
+
if (typeof body === "object") {
|
|
254
|
+
const b = body;
|
|
255
|
+
const parts = [];
|
|
256
|
+
if (typeof b.message === "string") parts.push(b.message);
|
|
257
|
+
if (typeof b.hint === "string") parts.push(`Hint: ${b.hint}`);
|
|
258
|
+
if (Array.isArray(b.missing_params) && b.missing_params.length) {
|
|
259
|
+
parts.push(`Missing: ${b.missing_params.join(", ")}`);
|
|
260
|
+
}
|
|
261
|
+
if (parts.length === 0) {
|
|
262
|
+
parts.push(JSON.stringify(b));
|
|
263
|
+
}
|
|
264
|
+
return `${base}
|
|
265
|
+
${parts.join("\n")}`;
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
return base;
|
|
270
|
+
}
|
|
152
271
|
function formatError(message) {
|
|
153
272
|
const msgLower = message.toLowerCase();
|
|
154
273
|
const isPaymentError = msgLower.includes("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !msgLower.includes("500");
|
|
@@ -157,15 +276,17 @@ function formatError(message) {
|
|
|
157
276
|
if (isServerError) {
|
|
158
277
|
errorText += `
|
|
159
278
|
|
|
160
|
-
This is a temporary API issue. The
|
|
279
|
+
This is a temporary API issue. The API may be experiencing problems.
|
|
161
280
|
Try again in a few minutes, or use a different model (e.g., openai/gpt-4o).`;
|
|
162
281
|
} else if (isPaymentError) {
|
|
282
|
+
const chain = getChain();
|
|
283
|
+
const network = chain === "solana" ? "Solana" : "Base";
|
|
163
284
|
errorText += `
|
|
164
285
|
|
|
165
286
|
This error usually means your wallet needs funding.
|
|
166
287
|
Run blockrun_wallet with action: "setup" to get funding instructions.
|
|
167
288
|
|
|
168
|
-
Quick fix: Send USDC to your wallet on
|
|
289
|
+
Quick fix: Send USDC to your wallet on ${network} network.`;
|
|
169
290
|
}
|
|
170
291
|
return errorText;
|
|
171
292
|
}
|
|
@@ -210,8 +331,9 @@ Do NOT call this for actual AI queries \u2014 use blockrun_chat for that.`,
|
|
|
210
331
|
}
|
|
211
332
|
},
|
|
212
333
|
async ({ action, budget_action, budget_amount, agent_id, agent_limit }) => {
|
|
213
|
-
const info = getWalletInfo();
|
|
334
|
+
const info = await getWalletInfo();
|
|
214
335
|
const address = info.address;
|
|
336
|
+
const chain = getChain();
|
|
215
337
|
if (action === "budget") {
|
|
216
338
|
const budgetAct = budget_action || "check";
|
|
217
339
|
if (budgetAct === "set") {
|
|
@@ -289,10 +411,11 @@ Pass agent_id: "${agent_id}" in any blockrun_* tool call to track and enforce th
|
|
|
289
411
|
}
|
|
290
412
|
if (action === "qr") {
|
|
291
413
|
try {
|
|
292
|
-
const qrPath = await generateQrPng(address);
|
|
414
|
+
const qrPath = await generateQrPng(address, chain);
|
|
293
415
|
await openQrInViewer(qrPath);
|
|
416
|
+
const scanNote = chain === "solana" ? "Scan with a Solana wallet (Phantom, Solflare) to send USDC on Solana." : "Scan with MetaMask to send USDC on Base.";
|
|
294
417
|
return {
|
|
295
|
-
content: [{ type: "text", text: `QR code opened!
|
|
418
|
+
content: [{ type: "text", text: `QR code opened! ${scanNote}
|
|
296
419
|
|
|
297
420
|
Address: ${address}
|
|
298
421
|
QR saved: ${qrPath}` }]
|
|
@@ -307,14 +430,46 @@ QR saved: ${qrPath}` }]
|
|
|
307
430
|
if (action === "setup") {
|
|
308
431
|
let qrMessage = "";
|
|
309
432
|
try {
|
|
310
|
-
const qrPath = await generateQrPng(address);
|
|
433
|
+
const qrPath = await generateQrPng(address, chain);
|
|
311
434
|
await openQrInViewer(qrPath);
|
|
312
435
|
qrMessage = `
|
|
313
436
|
QR code opened for scanning! (${qrPath})`;
|
|
314
437
|
} catch {
|
|
315
438
|
qrMessage = "\n(QR generation failed - use address above)";
|
|
316
439
|
}
|
|
317
|
-
const text2 = `
|
|
440
|
+
const text2 = chain === "solana" ? `
|
|
441
|
+
================================================================================
|
|
442
|
+
BLOCKRUN WALLET SETUP (SOLANA)
|
|
443
|
+
================================================================================
|
|
444
|
+
|
|
445
|
+
Your Solana wallet address: ${address}
|
|
446
|
+
${qrMessage}
|
|
447
|
+
|
|
448
|
+
HOW TO FUND YOUR WALLET:
|
|
449
|
+
------------------------
|
|
450
|
+
|
|
451
|
+
Option 1: Transfer from Coinbase
|
|
452
|
+
1. Open Coinbase app or website
|
|
453
|
+
2. Go to Send/Receive \u2192 Select USDC
|
|
454
|
+
3. Choose "Solana" network (important!)
|
|
455
|
+
4. Paste: ${address}
|
|
456
|
+
5. Send $1-5 to start
|
|
457
|
+
|
|
458
|
+
Option 2: Transfer from any Solana wallet (Phantom, Solflare, Backpack)
|
|
459
|
+
- Send USDC (SPL) to: ${address}
|
|
460
|
+
- Make sure to use Solana network, not EVM
|
|
461
|
+
|
|
462
|
+
Option 3: Bridge from other chains
|
|
463
|
+
https://portalbridge.com \u2192 Bridge USDC to Solana \u2192 Send to address above
|
|
464
|
+
|
|
465
|
+
VERIFY BALANCE: https://solscan.io/account/${address}
|
|
466
|
+
|
|
467
|
+
PRICING (pay per use):
|
|
468
|
+
- GPT-4o: ~$0.005/request | Claude Sonnet: ~$0.003/request
|
|
469
|
+
- Gemini Flash: ~$0.0001/request | Full pricing: https://blockrun.ai/pricing
|
|
470
|
+
|
|
471
|
+
SECURITY: Private key stored at ~/.blockrun/.solana-session (never leaves your machine)
|
|
472
|
+
================================================================================` : `
|
|
318
473
|
================================================================================
|
|
319
474
|
BLOCKRUN WALLET SETUP
|
|
320
475
|
================================================================================
|
|
@@ -351,9 +506,10 @@ SECURITY: Private key stored at ~/.blockrun/.session (never leaves your machine)
|
|
|
351
506
|
const balance = await getUsdcBalance(address);
|
|
352
507
|
const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch";
|
|
353
508
|
const lowBalance = balance !== null && balance < 1;
|
|
509
|
+
const explorerLabel = chain === "solana" ? "Solscan" : "Basescan";
|
|
354
510
|
const text = `Wallet: ${address}
|
|
355
511
|
Balance: ${balanceStr}${lowBalance ? " (low - add funds)" : ""}
|
|
356
|
-
Network:
|
|
512
|
+
Network: ${info.network} | View: ${info.explorerUrl}
|
|
357
513
|
${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions" : ""}`;
|
|
358
514
|
return {
|
|
359
515
|
content: [{ type: "text", text }],
|
|
@@ -363,7 +519,8 @@ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions
|
|
|
363
519
|
network: info.network,
|
|
364
520
|
chainId: info.chainId,
|
|
365
521
|
isNew: info.isNew,
|
|
366
|
-
|
|
522
|
+
explorerUrl: info.explorerUrl,
|
|
523
|
+
explorerLabel
|
|
367
524
|
}
|
|
368
525
|
};
|
|
369
526
|
}
|
|
@@ -372,6 +529,7 @@ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions
|
|
|
372
529
|
|
|
373
530
|
// src/tools/chat.ts
|
|
374
531
|
import { z as z2 } from "zod";
|
|
532
|
+
import { LLMClient as LLMClient2 } from "@blockrun/llm";
|
|
375
533
|
|
|
376
534
|
// src/utils/budget.ts
|
|
377
535
|
function checkBudget(budget, agentId) {
|
|
@@ -448,6 +606,12 @@ Run blockrun_models to see all 41+ models with pricing.`,
|
|
|
448
606
|
};
|
|
449
607
|
}
|
|
450
608
|
if (routing === "smart") {
|
|
609
|
+
if (!(llm instanceof LLMClient2)) {
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
612
|
+
isError: true
|
|
613
|
+
};
|
|
614
|
+
}
|
|
451
615
|
try {
|
|
452
616
|
const result = await llm.smartChat(message, {
|
|
453
617
|
system,
|
|
@@ -1006,39 +1170,31 @@ function registerSearchTool(server) {
|
|
|
1006
1170
|
server.registerTool(
|
|
1007
1171
|
"blockrun_search",
|
|
1008
1172
|
{
|
|
1009
|
-
description: `
|
|
1173
|
+
description: `Grok Live Search \u2014 real-time web + X/Twitter + news with AI-summarized results and citations. ~$0.025 per source.
|
|
1174
|
+
|
|
1175
|
+
Common shape:
|
|
1176
|
+
- body: { query: "...", sources: ["web","x","news"], maxResults: 10, fromDate: "YYYY-MM-DD", toDate: "YYYY-MM-DD" }
|
|
1010
1177
|
|
|
1011
|
-
|
|
1012
|
-
Pricing: ~$0.01/search
|
|
1178
|
+
\`sources\` accepts any subset of ["web","x","news"] (defaults to all three). For tweet-only searches, use ["x"].
|
|
1013
1179
|
|
|
1014
|
-
|
|
1180
|
+
Full request shape + worked examples in the \`search\` skill (\`skills/search/SKILL.md\`).`,
|
|
1015
1181
|
inputSchema: {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
max_results: z7.number().optional().default(10).describe("Max results per source (1-20)"),
|
|
1019
|
-
from_date: z7.string().optional().describe("Start date filter (YYYY-MM-DD)"),
|
|
1020
|
-
to_date: z7.string().optional().describe("End date filter (YYYY-MM-DD)")
|
|
1182
|
+
path: z7.string().optional().default("").describe("Endpoint sub-path under /v1/search/ (default empty = root /v1/search). Reserved for future surfaces."),
|
|
1183
|
+
body: z7.any().optional().describe("Request body. At minimum { query: '...' }. Sent as POST.")
|
|
1021
1184
|
}
|
|
1022
1185
|
},
|
|
1023
|
-
async ({
|
|
1186
|
+
async ({ path: path4, body }) => {
|
|
1024
1187
|
try {
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
fromDate: from_date,
|
|
1030
|
-
toDate: to_date
|
|
1031
|
-
});
|
|
1188
|
+
const client = getClient();
|
|
1189
|
+
const cleanPath = (path4 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
|
|
1190
|
+
const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
|
|
1191
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
1032
1192
|
return {
|
|
1033
1193
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1034
1194
|
structuredContent: result
|
|
1035
1195
|
};
|
|
1036
1196
|
} catch (err) {
|
|
1037
|
-
|
|
1038
|
-
return {
|
|
1039
|
-
content: [{ type: "text", text: formatError(errMsg) }],
|
|
1040
|
-
isError: true
|
|
1041
|
-
};
|
|
1197
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
1042
1198
|
}
|
|
1043
1199
|
}
|
|
1044
1200
|
);
|
|
@@ -1050,68 +1206,34 @@ function registerExaTool(server) {
|
|
|
1050
1206
|
server.registerTool(
|
|
1051
1207
|
"blockrun_exa",
|
|
1052
1208
|
{
|
|
1053
|
-
description: `Neural web search via Exa
|
|
1209
|
+
description: `Neural web search via Exa \u2014 understands meaning, not just keywords. Great for research.
|
|
1054
1210
|
|
|
1055
|
-
|
|
1056
|
-
- search:
|
|
1057
|
-
- answer:
|
|
1058
|
-
- contents:
|
|
1059
|
-
- similar
|
|
1211
|
+
Common paths (all POST, body shapes documented in the exa-research skill):
|
|
1212
|
+
- search \u2014 body: { query, numResults?, category?, includeDomains?, excludeDomains? } ($0.01/call)
|
|
1213
|
+
- answer \u2014 body: { query } ($0.01/call)
|
|
1214
|
+
- contents \u2014 body: { urls: [...] } ($0.002/URL, up to 100)
|
|
1215
|
+
- find-similar \u2014 body: { url, numResults? } ($0.01/call)
|
|
1216
|
+
|
|
1217
|
+
Categories for search: "news", "research paper", "company", "tweet", "github", "pdf".
|
|
1218
|
+
|
|
1219
|
+
Full request/response shapes + worked research workflows in the \`exa-research\` skill.`,
|
|
1060
1220
|
inputSchema: {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
url: z8.string().optional().describe("Reference URL to find similar pages (for similar action)"),
|
|
1064
|
-
urls: z8.array(z8.string()).optional().describe("URLs to fetch content from (for contents action, up to 100)"),
|
|
1065
|
-
num_results: z8.number().optional().describe("Number of results to return (default: 10)"),
|
|
1066
|
-
category: z8.string().optional().describe("Category filter: 'news', 'research paper', 'company', 'tweet', 'github', 'pdf'"),
|
|
1067
|
-
include_domains: z8.array(z8.string()).optional().describe("Only search within these domains"),
|
|
1068
|
-
exclude_domains: z8.array(z8.string()).optional().describe("Exclude these domains from results")
|
|
1221
|
+
path: z8.string().describe("Endpoint name under /v1/exa/, e.g. 'search', 'answer', 'contents', 'find-similar'"),
|
|
1222
|
+
body: z8.any().optional().describe("JSON body for the call. Sent as POST. Required for all four endpoints.")
|
|
1069
1223
|
}
|
|
1070
1224
|
},
|
|
1071
|
-
async ({
|
|
1225
|
+
async ({ path: path4, body }) => {
|
|
1072
1226
|
try {
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
case "search":
|
|
1078
|
-
if (!query) throw new Error("query required for search action");
|
|
1079
|
-
result = await req.requestWithPaymentRaw("/v1/exa/search", {
|
|
1080
|
-
query,
|
|
1081
|
-
numResults: num_results,
|
|
1082
|
-
category,
|
|
1083
|
-
includeDomains: include_domains,
|
|
1084
|
-
excludeDomains: exclude_domains
|
|
1085
|
-
});
|
|
1086
|
-
break;
|
|
1087
|
-
case "answer":
|
|
1088
|
-
if (!query) throw new Error("query required for answer action");
|
|
1089
|
-
result = await req.requestWithPaymentRaw("/v1/exa/answer", { query });
|
|
1090
|
-
break;
|
|
1091
|
-
case "contents":
|
|
1092
|
-
if (!urls?.length) throw new Error("urls array required for contents action");
|
|
1093
|
-
result = await req.requestWithPaymentRaw("/v1/exa/contents", { urls });
|
|
1094
|
-
break;
|
|
1095
|
-
case "similar":
|
|
1096
|
-
if (!url) throw new Error("url required for similar action");
|
|
1097
|
-
result = await req.requestWithPaymentRaw("/v1/exa/find-similar", {
|
|
1098
|
-
url,
|
|
1099
|
-
numResults: num_results
|
|
1100
|
-
});
|
|
1101
|
-
break;
|
|
1102
|
-
default:
|
|
1103
|
-
throw new Error(`Unknown action: ${action}`);
|
|
1104
|
-
}
|
|
1227
|
+
const client = getClient();
|
|
1228
|
+
const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
|
|
1229
|
+
const endpoint = `/v1/exa/${cleanPath}`;
|
|
1230
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
1105
1231
|
return {
|
|
1106
1232
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1107
1233
|
structuredContent: result
|
|
1108
1234
|
};
|
|
1109
1235
|
} catch (err) {
|
|
1110
|
-
|
|
1111
|
-
return {
|
|
1112
|
-
content: [{ type: "text", text: formatError(errMsg) }],
|
|
1113
|
-
isError: true
|
|
1114
|
-
};
|
|
1236
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
1115
1237
|
}
|
|
1116
1238
|
}
|
|
1117
1239
|
);
|
|
@@ -1180,10 +1302,10 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
|
|
|
1180
1302
|
body: z9.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)")
|
|
1181
1303
|
}
|
|
1182
1304
|
},
|
|
1183
|
-
async ({ path:
|
|
1305
|
+
async ({ path: path4, params, body }) => {
|
|
1184
1306
|
try {
|
|
1185
1307
|
const llm = getClient();
|
|
1186
|
-
const result = body ? await llm.pmQuery(
|
|
1308
|
+
const result = body ? await llm.pmQuery(path4, body) : await llm.pm(path4, params);
|
|
1187
1309
|
return {
|
|
1188
1310
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1189
1311
|
structuredContent: result
|
|
@@ -1302,132 +1424,8 @@ Examples:
|
|
|
1302
1424
|
);
|
|
1303
1425
|
}
|
|
1304
1426
|
|
|
1305
|
-
// src/tools/twitter.ts
|
|
1306
|
-
import { z as z11 } from "zod";
|
|
1307
|
-
var ACTION2 = z11.enum([
|
|
1308
|
-
"user_lookup",
|
|
1309
|
-
"user_info",
|
|
1310
|
-
"followers",
|
|
1311
|
-
"followings",
|
|
1312
|
-
"verified_followers",
|
|
1313
|
-
"user_tweets",
|
|
1314
|
-
"user_mentions",
|
|
1315
|
-
"tweet_lookup",
|
|
1316
|
-
"tweet_replies",
|
|
1317
|
-
"tweet_thread",
|
|
1318
|
-
"search"
|
|
1319
|
-
]);
|
|
1320
|
-
function registerTwitterTool(server) {
|
|
1321
|
-
server.registerTool(
|
|
1322
|
-
"blockrun_x",
|
|
1323
|
-
{
|
|
1324
|
-
description: `Structured X/Twitter data via AttentionVC partner API.
|
|
1325
|
-
|
|
1326
|
-
Actions:
|
|
1327
|
-
- user_lookup (usernames: string | string[])
|
|
1328
|
-
- user_info (username)
|
|
1329
|
-
- followers (username, cursor?)
|
|
1330
|
-
- followings (username, cursor?)
|
|
1331
|
-
- verified_followers (user_id, cursor?)
|
|
1332
|
-
- user_tweets (username, includeReplies?, cursor?)
|
|
1333
|
-
- user_mentions (username, sinceTime?, untilTime?, cursor?)
|
|
1334
|
-
- tweet_lookup (tweet_ids: string | string[])
|
|
1335
|
-
- tweet_replies (tweet_id, cursor?, queryType?)
|
|
1336
|
-
- tweet_thread (tweet_id, cursor?)
|
|
1337
|
-
- search (query, queryType?, cursor?)
|
|
1338
|
-
|
|
1339
|
-
Paid per request via x402; prices scale with the endpoint (e.g. user_lookup ~ $0.02, followers ~ $0.05/page).`,
|
|
1340
|
-
inputSchema: {
|
|
1341
|
-
action: ACTION2,
|
|
1342
|
-
usernames: z11.union([z11.string(), z11.array(z11.string())]).optional(),
|
|
1343
|
-
username: z11.string().optional(),
|
|
1344
|
-
user_id: z11.string().optional(),
|
|
1345
|
-
tweet_id: z11.string().optional(),
|
|
1346
|
-
tweet_ids: z11.union([z11.string(), z11.array(z11.string())]).optional(),
|
|
1347
|
-
query: z11.string().optional(),
|
|
1348
|
-
queryType: z11.enum(["Latest", "Top", "Default"]).optional(),
|
|
1349
|
-
cursor: z11.string().optional(),
|
|
1350
|
-
sinceTime: z11.string().optional(),
|
|
1351
|
-
untilTime: z11.string().optional(),
|
|
1352
|
-
includeReplies: z11.boolean().optional()
|
|
1353
|
-
}
|
|
1354
|
-
},
|
|
1355
|
-
async (args) => {
|
|
1356
|
-
try {
|
|
1357
|
-
const llm = getClient();
|
|
1358
|
-
const a = args.action;
|
|
1359
|
-
const require2 = (value, name) => {
|
|
1360
|
-
if (value === void 0 || value === null || value === "") {
|
|
1361
|
-
throw new Error(`${name} is required for action='${a}'`);
|
|
1362
|
-
}
|
|
1363
|
-
return value;
|
|
1364
|
-
};
|
|
1365
|
-
let result;
|
|
1366
|
-
switch (a) {
|
|
1367
|
-
case "user_lookup":
|
|
1368
|
-
result = await llm.xUserLookup(require2(args.usernames, "usernames"));
|
|
1369
|
-
break;
|
|
1370
|
-
case "user_info":
|
|
1371
|
-
result = await llm.xUserInfo(require2(args.username, "username"));
|
|
1372
|
-
break;
|
|
1373
|
-
case "followers":
|
|
1374
|
-
result = await llm.xFollowers(require2(args.username, "username"), args.cursor);
|
|
1375
|
-
break;
|
|
1376
|
-
case "followings":
|
|
1377
|
-
result = await llm.xFollowings(require2(args.username, "username"), args.cursor);
|
|
1378
|
-
break;
|
|
1379
|
-
case "verified_followers":
|
|
1380
|
-
result = await llm.xVerifiedFollowers(require2(args.user_id, "user_id"), args.cursor);
|
|
1381
|
-
break;
|
|
1382
|
-
case "user_tweets":
|
|
1383
|
-
result = await llm.xUserTweets(
|
|
1384
|
-
require2(args.username, "username"),
|
|
1385
|
-
args.includeReplies,
|
|
1386
|
-
args.cursor
|
|
1387
|
-
);
|
|
1388
|
-
break;
|
|
1389
|
-
case "user_mentions":
|
|
1390
|
-
result = await llm.xUserMentions(
|
|
1391
|
-
require2(args.username, "username"),
|
|
1392
|
-
args.sinceTime,
|
|
1393
|
-
args.untilTime,
|
|
1394
|
-
args.cursor
|
|
1395
|
-
);
|
|
1396
|
-
break;
|
|
1397
|
-
case "tweet_lookup":
|
|
1398
|
-
result = await llm.xTweetLookup(require2(args.tweet_ids, "tweet_ids"));
|
|
1399
|
-
break;
|
|
1400
|
-
case "tweet_replies":
|
|
1401
|
-
result = await llm.xTweetReplies(
|
|
1402
|
-
require2(args.tweet_id, "tweet_id"),
|
|
1403
|
-
args.queryType,
|
|
1404
|
-
args.cursor
|
|
1405
|
-
);
|
|
1406
|
-
break;
|
|
1407
|
-
case "tweet_thread":
|
|
1408
|
-
result = await llm.xTweetThread(require2(args.tweet_id, "tweet_id"), args.cursor);
|
|
1409
|
-
break;
|
|
1410
|
-
case "search":
|
|
1411
|
-
result = await llm.xSearch(require2(args.query, "query"), args.queryType, args.cursor);
|
|
1412
|
-
break;
|
|
1413
|
-
}
|
|
1414
|
-
return {
|
|
1415
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1416
|
-
structuredContent: result
|
|
1417
|
-
};
|
|
1418
|
-
} catch (err) {
|
|
1419
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1420
|
-
return {
|
|
1421
|
-
content: [{ type: "text", text: formatError(msg) }],
|
|
1422
|
-
isError: true
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
1427
|
// src/tools/dex.ts
|
|
1430
|
-
import { z as
|
|
1428
|
+
import { z as z11 } from "zod";
|
|
1431
1429
|
function registerDexTool(server) {
|
|
1432
1430
|
server.registerTool(
|
|
1433
1431
|
"blockrun_dex",
|
|
@@ -1444,10 +1442,10 @@ Examples:
|
|
|
1444
1442
|
blockrun_dex({ token: "So11...xxx" }) -> Get specific token data
|
|
1445
1443
|
blockrun_dex({ symbol: "PEPE" }) -> Search by symbol`,
|
|
1446
1444
|
inputSchema: {
|
|
1447
|
-
query:
|
|
1448
|
-
token:
|
|
1449
|
-
symbol:
|
|
1450
|
-
chain:
|
|
1445
|
+
query: z11.string().optional().describe("Search query (token name, symbol, or address)"),
|
|
1446
|
+
token: z11.string().optional().describe("Token address for direct lookup"),
|
|
1447
|
+
symbol: z11.string().optional().describe("Token symbol to search"),
|
|
1448
|
+
chain: z11.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
|
|
1451
1449
|
}
|
|
1452
1450
|
},
|
|
1453
1451
|
async ({ query, token, symbol, chain }) => {
|
|
@@ -1508,85 +1506,135 @@ ${lines.join("\n\n")}` }],
|
|
|
1508
1506
|
}
|
|
1509
1507
|
|
|
1510
1508
|
// src/tools/modal.ts
|
|
1511
|
-
import { z as
|
|
1512
|
-
var MODAL_GPU_TYPES = ["T4", "L4", "A10G", "A100", "A100-80GB", "H100"];
|
|
1509
|
+
import { z as z12 } from "zod";
|
|
1513
1510
|
function registerModalTool(server) {
|
|
1514
1511
|
server.registerTool(
|
|
1515
1512
|
"blockrun_modal",
|
|
1516
1513
|
{
|
|
1517
|
-
description: `Run isolated code in a BlockRun-hosted Modal sandbox.
|
|
1514
|
+
description: `Run isolated code in a BlockRun-hosted Modal sandbox \u2014 disposable remote container, optional GPU.
|
|
1518
1515
|
|
|
1519
|
-
Use
|
|
1520
|
-
- a disposable remote container
|
|
1521
|
-
- GPU access
|
|
1522
|
-
- a clean environment that will not affect the local machine
|
|
1523
|
-
- a safer place to run untrusted or heavy code
|
|
1516
|
+
Use when you need: a clean ephemeral environment, GPU access (T4/L4/A10G/A100/A100-80GB/H100), or a safer place for untrusted code. Prefer local tools for normal repo work.
|
|
1524
1517
|
|
|
1525
|
-
|
|
1518
|
+
Common paths (all POST):
|
|
1519
|
+
- sandbox/create \u2014 body: { image?, timeout?, cpu?, memory?, gpu?, setup_commands? } ($0.01)
|
|
1520
|
+
- sandbox/exec \u2014 body: { sandbox_id, command: ["python","-c","..."], timeout? } ($0.001)
|
|
1521
|
+
- sandbox/status \u2014 body: { sandbox_id } ($0.001)
|
|
1522
|
+
- sandbox/terminate \u2014 body: { sandbox_id } ($0.001)
|
|
1526
1523
|
|
|
1527
|
-
|
|
1528
|
-
- create: $0.01
|
|
1529
|
-
- exec/status/terminate: $0.001 each`,
|
|
1524
|
+
Full action shapes + GPU type details in the \`modal\` skill.`,
|
|
1530
1525
|
inputSchema: {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
),
|
|
1534
|
-
sandbox_id: z13.string().optional().describe("Sandbox ID returned by a previous create"),
|
|
1535
|
-
command: z13.array(z13.string()).optional().describe('Command array for exec, for example ["python", "-c", "print(2+2)"]'),
|
|
1536
|
-
image: z13.string().optional().describe("Container image for create (default: python:3.11)"),
|
|
1537
|
-
timeout: z13.number().optional().describe("Timeout in seconds for create or exec"),
|
|
1538
|
-
cpu: z13.number().optional().describe("CPU cores for create"),
|
|
1539
|
-
memory: z13.number().optional().describe("Memory in MB for create"),
|
|
1540
|
-
gpu: z13.enum(MODAL_GPU_TYPES).optional().describe("Optional GPU type for create"),
|
|
1541
|
-
setup_commands: z13.array(z13.string()).optional().describe("Shell commands to run during sandbox setup")
|
|
1526
|
+
path: z12.string().describe("Endpoint under /v1/modal/, e.g. 'sandbox/create', 'sandbox/exec'"),
|
|
1527
|
+
body: z12.any().optional().describe("JSON body. Sent as POST.")
|
|
1542
1528
|
}
|
|
1543
1529
|
},
|
|
1544
|
-
async ({
|
|
1530
|
+
async ({ path: path4, body }) => {
|
|
1545
1531
|
try {
|
|
1546
|
-
const
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
case "create":
|
|
1551
|
-
result = await req.requestWithPaymentRaw("/v1/modal/sandbox/create", {
|
|
1552
|
-
image,
|
|
1553
|
-
timeout,
|
|
1554
|
-
cpu,
|
|
1555
|
-
memory,
|
|
1556
|
-
gpu,
|
|
1557
|
-
setup_commands
|
|
1558
|
-
});
|
|
1559
|
-
break;
|
|
1560
|
-
case "exec":
|
|
1561
|
-
if (!sandbox_id) throw new Error("sandbox_id required for exec action");
|
|
1562
|
-
if (!command?.length) throw new Error("command array required for exec action");
|
|
1563
|
-
result = await req.requestWithPaymentRaw("/v1/modal/sandbox/exec", {
|
|
1564
|
-
sandbox_id,
|
|
1565
|
-
command,
|
|
1566
|
-
timeout
|
|
1567
|
-
});
|
|
1568
|
-
break;
|
|
1569
|
-
case "status":
|
|
1570
|
-
if (!sandbox_id) throw new Error("sandbox_id required for status action");
|
|
1571
|
-
result = await req.requestWithPaymentRaw("/v1/modal/sandbox/status", { sandbox_id });
|
|
1572
|
-
break;
|
|
1573
|
-
case "terminate":
|
|
1574
|
-
if (!sandbox_id) throw new Error("sandbox_id required for terminate action");
|
|
1575
|
-
result = await req.requestWithPaymentRaw("/v1/modal/sandbox/terminate", {
|
|
1576
|
-
sandbox_id
|
|
1577
|
-
});
|
|
1578
|
-
break;
|
|
1579
|
-
default:
|
|
1580
|
-
throw new Error(`Unknown action: ${String(action)}`);
|
|
1581
|
-
}
|
|
1532
|
+
const client = getClient();
|
|
1533
|
+
const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
|
|
1534
|
+
const endpoint = `/v1/modal/${cleanPath}`;
|
|
1535
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
1582
1536
|
return {
|
|
1583
1537
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1584
1538
|
structuredContent: result
|
|
1585
1539
|
};
|
|
1586
1540
|
} catch (err) {
|
|
1587
|
-
|
|
1541
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/tools/phone.ts
|
|
1548
|
+
import { z as z13 } from "zod";
|
|
1549
|
+
function registerPhoneTool(server) {
|
|
1550
|
+
server.registerTool(
|
|
1551
|
+
"blockrun_phone",
|
|
1552
|
+
{
|
|
1553
|
+
description: `Phone-number intelligence, US/CA number provisioning, and outbound AI voice calls.
|
|
1554
|
+
|
|
1555
|
+
Common paths (path = everything after /v1/):
|
|
1556
|
+
- phone/lookup POST body: { phoneNumber } ($0.01)
|
|
1557
|
+
- phone/lookup/fraud POST body: { phoneNumber } \u2014 SIM-swap + call-forwarding signals ($0.05)
|
|
1558
|
+
- phone/numbers/buy POST body: { country?: "US"|"CA", areaCode? } \u2014 30-day lease ($5.00)
|
|
1559
|
+
- phone/numbers/renew POST body: { phoneNumber } \u2014 extend 30 days ($5.00)
|
|
1560
|
+
- phone/numbers/list POST body: {} \u2014 your wallet-owned numbers ($0.001)
|
|
1561
|
+
- phone/numbers/release POST body: { phoneNumber } \u2014 release back to pool (free)
|
|
1562
|
+
- voice/call POST body: { to, task, from, voice?, max_duration?, ... } ($0.54 flat)
|
|
1563
|
+
- voice/call/{call_id} GET (no body) \u2014 poll status + transcript (free)
|
|
1564
|
+
|
|
1565
|
+
REQUIRED for voice/call: \`from\` must be a number your wallet owns. Provision one with \`phone/numbers/buy\` first ($5, 30-day lease).
|
|
1566
|
+
|
|
1567
|
+
Voice presets: nat, josh, maya, june, paige, derek, florian. Phone numbers use E.164: +14155550100.
|
|
1568
|
+
|
|
1569
|
+
Voice call flow + voice preset details + full body shapes in the \`phone\` skill.`,
|
|
1570
|
+
inputSchema: {
|
|
1571
|
+
path: z13.string().describe("Endpoint after /v1/. Use 'phone/...' for lookup + number ops, 'voice/call' for outbound AI calls, 'voice/call/{id}' (no body) to poll status."),
|
|
1572
|
+
body: z13.any().optional().describe("JSON body. Sent as POST. Omit for the free GET poll (voice/call/{call_id}).")
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
async ({ path: path4, body }) => {
|
|
1576
|
+
try {
|
|
1577
|
+
const client = getClient();
|
|
1578
|
+
const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\//, "");
|
|
1579
|
+
const endpoint = `/v1/${cleanPath}`;
|
|
1580
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
|
|
1588
1581
|
return {
|
|
1589
|
-
content: [{ type: "text", text:
|
|
1582
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1583
|
+
structuredContent: result
|
|
1584
|
+
};
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/tools/surf.ts
|
|
1593
|
+
import { z as z14 } from "zod";
|
|
1594
|
+
function registerSurfTool(server) {
|
|
1595
|
+
server.registerTool(
|
|
1596
|
+
"blockrun_surf",
|
|
1597
|
+
{
|
|
1598
|
+
description: `Unified crypto data via Surf (asksurf.ai) \u2014 84 endpoints, one API.
|
|
1599
|
+
|
|
1600
|
+
Coverage: CEX market data (16 exchanges), on-chain SQL across 13 chains, 100M+ labeled wallets, prediction markets (Polymarket + Kalshi), social mindshare / CT intelligence, news, unified search, and Surf-1.5 chat with citations.
|
|
1601
|
+
|
|
1602
|
+
Pricing (settled in USDC to Surf's Base treasury):
|
|
1603
|
+
- Tier 1 $0.001 \u2014 prices, rankings, lists, news, profiles, simple reads
|
|
1604
|
+
- Tier 2 $0.005 \u2014 order books, candles, search, wallet detail, social aggregates
|
|
1605
|
+
- Tier 3 $0.020 \u2014 raw on-chain SQL, structured queries, surf-1.5 chat
|
|
1606
|
+
|
|
1607
|
+
Common paths (full 84-endpoint catalog in the surf skill):
|
|
1608
|
+
- market/price?symbol=BTC (T1)
|
|
1609
|
+
- exchange/price?pair=BTC-USDT (T1)
|
|
1610
|
+
- prediction-market/polymarket/ranking (T1)
|
|
1611
|
+
- search/web?q=ethereum+pectra+upgrade (T2)
|
|
1612
|
+
- wallet/detail?address=0x... (T2)
|
|
1613
|
+
- social/mindshare?q=ethereum&interval=1d (T2)
|
|
1614
|
+
- onchain/sql + body:{ sql: "SELECT ..." } (T3)
|
|
1615
|
+
- chat/completions + body:{ model:"surf/surf-1.5", messages:[]} (T3, $0.02 flat)
|
|
1616
|
+
|
|
1617
|
+
Method is auto-routed: pass 'body' for POST endpoints; otherwise GET with 'params'.
|
|
1618
|
+
Each Surf endpoint pre-validates required params before settling \u2014 you get a 400 (not a charge) if a required field is missing. Browse the full catalog: https://blockrun.ai/marketplace/surf`,
|
|
1619
|
+
inputSchema: {
|
|
1620
|
+
path: z14.string().describe("Endpoint path under /v1/surf/, e.g. 'market/price', 'prediction-market/polymarket/ranking', 'wallet/detail', 'onchain/sql', 'chat/completions'"),
|
|
1621
|
+
params: z14.record(z14.string(), z14.string()).optional().describe("Query parameters for GET endpoints, e.g. { symbol: 'BTC' } or { address: '0x...', chain: 'ethereum' }"),
|
|
1622
|
+
body: z14.any().optional().describe("JSON body for POST endpoints. Provide for: onchain/query, onchain/sql, chat/completions. When set, the call is sent as POST; otherwise GET with params.")
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
async ({ path: path4, params, body }) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const client = getClient();
|
|
1628
|
+
const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
|
|
1629
|
+
const endpoint = `/v1/surf/${cleanPath}`;
|
|
1630
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
|
|
1631
|
+
return {
|
|
1632
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1633
|
+
structuredContent: result
|
|
1634
|
+
};
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
return {
|
|
1637
|
+
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
1590
1638
|
isError: true
|
|
1591
1639
|
};
|
|
1592
1640
|
}
|
|
@@ -1608,20 +1656,24 @@ function initializeMcpServer(server) {
|
|
|
1608
1656
|
registerExaTool(server);
|
|
1609
1657
|
registerMarketsTool(server);
|
|
1610
1658
|
registerPriceTool(server);
|
|
1611
|
-
registerTwitterTool(server);
|
|
1612
1659
|
registerDexTool(server);
|
|
1613
1660
|
registerModalTool(server);
|
|
1661
|
+
registerPhoneTool(server);
|
|
1662
|
+
registerSurfTool(server);
|
|
1614
1663
|
server.registerResource(
|
|
1615
1664
|
"wallet",
|
|
1616
1665
|
"blockrun://wallet",
|
|
1617
1666
|
{ description: "Wallet address and status", mimeType: "application/json" },
|
|
1618
|
-
async () =>
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1667
|
+
async () => {
|
|
1668
|
+
const info = await getWalletInfo();
|
|
1669
|
+
return {
|
|
1670
|
+
contents: [{
|
|
1671
|
+
uri: "blockrun://wallet",
|
|
1672
|
+
mimeType: "application/json",
|
|
1673
|
+
text: JSON.stringify(info, null, 2)
|
|
1674
|
+
}]
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1625
1677
|
);
|
|
1626
1678
|
server.registerResource(
|
|
1627
1679
|
"models",
|
|
@@ -1647,9 +1699,9 @@ function initializeMcpServer(server) {
|
|
|
1647
1699
|
}
|
|
1648
1700
|
|
|
1649
1701
|
// src/utils/key-leak-scanner.ts
|
|
1650
|
-
import
|
|
1651
|
-
import
|
|
1652
|
-
import
|
|
1702
|
+
import fs3 from "fs";
|
|
1703
|
+
import path3 from "path";
|
|
1704
|
+
import os3 from "os";
|
|
1653
1705
|
function looksLikeRawPrivateKey(value) {
|
|
1654
1706
|
if (typeof value !== "string") return false;
|
|
1655
1707
|
if (/^0x[0-9a-fA-F]{64}$/.test(value)) return true;
|
|
@@ -1674,8 +1726,8 @@ function walk(obj, file, jsonPath, out) {
|
|
|
1674
1726
|
}
|
|
1675
1727
|
function scanFile(file) {
|
|
1676
1728
|
try {
|
|
1677
|
-
if (!
|
|
1678
|
-
const raw =
|
|
1729
|
+
if (!fs3.existsSync(file)) return [];
|
|
1730
|
+
const raw = fs3.readFileSync(file, "utf-8");
|
|
1679
1731
|
const data = JSON.parse(raw);
|
|
1680
1732
|
const out = [];
|
|
1681
1733
|
walk(data, file, "", out);
|
|
@@ -1685,12 +1737,12 @@ function scanFile(file) {
|
|
|
1685
1737
|
}
|
|
1686
1738
|
}
|
|
1687
1739
|
function warnOnLeakedKeys() {
|
|
1688
|
-
const home =
|
|
1740
|
+
const home = os3.homedir();
|
|
1689
1741
|
const candidates = [
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1742
|
+
path3.join(home, ".claude.json"),
|
|
1743
|
+
path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
1744
|
+
path3.join(home, ".config", "claude", "claude_desktop_config.json"),
|
|
1745
|
+
path3.join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
|
|
1694
1746
|
];
|
|
1695
1747
|
const findings = [];
|
|
1696
1748
|
for (const f of candidates) findings.push(...scanFile(f));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"mcpName": "io.github.BlockRunAI/blockrun-mcp",
|
|
5
5
|
"description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, crypto data, X/Twitter intelligence. Paid via x402 micropayments.",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
17
|
+
"build": "tsup src/index.ts --format esm --dts --clean --external sharp",
|
|
18
18
|
"dev": "tsx watch src/index.ts",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
49
49
|
"open": "^11.0.0",
|
|
50
50
|
"qrcode": "^1.5.4",
|
|
51
|
+
"sharp": "^0.34.5",
|
|
51
52
|
"viem": "^2.21.0",
|
|
52
53
|
"zod": "^4.3.5"
|
|
53
54
|
},
|