@blockrun/mcp 0.14.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.
Files changed (3) hide show
  1. package/README.md +2 -4
  2. package/dist/index.js +147 -413
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -144,7 +144,6 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
144
144
  | `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
145
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
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 |
147
- | `blockrun_x` | X/Twitter — profiles, tweets, followers, mentions, search (AttentionVC) | per call |
148
147
  | `blockrun_exa` | Neural web search (Exa) — research, competitors, papers, URL content | $0.01/query |
149
148
  | `blockrun_search` | Grok Live Search — web + news with citations | ~$0.025 per source |
150
149
  | `blockrun_dex` | Live DEX prices via DexScreener | free |
@@ -186,7 +185,7 @@ What kinds of questions can Claude (or any LLM agent) answer once BlockRun MCP i
186
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
187
186
 
188
187
  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`
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}`
190
189
 
191
190
  7. **Multi-agent research with budget cap**
192
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`
@@ -203,8 +202,7 @@ What kinds of questions can Claude (or any LLM agent) answer once BlockRun MCP i
203
202
  | Exa | Sign up, $20/mo minimum | $0.01/call, no subscription |
204
203
  | Polymarket | Undocumented, rate-limited | $0.001/call, clean JSON |
205
204
  | Surf (asksurf.ai) | Account + monthly plan | $0.001/call, no Surf account, 84 endpoints |
206
- | Twitter/X API | $100–$5000/month | $0.03/page, no approval |
207
- | Multiple sources | 4 accounts, 4 API keys, 4 billing pages | 1 wallet |
205
+ | Multiple sources | 3 accounts, 3 API keys, 3 billing pages | 1 wallet |
208
206
 
209
207
  One wallet. All sources. No dashboards.
210
208
 
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
 
8
8
  // src/utils/wallet.ts
9
9
  import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
10
12
  import {
11
13
  LLMClient,
12
14
  ImageClient,
@@ -19,12 +21,30 @@ import {
19
21
  formatNeedsFundingMessage,
20
22
  SOLANA_WALLET_FILE_PATH
21
23
  } from "@blockrun/llm";
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
+ ];
22
29
  var _evmClient = null;
23
30
  var _imageClient = null;
24
31
  var _priceClient = null;
25
32
  var _evmWalletInfo = null;
