@blockrun/mcp 0.13.0 → 0.14.0

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.
Files changed (3) hide show
  1. package/README.md +127 -1
  2. package/dist/index.js +360 -42
  3. 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,6 +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 |
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 |
121
147
  | `blockrun_x` | X/Twitter — profiles, tweets, followers, mentions, search (AttentionVC) | per call |
122
148
  | `blockrun_exa` | Neural web search (Exa) — research, competitors, papers, URL content | $0.01/query |
123
149
  | `blockrun_search` | Grok Live Search — web + news with citations | ~$0.025 per source |
@@ -127,12 +153,56 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
127
153
 
128
154
  ---
129
155
 
156
+ ## Tips for effective LLMs
157
+
158
+ > **For LLMs and agents invoking BlockRun MCP tools:**
159
+
160
+ - **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.
161
+ - **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.
162
+ - **CRITICAL: `blockrun_chat routing:"smart"` (ClawRouter) only works on Base wallets.** On Solana, pass `mode:` or `model:` to pick a model directly.
163
+ - **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.
164
+ - **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.
165
+ - **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.
166
+
167
+ ---
168
+
169
+ ## Key Use Cases
170
+
171
+ What kinds of questions can Claude (or any LLM agent) answer once BlockRun MCP is installed:
172
+
173
+ 1. **Price reads / market data**
174
+ > *"What's BTC trading at? Compare with last week's average."* → `blockrun_price` (free) or `blockrun_surf` path:`market/price`
175
+
176
+ 2. **Prediction-market consensus**
177
+ > *"What's Polymarket's odds for the next Fed rate decision?"* → `blockrun_markets` path:`polymarket/events` + filter
178
+
179
+ 3. **On-chain forensics**
180
+ > *"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`
181
+
182
+ 4. **Cited research with sources**
183
+ > *"Find the 5 most-cited papers on speculative decoding from the last 90 days. Summarize the dominant approach."* → `blockrun_exa` action:`search` then `contents`
184
+
185
+ 5. **Image generation with on-image text**
186
+ > *"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
187
+
188
+ 6. **Voice phone-out**
189
+ > *"Call +1-415-555-... and confirm the appointment on Friday at 3pm."* → `blockrun_phone` action:`voice_call`, then poll `voice_status`
190
+
191
+ 7. **Multi-agent research with budget cap**
192
+ > *"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`
193
+
194
+ 8. **Cross-chain SQL forensics**
195
+ > *"Top 10 tokens by DEX volume on Base in the last 24h."* → `blockrun_surf` path:`onchain/sql`, body: `{ sql: "SELECT..." }`
196
+
197
+ ---
198
+
130
199
  ## Why not just use the APIs directly?
131
200
 
132
201
  | | Direct APIs | BlockRun |
133
202
  |---|---|---|
134
203
  | Exa | Sign up, $20/mo minimum | $0.01/call, no subscription |
135
204
  | Polymarket | Undocumented, rate-limited | $0.001/call, clean JSON |
205
+ | Surf (asksurf.ai) | Account + monthly plan | $0.001/call, no Surf account, 84 endpoints |
136
206
  | Twitter/X API | $100–$5000/month | $0.03/page, no approval |
137
207
  | Multiple sources | 4 accounts, 4 API keys, 4 billing pages | 1 wallet |
138
208
 
@@ -140,15 +210,71 @@ One wallet. All sources. No dashboards.
140
210
 
141
211
  ---
142
212
 
213
+ ## When NOT to use BlockRun MCP
214
+
215
+ BlockRun shines when you want **unified billing + many sources + LLM-readable errors**. It is not the right fit for:
216
+
217
+ - **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.
218
+ - **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.
219
+ - **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.
220
+ - **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.
221
+
222
+ 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.
223
+
224
+ ---
225
+
143
226
  ## Multi-agent budget delegation
144
227
 
145
228
  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
229
 
147
230
  ---
148
231
 
232
+ ## Troubleshooting
233
+
234
+ - **`Insufficient balance` / HTTP 402 after retry** → Run `blockrun_wallet action:"setup"`. Send USDC on Base (or Solana — see [Environment Variables](#environment-variables)).
235
+ - **`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`.
236
+ - **`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.
237
+ - **`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.
238
+ - **`ENOENT: ~/.blockrun/.session`** → Expected on first run. The server auto-creates the wallet; check stderr for the `WALLET_CREATED` line confirming the address.
239
+ - **`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`).
240
+ - **`Music generation timed out` (200s cap)** → Same pattern. **No charge**. Retry; if it persists, the upstream model is rate-limited — try off-peak.
241
+
242
+ ---
243
+
244
+ ## Environment Variables
245
+
246
+ | Variable / File | Default | Effect |
247
+ |---|---|---|
248
+ | `~/.blockrun/.session` | auto-created on first run | EVM private key (0x...). File exists → use Base. |
249
+ | `~/.blockrun/.solana-session` | not created | Solana private key. File exists → switch to Solana. |
250
+ | `SOLANA_WALLET_KEY` | unset | Env-var override of `.solana-session`. Set → use Solana. |
251
+
252
+ Chain selection priority (see `src/utils/wallet.ts:24`):
253
+
254
+ 1. `SOLANA_WALLET_KEY` env var present → Solana
255
+ 2. `~/.blockrun/.solana-session` exists → Solana
256
+ 3. Otherwise → Base (`~/.blockrun/.session` auto-created)
257
+
258
+ **Switching chains:**
259
+
260
+ - Base → Solana: `export SOLANA_WALLET_KEY=...`, or `echo "<secret>" > ~/.blockrun/.solana-session`
261
+ - Solana → Base: `unset SOLANA_WALLET_KEY && rm ~/.blockrun/.solana-session` (the existing `.session` is reused, so it's the same Base wallet)
262
+
263
+ 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.
264
+
265
+ ---
266
+
149
267
  ## How it works
150
268
 
151
- Pay-per-call via [x402](https://x402.org) micropayments in USDC on Base. Your wallet lives at `~/.blockrun/.session`. Private key never leaves your machine.
269
+ 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.
270
+
271
+ ---
272
+
273
+ ## Contributing
274
+
275
+ PRs welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, the tool-vs-skill design rule, and how to add a new partner API.
276
+
277
+ Issues: [github.com/blockrunai/blockrun-mcp/issues](https://github.com/blockrunai/blockrun-mcp/issues)
152
278
 
153
279
  ---
154
280
 
package/dist/index.js CHANGED
@@ -6,38 +6,58 @@ 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";
9
10
  import {
10
11
  LLMClient,
11
12
  ImageClient,
12
13
  PriceClient,
14
+ SolanaLLMClient,
13
15
  getOrCreateWallet,
16
+ loadSolanaWallet,
14
17
  getPaymentLinks,
15
18
  formatWalletCreatedMessage,
16
- formatNeedsFundingMessage
19
+ formatNeedsFundingMessage,
20
+ SOLANA_WALLET_FILE_PATH
17
21
  } from "@blockrun/llm";
18
- var _client = null;
22
+ var _evmClient = null;
19
23
  var _imageClient = null;
20
24
  var _priceClient = null;
21
- var _walletInfo = null;
22
- function ensureWallet() {
23
- if (!_walletInfo) {
24
- _walletInfo = getOrCreateWallet();
25
- if (_walletInfo.isNew) {
26
- console.error(formatWalletCreatedMessage(_walletInfo.address));
25
+ var _evmWalletInfo = null;
26
+ var _solanaClient = null;
27
+ function getChain() {
28
+ if (process.env.SOLANA_WALLET_KEY) return "solana";
29
+ try {
30
+ if (fs.existsSync(SOLANA_WALLET_FILE_PATH)) return "solana";
31
+ } catch {
32
+ }
33
+ return "base";
34
+ }
35
+ function ensureEvmWallet() {
36
+ if (!_evmWalletInfo) {
37
+ _evmWalletInfo = getOrCreateWallet();
38
+ if (_evmWalletInfo.isNew) {
39
+ console.error(formatWalletCreatedMessage(_evmWalletInfo.address));
27
40
  }
28
41
  }
29
- return _walletInfo;
42
+ return _evmWalletInfo;
30
43
  }
31
44
  function getOrCreateWalletKey() {
32
- const info = ensureWallet();
45
+ const info = ensureEvmWallet();
33
46
  return info.privateKey;
34
47
  }
35
48
  function getClient() {
36
- if (!_client) {
49
+ if (getChain() === "solana") {
50
+ if (!_solanaClient) {
51
+ const privateKey = process.env.SOLANA_WALLET_KEY || loadSolanaWallet() || void 0;
52
+ _solanaClient = new SolanaLLMClient(privateKey ? { privateKey } : void 0);
53
+ }
54
+ return _solanaClient;
55
+ }
56
+ if (!_evmClient) {
37
57
  const privateKey = getOrCreateWalletKey();
38
- _client = new LLMClient({ privateKey });
58
+ _evmClient = new LLMClient({ privateKey });
39
59
  }
40
- return _client;
60
+ return _evmClient;
41
61
  }
42
62
  function getImageClient() {
43
63
  if (!_imageClient) {
@@ -53,8 +73,21 @@ function getPriceClient() {
53
73
  }
54
74
  return _priceClient;
55
75
  }
56
- function getWalletInfo() {
57
- const info = ensureWallet();
76
+ async function getWalletInfo() {
77
+ if (getChain() === "solana") {
78
+ const client = getClient();
79
+ const address = await client.getWalletAddress();
80
+ return {
81
+ address,
82
+ network: "Solana",
83
+ chainId: null,
84
+ currency: "USDC",
85
+ isNew: false,
86
+ explorerUrl: `https://solscan.io/account/${address}`,
87
+ fundingUrl: "https://sol.blockrun.ai"
88
+ };
89
+ }
90
+ const info = ensureEvmWallet();
58
91
  const links = getPaymentLinks(info.address);
59
92
  return {
60
93
  address: info.address,
@@ -62,11 +95,19 @@ function getWalletInfo() {
62
95
  chainId: 8453,
63
96
  currency: "USDC",
64
97
  isNew: info.isNew,
65
- basescanUrl: links.basescan,
98
+ explorerUrl: links.basescan,
66
99
  fundingUrl: links.blockrun
67
100
  };
68
101
  }
69
102
  async function getUsdcBalance(address) {
103
+ if (getChain() === "solana") {
104
+ try {
105
+ const client = getClient();
106
+ return await client.getBalance();
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
70
111
  const USDC_ADDRESS2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
71
112
  const BASE_RPC_URLS = [
72
113
  "https://mainnet.base.org",
@@ -101,7 +142,8 @@ import { z } from "zod";
101
142
  // src/utils/qr.ts
102
143
  import QRCode from "qrcode";
103
144
  import open from "open";
104
- import * as fs from "fs";
145
+ import * as fs2 from "fs";
146
+ import sharp from "sharp";
105
147
 
106
148
  // src/utils/constants.ts
107
149
  import * as path from "path";
@@ -123,22 +165,53 @@ var MODEL_TIERS = {
123
165
  };
124
166
 
125
167
  // src/utils/qr.ts
168
+ var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
126
169
  function getEip681Uri(address, amountUsdc = 1) {
127
170
  const amountWei = Math.floor(amountUsdc * 1e6);
128
171
  return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
129
172
  }
130
- async function generateQrPng(address) {
131
- const eip681Uri = getEip681Uri(address);
132
- if (!fs.existsSync(WALLET_DIR)) {
133
- fs.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
173
+ function getSolanaPayUri(address, amountUsdc = 1) {
174
+ return `solana:${address}?spl-token=${SOLANA_USDC_MINT}&amount=${amountUsdc}&label=BlockRun`;
175
+ }
176
+ function buildSolanaLogoSvg(size) {
177
+ const half = size / 2;
178
+ return `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
179
+ <defs>
180
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
181
+ <stop offset="0%" style="stop-color:#9945FF"/>
182
+ <stop offset="100%" style="stop-color:#14F195"/>
183
+ </linearGradient>
184
+ <clipPath id="c"><circle cx="${half}" cy="${half}" r="${half}"/></clipPath>
185
+ </defs>
186
+ <circle cx="${half}" cy="${half}" r="${half}" fill="url(#g)" clip-path="url(#c)"/>
187
+ <text x="${half}" y="${half + 14}" font-size="40" font-weight="bold" fill="white"
188
+ font-family="Arial,sans-serif" text-anchor="middle">\u25CE</text>
189
+ </svg>`;
190
+ }
191
+ async function overlayLogo(qrBuf, chain, qrSize) {
192
+ if (chain !== "solana") return qrBuf;
193
+ const logoSize = Math.round(qrSize * 0.18);
194
+ const pad = Math.round(logoSize * 0.08);
195
+ 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();
196
+ const totalSize = logoSize + pad * 2;
197
+ const offset = Math.round((qrSize - totalSize) / 2);
198
+ return sharp(qrBuf).composite([{ input: logoBuf, left: offset, top: offset }]).toBuffer();
199
+ }
200
+ async function generateQrPng(address, chain = "base") {
201
+ const uri = chain === "solana" ? getSolanaPayUri(address) : getEip681Uri(address);
202
+ const qrSize = 400;
203
+ if (!fs2.existsSync(WALLET_DIR)) {
204
+ fs2.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
134
205
  }
135
- await QRCode.toFile(QR_FILE, eip681Uri, {
206
+ const qrBuf = await QRCode.toBuffer(uri, {
136
207
  type: "png",
137
- width: 400,
208
+ width: qrSize,
138
209
  margin: 2,
139
210
  errorCorrectionLevel: "H",
140
211
  color: { dark: "#000000", light: "#FFFFFF" }
141
212
  });
213
+ const finalBuf = await overlayLogo(qrBuf, chain, qrSize);
214
+ fs2.writeFileSync(QR_FILE, finalBuf);
142
215
  return QR_FILE;
143
216
  }
144
217
  async function openQrInViewer(qrPath) {
@@ -157,15 +230,17 @@ function formatError(message) {
157
230
  if (isServerError) {
158
231
  errorText += `
159
232
 
160
- This is a temporary API issue. The xAI/Grok API may be experiencing problems.
233
+ This is a temporary API issue. The API may be experiencing problems.
161
234
  Try again in a few minutes, or use a different model (e.g., openai/gpt-4o).`;
162
235
  } else if (isPaymentError) {
236
+ const chain = getChain();
237
+ const network = chain === "solana" ? "Solana" : "Base";
163
238
  errorText += `
164
239
 
165
240
  This error usually means your wallet needs funding.
166
241
  Run blockrun_wallet with action: "setup" to get funding instructions.
167
242
 
168
- Quick fix: Send USDC to your wallet on Base network.`;
243
+ Quick fix: Send USDC to your wallet on ${network} network.`;
169
244
  }
170
245
  return errorText;
171
246
  }
@@ -210,8 +285,9 @@ Do NOT call this for actual AI queries \u2014 use blockrun_chat for that.`,
210
285
  }
211
286
  },
212
287
  async ({ action, budget_action, budget_amount, agent_id, agent_limit }) => {
213
- const info = getWalletInfo();
288
+ const info = await getWalletInfo();
214
289
  const address = info.address;
290
+ const chain = getChain();
215
291
  if (action === "budget") {
216
292
  const budgetAct = budget_action || "check";
217
293
  if (budgetAct === "set") {
@@ -289,10 +365,11 @@ Pass agent_id: "${agent_id}" in any blockrun_* tool call to track and enforce th
289
365
  }
290
366
  if (action === "qr") {
291
367
  try {
292
- const qrPath = await generateQrPng(address);
368
+ const qrPath = await generateQrPng(address, chain);
293
369
  await openQrInViewer(qrPath);
370
+ 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
371
  return {
295
- content: [{ type: "text", text: `QR code opened! Scan with MetaMask to send USDC on Base.
372
+ content: [{ type: "text", text: `QR code opened! ${scanNote}
296
373
 
297
374
  Address: ${address}
298
375
  QR saved: ${qrPath}` }]
@@ -307,14 +384,46 @@ QR saved: ${qrPath}` }]
307
384
  if (action === "setup") {
308
385
  let qrMessage = "";
309
386
  try {
310
- const qrPath = await generateQrPng(address);
387
+ const qrPath = await generateQrPng(address, chain);
311
388
  await openQrInViewer(qrPath);
312
389
  qrMessage = `
313
390
  QR code opened for scanning! (${qrPath})`;
314
391
  } catch {
315
392
  qrMessage = "\n(QR generation failed - use address above)";
316
393
  }
317
- const text2 = `
394
+ const text2 = chain === "solana" ? `
395
+ ================================================================================
396
+ BLOCKRUN WALLET SETUP (SOLANA)
397
+ ================================================================================
398
+
399
+ Your Solana wallet address: ${address}
400
+ ${qrMessage}
401
+
402
+ HOW TO FUND YOUR WALLET:
403
+ ------------------------
404
+
405
+ Option 1: Transfer from Coinbase
406
+ 1. Open Coinbase app or website
407
+ 2. Go to Send/Receive \u2192 Select USDC
408
+ 3. Choose "Solana" network (important!)
409
+ 4. Paste: ${address}
410
+ 5. Send $1-5 to start
411
+
412
+ Option 2: Transfer from any Solana wallet (Phantom, Solflare, Backpack)
413
+ - Send USDC (SPL) to: ${address}
414
+ - Make sure to use Solana network, not EVM
415
+
416
+ Option 3: Bridge from other chains
417
+ https://portalbridge.com \u2192 Bridge USDC to Solana \u2192 Send to address above
418
+
419
+ VERIFY BALANCE: https://solscan.io/account/${address}
420
+
421
+ PRICING (pay per use):
422
+ - GPT-4o: ~$0.005/request | Claude Sonnet: ~$0.003/request
423
+ - Gemini Flash: ~$0.0001/request | Full pricing: https://blockrun.ai/pricing
424
+
425
+ SECURITY: Private key stored at ~/.blockrun/.solana-session (never leaves your machine)
426
+ ================================================================================` : `
318
427
  ================================================================================
319
428
  BLOCKRUN WALLET SETUP
320
429
  ================================================================================
@@ -351,9 +460,10 @@ SECURITY: Private key stored at ~/.blockrun/.session (never leaves your machine)
351
460
  const balance = await getUsdcBalance(address);
352
461
  const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch";
353
462
  const lowBalance = balance !== null && balance < 1;
463
+ const explorerLabel = chain === "solana" ? "Solscan" : "Basescan";
354
464
  const text = `Wallet: ${address}
355
465
  Balance: ${balanceStr}${lowBalance ? " (low - add funds)" : ""}
356
- Network: Base | View: ${info.basescanUrl}
466
+ Network: ${info.network} | View: ${info.explorerUrl}
357
467
  ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions" : ""}`;
358
468
  return {
359
469
  content: [{ type: "text", text }],
@@ -363,7 +473,8 @@ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions
363
473
  network: info.network,
364
474
  chainId: info.chainId,
365
475
  isNew: info.isNew,
366
- basescanUrl: info.basescanUrl
476
+ explorerUrl: info.explorerUrl,
477
+ explorerLabel
367
478
  }
368
479
  };
369
480
  }
@@ -372,6 +483,7 @@ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions
372
483
 
373
484
  // src/tools/chat.ts
374
485
  import { z as z2 } from "zod";
486
+ import { LLMClient as LLMClient2 } from "@blockrun/llm";
375
487
 
376
488
  // src/utils/budget.ts
377
489
  function checkBudget(budget, agentId) {
@@ -448,6 +560,12 @@ Run blockrun_models to see all 41+ models with pricing.`,
448
560
  };
449
561
  }
450
562
  if (routing === "smart") {
563
+ if (!(llm instanceof LLMClient2)) {
564
+ return {
565
+ content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
566
+ isError: true
567
+ };
568
+ }
451
569
  try {
452
570
  const result = await llm.smartChat(message, {
453
571
  system,
@@ -1594,6 +1712,201 @@ Pricing:
1594
1712
  );
1595
1713
  }
1596
1714
 
1715
+ // src/tools/phone.ts
1716
+ import { z as z14 } from "zod";
1717
+ function registerPhoneTool(server) {
1718
+ server.registerTool(
1719
+ "blockrun_phone",
1720
+ {
1721
+ description: `Phone number intelligence, provisioning, and outbound AI voice calls via BlockRun x402.
1722
+
1723
+ Pricing:
1724
+ - lookup: $0.01 \u2014 carrier + line type for any number
1725
+ - lookup_fraud: $0.05 \u2014 + SIM swap / call forwarding signals
1726
+ - numbers_buy: $5.00 \u2014 provision a US/CA number for 30 days
1727
+ - numbers_renew: $5.00 \u2014 extend lease 30 days
1728
+ - numbers_list: $0.001 \u2014 list your wallet-owned numbers
1729
+ - numbers_release: free \u2014 release number back to pool
1730
+ - voice_call: $0.54 flat \u2014 outbound AI voice call via Bland.ai (up to 5 min default)
1731
+ - voice_status: free \u2014 poll call status + transcript
1732
+
1733
+ Voice call flow:
1734
+ 1. blockrun_phone action:"voice_call" to:"+1..." task:"Confirm appointment for..."
1735
+ 2. Returns call_id immediately (call runs async)
1736
+ 3. blockrun_phone action:"voice_status" call_id:"..." to poll until completed
1737
+
1738
+ Voice presets: nat, josh, maya, june, paige, derek, florian
1739
+ Phone numbers use E.164 format: +14155552671`,
1740
+ inputSchema: {
1741
+ action: z14.enum([
1742
+ "lookup",
1743
+ "lookup_fraud",
1744
+ "numbers_buy",
1745
+ "numbers_renew",
1746
+ "numbers_list",
1747
+ "numbers_release",
1748
+ "voice_call",
1749
+ "voice_status"
1750
+ ]).describe("Action to perform"),
1751
+ phone_number: z14.string().optional().describe("E.164 phone number, e.g. +14155552671 (required for lookup, lookup_fraud, numbers_renew, numbers_release)"),
1752
+ country: z14.string().optional().describe("Country for numbers_buy: US or CA (default: US)"),
1753
+ area_code: z14.string().optional().describe("Preferred 3-digit area code for numbers_buy (best effort)"),
1754
+ to: z14.string().optional().describe("Destination E.164 number (required for voice_call)"),
1755
+ task: z14.string().optional().describe("What the AI should do on the call, 10\u20134000 chars (required for voice_call)"),
1756
+ from: z14.string().optional().describe("Your provisioned BlockRun caller ID number (optional for voice_call)"),
1757
+ voice: z14.enum(["nat", "josh", "maya", "june", "paige", "derek", "florian"]).optional().describe("AI voice preset"),
1758
+ max_duration: z14.number().min(1).max(30).optional().describe("Max call duration in minutes (1\u201330, default: 5)"),
1759
+ language: z14.string().optional().describe("Language code, e.g. en-US (default: en-US)"),
1760
+ first_sentence: z14.string().optional().describe("Custom opening line for the AI agent"),
1761
+ wait_for_greeting: z14.boolean().optional().describe("Wait for recipient to speak before AI starts"),
1762
+ call_id: z14.string().optional().describe("Call ID from voice_call response (required for voice_status)")
1763
+ }
1764
+ },
1765
+ async ({
1766
+ action,
1767
+ phone_number,
1768
+ country,
1769
+ area_code,
1770
+ to,
1771
+ task,
1772
+ from,
1773
+ voice,
1774
+ max_duration,
1775
+ language,
1776
+ first_sentence,
1777
+ wait_for_greeting,
1778
+ call_id
1779
+ }) => {
1780
+ const client = getClient();
1781
+ const req = client;
1782
+ const chain = getChain();
1783
+ try {
1784
+ let result;
1785
+ switch (action) {
1786
+ case "lookup": {
1787
+ if (!phone_number) return { content: [{ type: "text", text: "phone_number required (E.164)" }], isError: true };
1788
+ result = await req.requestWithPaymentRaw("/v1/phone/lookup", { phoneNumber: phone_number });
1789
+ break;
1790
+ }
1791
+ case "lookup_fraud": {
1792
+ if (!phone_number) return { content: [{ type: "text", text: "phone_number required (E.164)" }], isError: true };
1793
+ result = await req.requestWithPaymentRaw("/v1/phone/lookup/fraud", { phoneNumber: phone_number });
1794
+ break;
1795
+ }
1796
+ case "numbers_buy": {
1797
+ const body = {};
1798
+ if (country) body.country = country;
1799
+ if (area_code) body.areaCode = area_code;
1800
+ result = await req.requestWithPaymentRaw("/v1/phone/numbers/buy", body);
1801
+ break;
1802
+ }
1803
+ case "numbers_renew": {
1804
+ if (!phone_number) return { content: [{ type: "text", text: "phone_number required (E.164)" }], isError: true };
1805
+ result = await req.requestWithPaymentRaw("/v1/phone/numbers/renew", { phoneNumber: phone_number });
1806
+ break;
1807
+ }
1808
+ case "numbers_list": {
1809
+ result = await req.requestWithPaymentRaw("/v1/phone/numbers/list", {});
1810
+ break;
1811
+ }
1812
+ case "numbers_release": {
1813
+ if (!phone_number) return { content: [{ type: "text", text: "phone_number required (E.164)" }], isError: true };
1814
+ result = await req.requestWithPaymentRaw("/v1/phone/numbers/release", { phoneNumber: phone_number });
1815
+ break;
1816
+ }
1817
+ case "voice_call": {
1818
+ if (!to) return { content: [{ type: "text", text: "to (destination phone number) required" }], isError: true };
1819
+ if (!task) return { content: [{ type: "text", text: "task required (what the AI should do on the call)" }], isError: true };
1820
+ const body = { to, task };
1821
+ if (from) body.from = from;
1822
+ if (voice) body.voice = voice;
1823
+ if (max_duration !== void 0) body.max_duration = max_duration;
1824
+ if (language) body.language = language;
1825
+ if (first_sentence) body.first_sentence = first_sentence;
1826
+ if (wait_for_greeting !== void 0) body.wait_for_greeting = wait_for_greeting;
1827
+ result = await req.requestWithPaymentRaw("/v1/voice/call", body);
1828
+ break;
1829
+ }
1830
+ case "voice_status": {
1831
+ if (!call_id) return { content: [{ type: "text", text: "call_id required" }], isError: true };
1832
+ const apiBase = chain === "solana" ? "https://sol.blockrun.ai/api" : "https://blockrun.ai/api";
1833
+ const resp = await fetch(`${apiBase}/v1/voice/call/${encodeURIComponent(call_id)}`, {
1834
+ signal: AbortSignal.timeout(15e3)
1835
+ });
1836
+ if (!resp.ok) {
1837
+ const err = await resp.text().catch(() => resp.statusText);
1838
+ return { content: [{ type: "text", text: formatError(`voice_status ${resp.status}: ${err}`) }], isError: true };
1839
+ }
1840
+ result = await resp.json();
1841
+ break;
1842
+ }
1843
+ }
1844
+ const text = typeof result === "object" ? JSON.stringify(result, null, 2) : String(result);
1845
+ return {
1846
+ content: [{ type: "text", text }],
1847
+ structuredContent: result
1848
+ };
1849
+ } catch (err) {
1850
+ return { content: [{ type: "text", text: formatError(err instanceof Error ? err.message : String(err)) }], isError: true };
1851
+ }
1852
+ }
1853
+ );
1854
+ }
1855
+
1856
+ // src/tools/surf.ts
1857
+ import { z as z15 } from "zod";
1858
+ function registerSurfTool(server) {
1859
+ server.registerTool(
1860
+ "blockrun_surf",
1861
+ {
1862
+ description: `Unified crypto data via Surf (asksurf.ai) \u2014 84 endpoints, one API.
1863
+
1864
+ 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.
1865
+
1866
+ Pricing (settled in USDC to Surf's Base treasury):
1867
+ - Tier 1 $0.001 \u2014 prices, rankings, lists, news, profiles, simple reads
1868
+ - Tier 2 $0.005 \u2014 order books, candles, search, wallet detail, social aggregates
1869
+ - Tier 3 $0.020 \u2014 raw on-chain SQL, structured queries, surf-1.5 chat
1870
+
1871
+ Common paths (full 84-endpoint catalog in the surf skill):
1872
+ - market/price?symbol=BTC (T1)
1873
+ - exchange/price?pair=BTC-USDT (T1)
1874
+ - prediction-market/polymarket/ranking (T1)
1875
+ - search/web?q=ethereum+pectra+upgrade (T2)
1876
+ - wallet/detail?address=0x... (T2)
1877
+ - social/mindshare?q=ethereum&interval=1d (T2)
1878
+ - onchain/sql + body:{ sql: "SELECT ..." } (T3)
1879
+ - chat/completions + body:{ model:"surf/surf-1.5", messages:[]} (T3, $0.02 flat)
1880
+
1881
+ Method is auto-routed: pass 'body' for POST endpoints; otherwise GET with 'params'.
1882
+ 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`,
1883
+ inputSchema: {
1884
+ path: z15.string().describe("Endpoint path under /v1/surf/, e.g. 'market/price', 'prediction-market/polymarket/ranking', 'wallet/detail', 'onchain/sql', 'chat/completions'"),
1885
+ params: z15.record(z15.string(), z15.string()).optional().describe("Query parameters for GET endpoints, e.g. { symbol: 'BTC' } or { address: '0x...', chain: 'ethereum' }"),
1886
+ body: z15.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.")
1887
+ }
1888
+ },
1889
+ async ({ path: path3, params, body }) => {
1890
+ try {
1891
+ const client = getClient();
1892
+ const cleanPath = path3.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
1893
+ const endpoint = `/v1/surf/${cleanPath}`;
1894
+ const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
1895
+ return {
1896
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1897
+ structuredContent: result
1898
+ };
1899
+ } catch (err) {
1900
+ const errMsg = err instanceof Error ? err.message : String(err);
1901
+ return {
1902
+ content: [{ type: "text", text: formatError(errMsg) }],
1903
+ isError: true
1904
+ };
1905
+ }
1906
+ }
1907
+ );
1908
+ }
1909
+
1597
1910
  // src/mcp-handler.ts
1598
1911
  function initializeMcpServer(server) {
1599
1912
  const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
@@ -1611,17 +1924,22 @@ function initializeMcpServer(server) {
1611
1924
  registerTwitterTool(server);
1612
1925
  registerDexTool(server);
1613
1926
  registerModalTool(server);
1927
+ registerPhoneTool(server);
1928
+ registerSurfTool(server);
1614
1929
  server.registerResource(
1615
1930
  "wallet",
1616
1931
  "blockrun://wallet",
1617
1932
  { description: "Wallet address and status", mimeType: "application/json" },
1618
- async () => ({
1619
- contents: [{
1620
- uri: "blockrun://wallet",
1621
- mimeType: "application/json",
1622
- text: JSON.stringify(getWalletInfo(), null, 2)
1623
- }]
1624
- })
1933
+ async () => {
1934
+ const info = await getWalletInfo();
1935
+ return {
1936
+ contents: [{
1937
+ uri: "blockrun://wallet",
1938
+ mimeType: "application/json",
1939
+ text: JSON.stringify(info, null, 2)
1940
+ }]
1941
+ };
1942
+ }
1625
1943
  );
1626
1944
  server.registerResource(
1627
1945
  "models",
@@ -1647,7 +1965,7 @@ function initializeMcpServer(server) {
1647
1965
  }
1648
1966
 
1649
1967
  // src/utils/key-leak-scanner.ts
1650
- import fs2 from "fs";
1968
+ import fs3 from "fs";
1651
1969
  import path2 from "path";
1652
1970
  import os2 from "os";
1653
1971
  function looksLikeRawPrivateKey(value) {
@@ -1674,8 +1992,8 @@ function walk(obj, file, jsonPath, out) {
1674
1992
  }
1675
1993
  function scanFile(file) {
1676
1994
  try {
1677
- if (!fs2.existsSync(file)) return [];
1678
- const raw = fs2.readFileSync(file, "utf-8");
1995
+ if (!fs3.existsSync(file)) return [];
1996
+ const raw = fs3.readFileSync(file, "utf-8");
1679
1997
  const data = JSON.parse(raw);
1680
1998
  const out = [];
1681
1999
  walk(data, file, "", out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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
  },