26
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 {
41
+ }
42
+ }
43
+ return null;
44
+ }
27
45
  function getChain() {
46
+ const preferred = readChainPreference();
47
+ if (preferred) return preferred;
28
48
  if (process.env.SOLANA_WALLET_KEY) return "solana";
29
49
  try {
30
50
  if (fs.existsSync(SOLANA_WALLET_FILE_PATH)) return "solana";
@@ -146,11 +166,11 @@ import * as fs2 from "fs";
146
166
  import sharp from "sharp";
147
167
 
148
168
  // src/utils/constants.ts
149
- import * as path from "path";
150
- import * as os from "os";
151
- var WALLET_DIR = path.join(os.homedir(), ".blockrun");
152
- var WALLET_FILE = path.join(WALLET_DIR, ".session");
153
- var QR_FILE = path.join(WALLET_DIR, "qr.png");
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");
154
174
  var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
155
175
  var BASE_CHAIN_ID = "8453";
156
176
  var MODEL_TIERS = {
@@ -222,6 +242,32 @@ async function openQrInViewer(qrPath) {
222
242
  }
223
243
 
224
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
+ }
225
271
  function formatError(message) {
226
272
  const msgLower = message.toLowerCase();
227
273
  const isPaymentError = msgLower.includes("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !msgLower.includes("500");
@@ -1124,39 +1170,31 @@ function registerSearchTool(server) {
1124
1170
  server.registerTool(
1125
1171
  "blockrun_search",
1126
1172
  {
1127
- description: `Real-time web, X/Twitter, and news search with AI-summarized results and citations.
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" }
1128
1177
 
1129
- Sources: web, x (X/Twitter), news \u2014 defaults to all three.
1130
- Pricing: ~$0.01/search
1178
+ \`sources\` accepts any subset of ["web","x","news"] (defaults to all three). For tweet-only searches, use ["x"].
1131
1179
 
1132
- Returns a summary with cited sources.`,
1180
+ Full request shape + worked examples in the \`search\` skill (\`skills/search/SKILL.md\`).`,
1133
1181
  inputSchema: {
1134
- query: z7.string().describe("Search query"),
1135
- sources: z7.array(z7.enum(["web", "x", "news"])).optional().describe("Sources to search (default: web + x + news)"),
1136
- max_results: z7.number().optional().default(10).describe("Max results per source (1-20)"),
1137
- from_date: z7.string().optional().describe("Start date filter (YYYY-MM-DD)"),
1138
- 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.")
1139
1184
  }
1140
1185
  },
1141
- async ({ query, sources, max_results, from_date, to_date }) => {
1186
+ async ({ path: path4, body }) => {
1142
1187
  try {
1143
- const llm = getClient();
1144
- const result = await llm.search(query, {
1145
- sources,
1146
- maxResults: max_results,
1147
- fromDate: from_date,
1148
- toDate: to_date
1149
- });
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 ?? {});
1150
1192
  return {
1151
1193
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1152
1194
  structuredContent: result
1153
1195
  };
1154
1196
  } catch (err) {
1155
- const errMsg = err instanceof Error ? err.message : String(err);
1156
- return {
1157
- content: [{ type: "text", text: formatError(errMsg) }],
1158
- isError: true
1159
- };
1197
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1160
1198
  }
1161
1199
  }
1162
1200
  );
@@ -1168,68 +1206,34 @@ function registerExaTool(server) {
1168
1206
  server.registerTool(
1169
1207
  "blockrun_exa",
1170
1208
  {
1171
- description: `Neural web search via Exa. Understands meaning, not just keywords. Great for research.
1209
+ description: `Neural web search via Exa \u2014 understands meaning, not just keywords. Great for research.
1172
1210
 
1173
- Actions:
1174
- - search: Find semantically relevant URLs and metadata ($0.01/call)
1175
- - answer: Get a cited, hallucination-free answer grounded in real web sources ($0.01/call)
1176
- - contents: Fetch full Markdown text from URLs, ready for LLM context ($0.002/URL)
1177
- - similar: Find pages semantically similar to a given URL ($0.01/call)`,
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.`,
1178
1220
  inputSchema: {
1179
- action: z8.enum(["search", "answer", "contents", "similar"]).describe("Action to perform"),
1180
- query: z8.string().optional().describe("Natural language query (for search/answer)"),
1181
- url: z8.string().optional().describe("Reference URL to find similar pages (for similar action)"),
1182
- urls: z8.array(z8.string()).optional().describe("URLs to fetch content from (for contents action, up to 100)"),
1183
- num_results: z8.number().optional().describe("Number of results to return (default: 10)"),
1184
- category: z8.string().optional().describe("Category filter: 'news', 'research paper', 'company', 'tweet', 'github', 'pdf'"),
1185
- include_domains: z8.array(z8.string()).optional().describe("Only search within these domains"),
1186
- 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.")
1187
1223
  }
1188
1224
  },
1189
- async ({ action, query, url, urls, num_results, category, include_domains, exclude_domains }) => {
1225
+ async ({ path: path4, body }) => {
1190
1226
  try {
1191
- const llm = getClient();
1192
- let result;
1193
- const req = llm;
1194
- switch (action) {
1195
- case "search":
1196
- if (!query) throw new Error("query required for search action");
1197
- result = await req.requestWithPaymentRaw("/v1/exa/search", {
1198
- query,
1199
- numResults: num_results,
1200
- category,
1201
- includeDomains: include_domains,
1202
- excludeDomains: exclude_domains
1203
- });
1204
- break;
1205
- case "answer":
1206
- if (!query) throw new Error("query required for answer action");
1207
- result = await req.requestWithPaymentRaw("/v1/exa/answer", { query });
1208
- break;
1209
- case "contents":
1210
- if (!urls?.length) throw new Error("urls array required for contents action");
1211
- result = await req.requestWithPaymentRaw("/v1/exa/contents", { urls });
1212
- break;
1213
- case "similar":
1214
- if (!url) throw new Error("url required for similar action");
1215
- result = await req.requestWithPaymentRaw("/v1/exa/find-similar", {
1216
- url,
1217
- numResults: num_results
1218
- });
1219
- break;
1220
- default:
1221
- throw new Error(`Unknown action: ${action}`);
1222
- }
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 ?? {});
1223
1231
  return {
1224
1232
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1225
1233
  structuredContent: result
1226
1234
  };
1227
1235
  } catch (err) {
1228
- const errMsg = err instanceof Error ? err.message : String(err);
1229
- return {
1230
- content: [{ type: "text", text: formatError(errMsg) }],
1231
- isError: true
1232
- };
1236
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1233
1237
  }
1234
1238
  }
1235
1239
  );
@@ -1298,10 +1302,10 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
1298
1302
  body: z9.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)")
1299
1303
  }
1300
1304
  },
1301
- async ({ path: path3, params, body }) => {
1305
+ async ({ path: path4, params, body }) => {
1302
1306
  try {
1303
1307
  const llm = getClient();
1304
- const result = body ? await llm.pmQuery(path3, body) : await llm.pm(path3, params);
1308
+ const result = body ? await llm.pmQuery(path4, body) : await llm.pm(path4, params);
1305
1309
  return {
1306
1310
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1307
1311
  structuredContent: result
@@ -1420,132 +1424,8 @@ Examples:
1420
1424
  );
1421
1425
  }
1422
1426
 
1423
- // src/tools/twitter.ts
1424
- import { z as z11 } from "zod";
1425
- var ACTION2 = z11.enum([
1426
- "user_lookup",
1427
- "user_info",
1428
- "followers",
1429
- "followings",
1430
- "verified_followers",
1431
- "user_tweets",
1432
- "user_mentions",
1433
- "tweet_lookup",
1434
- "tweet_replies",
1435
- "tweet_thread",
1436
- "search"
1437
- ]);
1438
- function registerTwitterTool(server) {
1439
- server.registerTool(
1440
- "blockrun_x",
1441
- {
1442
- description: `Structured X/Twitter data via AttentionVC partner API.
1443
-
1444
- Actions:
1445
- - user_lookup (usernames: string | string[])
1446
- - user_info (username)
1447
- - followers (username, cursor?)
1448
- - followings (username, cursor?)
1449
- - verified_followers (user_id, cursor?)
1450
- - user_tweets (username, includeReplies?, cursor?)
1451
- - user_mentions (username, sinceTime?, untilTime?, cursor?)
1452
- - tweet_lookup (tweet_ids: string | string[])
1453
- - tweet_replies (tweet_id, cursor?, queryType?)
1454
- - tweet_thread (tweet_id, cursor?)
1455
- - search (query, queryType?, cursor?)
1456
-
1457
- Paid per request via x402; prices scale with the endpoint (e.g. user_lookup ~ $0.02, followers ~ $0.05/page).`,
1458
- inputSchema: {
1459
- action: ACTION2,
1460
- usernames: z11.union([z11.string(), z11.array(z11.string())]).optional(),
1461
- username: z11.string().optional(),
1462
- user_id: z11.string().optional(),
1463
- tweet_id: z11.string().optional(),
1464
- tweet_ids: z11.union([z11.string(), z11.array(z11.string())]).optional(),
1465
- query: z11.string().optional(),
1466
- queryType: z11.enum(["Latest", "Top", "Default"]).optional(),
1467
- cursor: z11.string().optional(),
1468
- sinceTime: z11.string().optional(),
1469
- untilTime: z11.string().optional(),
1470
- includeReplies: z11.boolean().optional()
1471
- }
1472
- },
1473
- async (args) => {
1474
- try {
1475
- const llm = getClient();
1476
- const a = args.action;
1477
- const require2 = (value, name) => {
1478
- if (value === void 0 || value === null || value === "") {
1479
- throw new Error(`${name} is required for action='${a}'`);
1480
- }
1481
- return value;
1482
- };
1483
- let result;
1484
- switch (a) {
1485
- case "user_lookup":
1486
- result = await llm.xUserLookup(require2(args.usernames, "usernames"));
1487
- break;
1488
- case "user_info":
1489
- result = await llm.xUserInfo(require2(args.username, "username"));
1490
- break;
1491
- case "followers":
1492
- result = await llm.xFollowers(require2(args.username, "username"), args.cursor);
1493
- break;
1494
- case "followings":
1495
- result = await llm.xFollowings(require2(args.username, "username"), args.cursor);
1496
- break;
1497
- case "verified_followers":
1498
- result = await llm.xVerifiedFollowers(require2(args.user_id, "user_id"), args.cursor);
1499
- break;
1500
- case "user_tweets":
1501
- result = await llm.xUserTweets(
1502
- require2(args.username, "username"),
1503
- args.includeReplies,
1504
- args.cursor
1505
- );
1506
- break;
1507
- case "user_mentions":
1508
- result = await llm.xUserMentions(
1509
- require2(args.username, "username"),
1510
- args.sinceTime,
1511
- args.untilTime,
1512
- args.cursor
1513
- );
1514
- break;
1515
- case "tweet_lookup":
1516
- result = await llm.xTweetLookup(require2(args.tweet_ids, "tweet_ids"));
1517
- break;
1518
- case "tweet_replies":
1519
- result = await llm.xTweetReplies(
1520
- require2(args.tweet_id, "tweet_id"),
1521
- args.queryType,
1522
- args.cursor
1523
- );
1524
- break;
1525
- case "tweet_thread":
1526
- result = await llm.xTweetThread(require2(args.tweet_id, "tweet_id"), args.cursor);
1527
- break;
1528
- case "search":
1529
- result = await llm.xSearch(require2(args.query, "query"), args.queryType, args.cursor);
1530
- break;
1531
- }
1532
- return {
1533
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1534
- structuredContent: result
1535
- };
1536
- } catch (err) {
1537
- const msg = err instanceof Error ? err.message : String(err);
1538
- return {
1539
- content: [{ type: "text", text: formatError(msg) }],
1540
- isError: true
1541
- };
1542
- }
1543
- }
1544
- );
1545
- }
1546
-
1547
1427
  // src/tools/dex.ts
1548
- import { z as z12 } from "zod";
1428
+ import { z as z11 } from "zod";
1549
1429
  function registerDexTool(server) {
1550
1430
  server.registerTool(
1551
1431
  "blockrun_dex",
@@ -1562,10 +1442,10 @@ Examples:
1562
1442
  blockrun_dex({ token: "So11...xxx" }) -> Get specific token data
1563
1443
  blockrun_dex({ symbol: "PEPE" }) -> Search by symbol`,
1564
1444
  inputSchema: {
1565
- query: z12.string().optional().describe("Search query (token name, symbol, or address)"),
1566
- token: z12.string().optional().describe("Token address for direct lookup"),
1567
- symbol: z12.string().optional().describe("Token symbol to search"),
1568
- chain: z12.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
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.)")
1569
1449
  }
1570
1450
  },
1571
1451
  async ({ query, token, symbol, chain }) => {
@@ -1626,235 +1506,91 @@ ${lines.join("\n\n")}` }],
1626
1506
  }
1627
1507
 
1628
1508
  // src/tools/modal.ts
1629
- import { z as z13 } from "zod";
1630
- var MODAL_GPU_TYPES = ["T4", "L4", "A10G", "A100", "A100-80GB", "H100"];
1509
+ import { z as z12 } from "zod";
1631
1510
  function registerModalTool(server) {
1632
1511
  server.registerTool(
1633
1512
  "blockrun_modal",
1634
1513
  {
1635
- 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.
1636
1515
 
1637
- Use this when you need:
1638
- - a disposable remote container
1639
- - GPU access
1640
- - a clean environment that will not affect the local machine
1641
- - 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.
1642
1517
 
1643
- Prefer local tools for normal repo work. Modal is best for isolation or remote execution.
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)
1644
1523
 
1645
- Pricing:
1646
- - create: $0.01
1647
- - exec/status/terminate: $0.001 each`,
1524
+ Full action shapes + GPU type details in the \`modal\` skill.`,
1648
1525
  inputSchema: {
1649
- action: z13.enum(["create", "exec", "status", "terminate"]).describe(
1650
- "Sandbox action to perform"
1651
- ),
1652
- sandbox_id: z13.string().optional().describe("Sandbox ID returned by a previous create"),
1653
- command: z13.array(z13.string()).optional().describe('Command array for exec, for example ["python", "-c", "print(2+2)"]'),
1654
- image: z13.string().optional().describe("Container image for create (default: python:3.11)"),
1655
- timeout: z13.number().optional().describe("Timeout in seconds for create or exec"),
1656
- cpu: z13.number().optional().describe("CPU cores for create"),
1657
- memory: z13.number().optional().describe("Memory in MB for create"),
1658
- gpu: z13.enum(MODAL_GPU_TYPES).optional().describe("Optional GPU type for create"),
1659
- 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.")
1660
1528
  }
1661
1529
  },
1662
- async ({ action, sandbox_id, command, image, timeout, cpu, memory, gpu, setup_commands }) => {
1530
+ async ({ path: path4, body }) => {
1663
1531
  try {
1664
- const llm = getClient();
1665
- const req = llm;
1666
- let result;
1667
- switch (action) {
1668
- case "create":
1669
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/create", {
1670
- image,
1671
- timeout,
1672
- cpu,
1673
- memory,
1674
- gpu,
1675
- setup_commands
1676
- });
1677
- break;
1678
- case "exec":
1679
- if (!sandbox_id) throw new Error("sandbox_id required for exec action");
1680
- if (!command?.length) throw new Error("command array required for exec action");
1681
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/exec", {
1682
- sandbox_id,
1683
- command,
1684
- timeout
1685
- });
1686
- break;
1687
- case "status":
1688
- if (!sandbox_id) throw new Error("sandbox_id required for status action");
1689
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/status", { sandbox_id });
1690
- break;
1691
- case "terminate":
1692
- if (!sandbox_id) throw new Error("sandbox_id required for terminate action");
1693
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/terminate", {
1694
- sandbox_id
1695
- });
1696
- break;
1697
- default:
1698
- throw new Error(`Unknown action: ${String(action)}`);
1699
- }
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 ?? {});
1700
1536
  return {
1701
1537
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1702
1538
  structuredContent: result
1703
1539
  };
1704
1540
  } catch (err) {
1705
- const errMsg = err instanceof Error ? err.message : String(err);
1706
- return {
1707
- content: [{ type: "text", text: formatError(errMsg) }],
1708
- isError: true
1709
- };
1541
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1710
1542
  }
1711
1543
  }
1712
1544
  );
1713
1545
  }
1714
1546
 
1715
1547
  // src/tools/phone.ts
1716
- import { z as z14 } from "zod";
1548
+ import { z as z13 } from "zod";
1717
1549
  function registerPhoneTool(server) {
1718
1550
  server.registerTool(
1719
1551
  "blockrun_phone",
1720
1552
  {
1721
- description: `Phone number intelligence, provisioning, and outbound AI voice calls via BlockRun x402.
1553
+ description: `Phone-number intelligence, US/CA number provisioning, and outbound AI voice calls.
1722
1554
 
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`,
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.`,
1740
1570
  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)")
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}).")
1763
1573
  }
1764
1574
  },
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();
1575
+ async ({ path: path4, body }) => {
1783
1576
  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);
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);
1845
1581
  return {
1846
- content: [{ type: "text", text }],
1582
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1847
1583
  structuredContent: result
1848
1584
  };
1849
1585
  } catch (err) {
1850
- return { content: [{ type: "text", text: formatError(err instanceof Error ? err.message : String(err)) }], isError: true };
1586
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1851
1587
  }
1852
1588
  }
1853
1589
  );
1854
1590
  }
1855
1591
 
1856
1592
  // src/tools/surf.ts
1857
- import { z as z15 } from "zod";
1593
+ import { z as z14 } from "zod";
1858
1594
  function registerSurfTool(server) {
1859
1595
  server.registerTool(
1860
1596
  "blockrun_surf",
@@ -1881,15 +1617,15 @@ Common paths (full 84-endpoint catalog in the surf skill):
1881
1617
  Method is auto-routed: pass 'body' for POST endpoints; otherwise GET with 'params'.
1882
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`,
1883
1619
  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.")
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.")
1887
1623
  }
1888
1624
  },
1889
- async ({ path: path3, params, body }) => {
1625
+ async ({ path: path4, params, body }) => {
1890
1626
  try {
1891
1627
  const client = getClient();
1892
- const cleanPath = path3.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
1628
+ const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
1893
1629
  const endpoint = `/v1/surf/${cleanPath}`;
1894
1630
  const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
1895
1631
  return {
@@ -1897,9 +1633,8 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
1897
1633
  structuredContent: result
1898
1634
  };
1899
1635
  } catch (err) {
1900
- const errMsg = err instanceof Error ? err.message : String(err);
1901
1636
  return {
1902
- content: [{ type: "text", text: formatError(errMsg) }],
1637
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
1903
1638
  isError: true
1904
1639
  };
1905
1640
  }
@@ -1921,7 +1656,6 @@ function initializeMcpServer(server) {
1921
1656
  registerExaTool(server);
1922
1657
  registerMarketsTool(server);
1923
1658
  registerPriceTool(server);
1924
- registerTwitterTool(server);
1925
1659
  registerDexTool(server);
1926
1660
  registerModalTool(server);
1927
1661
  registerPhoneTool(server);
@@ -1966,8 +1700,8 @@ function initializeMcpServer(server) {
1966
1700
 
1967
1701
  // src/utils/key-leak-scanner.ts
1968
1702
  import fs3 from "fs";
1969
- import path2 from "path";
1970
- import os2 from "os";
1703
+ import path3 from "path";
1704
+ import os3 from "os";
1971
1705
  function looksLikeRawPrivateKey(value) {
1972
1706
  if (typeof value !== "string") return false;
1973
1707
  if (/^0x[0-9a-fA-F]{64}$/.test(value)) return true;
@@ -2003,12 +1737,12 @@ function scanFile(file) {
2003
1737
  }
2004
1738
  }
2005
1739
  function warnOnLeakedKeys() {
2006
- const home = os2.homedir();
1740
+ const home = os3.homedir();
2007
1741
  const candidates = [
2008
- path2.join(home, ".claude.json"),
2009
- path2.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
2010
- path2.join(home, ".config", "claude", "claude_desktop_config.json"),
2011
- path2.join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
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")
2012
1746
  ];
2013
1747
  const findings = [];
2014
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.14.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",