@blockrun/mcp 0.24.0 → 0.24.2

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 +4 -4
  2. package/dist/index.js +90 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -77,7 +77,7 @@ Prompts and a worked example for these are in [`skills/image-prompting/SKILL.md`
77
77
 
78
78
  ## Prerequisites
79
79
 
80
- - **Node.js ≥ 18** (`node -v`)
80
+ - **Node.js ≥ 20.19** (`node -v`)
81
81
  - **~$5 USDC** on Base or Solana (the server auto-creates a wallet on first run; see [Fund your wallet](#fund-your-wallet))
82
82
  - **An MCP client**: Claude Code, Claude Desktop, Cursor, Windsurf, or ChatGPT Desktop
83
83
 
@@ -175,7 +175,7 @@ Then send USDC (SPL) on the **Solana** network — from Coinbase (pick "Solana")
175
175
 
176
176
  **Base-only — these fall back to Base regardless of active chain:**
177
177
 
178
- - Tools: `blockrun_image`, `blockrun_music`, `blockrun_speech`, `blockrun_video`, paid stock `blockrun_price`. In Solana mode they return a "switch to Base" message instead of charging.
178
+ - Tools: `blockrun_image`, `blockrun_music`, `blockrun_speech`, `blockrun_video`, paid `blockrun_realface` (enroll/portrait), paid stock `blockrun_price`. In Solana mode they return a "switch to Base" message instead of charging.
179
179
  - `blockrun_chat routing:"smart"` (ClawRouter) and native Anthropic (`claude-*`) passthrough — on Solana, pass `model:` or `mode:` explicitly.
180
180
 
181
181
  > Advanced: chain selection can also be forced before startup via files/env (`~/.blockrun/.chain`, `SOLANA_WALLET_KEY`) — see [Environment Variables](#environment-variables). The `action:"chain"` command above is the recommended path.
@@ -196,7 +196,7 @@ Then send USDC (SPL) on the **Solana** network — from Coinbase (pick "Solana")
196
196
  | `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 |
197
197
  | `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 |
198
198
  | `blockrun_exa` | Neural web search (Exa) — research, competitors, papers, URL content | $0.01/query |
199
- | `blockrun_search` | Grok Live Search — web + news with citations | $0.025 × max_results (default 10) |
199
+ | `blockrun_search` | Grok Live Search — web + X/Twitter + news with citations | $0.025 × max_results (default 10) |
200
200
  | `blockrun_dex` | Live DEX prices via DexScreener | free |
201
201
  | `blockrun_rpc` | Raw JSON-RPC on 40+ chains (Ethereum, Base, Solana, Bitcoin, Sui, NEAR, ...) via Tatum gateway — eth_call, balances, blocks, logs | $0.002/call |
202
202
  | `blockrun_defi` | DefiLlama — protocol TVL, chain TVL, yield pools (APY), token prices | $0.001–0.005/call |
@@ -330,7 +330,7 @@ blockrun_wallet action:"setup" # funding instructions for the a
330
330
 
331
331
  *Advanced (force a chain before startup, e.g. in CI):* `echo solana > ~/.blockrun/.chain` then set `SOLANA_WALLET_KEY` or create `~/.blockrun/.solana-session`; `echo base > ~/.blockrun/.chain` reuses the existing `.session` (same Base wallet). These edit the same preference file that `action:"chain"` writes — prefer the tool unless you need pre-startup control.
332
332
 
333
- Some media and paid market-data tools still settle on Base only: `blockrun_image`, `blockrun_music`, `blockrun_speech`, `blockrun_video`, and paid stock `blockrun_price` calls — plus `blockrun_chat routing:"smart"` and native Anthropic (`claude-*`) passthrough. In Solana mode these return a "switch to Base" message instead of charging.
333
+ Some media and paid market-data tools still settle on Base only: `blockrun_image`, `blockrun_music`, `blockrun_speech`, `blockrun_video`, paid `blockrun_realface` (enroll/portrait), and paid stock `blockrun_price` calls — plus `blockrun_chat routing:"smart"` and native Anthropic (`claude-*`) passthrough. In Solana mode these return a "switch to Base" message instead of charging.
334
334
 
335
335
  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.
336
336
 
package/dist/index.js CHANGED
@@ -80,7 +80,9 @@ function getChain() {
80
80
  if (preferred) return preferred;
81
81
  if (process.env.SOLANA_WALLET_KEY) return "solana";
82
82
  try {
83
- if (fs.existsSync(SOLANA_WALLET_FILE_PATH)) return "solana";
83
+ if (fs.existsSync(SOLANA_WALLET_FILE_PATH) && fs.readFileSync(SOLANA_WALLET_FILE_PATH, "utf-8").trim()) {
84
+ return "solana";
85
+ }
84
86
  } catch {
85
87
  }
86
88
  return "base";
@@ -154,6 +156,10 @@ function buildClientWithTimeout(timeoutMs) {
154
156
  const privateKey = getOrCreateWalletKey();
155
157
  return new LLMClient({ privateKey, timeout: timeoutMs });
156
158
  }
159
+ function buildClient() {
160
+ if (getChain() === "solana") return buildSolanaClient();
161
+ return new LLMClient({ privateKey: getOrCreateWalletKey() });
162
+ }
157
163
  function getAnthropicClient() {
158
164
  if (!_anthropicClient) {
159
165
  const privateKey = getOrCreateWalletKey();
@@ -230,7 +236,8 @@ async function getBaseUsdcBalance(address) {
230
236
  const response = await fetch(rpcUrl, {
231
237
  method: "POST",
232
238
  headers: { "Content-Type": "application/json" },
233
- body: JSON.stringify(data)
239
+ body: JSON.stringify(data),
240
+ signal: AbortSignal.timeout(8e3)
234
241
  });
235
242
  const result = await response.json();
236
243
  const usd = parseBaseUsdcCallResult(result.result);
@@ -248,7 +255,7 @@ async function getChainBalance(chain, address) {
248
255
  // src/utils/model-cache.ts
249
256
  var CACHE_TTL_MS = 5 * 60 * 1e3;
250
257
  async function loadModels(llm, cache) {
251
- if (!cache.models) {
258
+ if (cache.models === null || cache.models.length === 0) {
252
259
  cache.models = llm.listAllModels ? await llm.listAllModels() : await llm.listModels();
253
260
  setTimeout(() => {
254
261
  cache.models = null;
@@ -800,11 +807,14 @@ function isAnthropicModel(model) {
800
807
  return /^anthropic\//i.test(id) || /^claude-/i.test(id);
801
808
  }
802
809
  function parseDataUri(url) {
803
- const match = /^data:(image\/(?:jpeg|png|gif|webp));base64,(.+)$/i.exec(url.trim());
810
+ const match = /^data:image\/([a-z0-9.+-]+)(?:;[^;,]+)*;base64,(.+)$/i.exec(url.trim());
804
811
  if (!match) return null;
812
+ let subtype = match[1].toLowerCase();
813
+ if (subtype === "jpg") subtype = "jpeg";
814
+ if (!["jpeg", "png", "gif", "webp"].includes(subtype)) return null;
805
815
  return {
806
816
  type: "base64",
807
- media_type: match[1].toLowerCase(),
817
+ media_type: `image/${subtype}`,
808
818
  data: match[2]
809
819
  };
810
820
  }
@@ -817,9 +827,13 @@ function toAnthropicContent(content) {
817
827
  } else if (part.type === "image_url") {
818
828
  const url = part.image_url?.url;
819
829
  if (!url) continue;
820
- const base64 = parseDataUri(url);
821
- const source = base64 ? base64 : { type: "url", url };
822
- blocks.push({ type: "image", source });
830
+ if (/^data:/i.test(url)) {
831
+ const base64 = parseDataUri(url);
832
+ if (!base64) continue;
833
+ blocks.push({ type: "image", source: base64 });
834
+ } else {
835
+ blocks.push({ type: "image", source: { type: "url", url } });
836
+ }
823
837
  }
824
838
  }
825
839
  return blocks;
@@ -855,7 +869,9 @@ async function handleAnthropicNative(args) {
855
869
  if (text) systemParts.push(text);
856
870
  continue;
857
871
  }
858
- apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
872
+ const content2 = toAnthropicContent(m.content);
873
+ if (Array.isArray(content2) && content2.length === 0) continue;
874
+ apiMessages.push({ role: m.role, content: content2 });
859
875
  }
860
876
  if (responseFormat?.type === "json_object") {
861
877
  systemParts.push("Respond with only valid JSON. Do not wrap it in markdown code fences or add any prose before or after.");
@@ -926,10 +942,10 @@ ${thinkingText}` });
926
942
  }
927
943
 
928
944
  // src/tools/chat.ts
929
- function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
945
+ function estimateChatCost(maxTokens, mode, model, routing, routingProfile, thinkingBudget) {
930
946
  if (mode === "free") return 0;
931
947
  if (model?.startsWith("nvidia/")) return 0;
932
- const out = Math.max(maxTokens ?? 1024, 256);
948
+ const out = Math.max((maxTokens ?? 1024) + (thinkingBudget ?? 0), 256);
933
949
  const frontierReserve = Math.max(0.01, out / 1e6 * 20);
934
950
  if (routing === "smart") {
935
951
  switch (routingProfile) {
@@ -982,7 +998,7 @@ Run blockrun_models to see all available models with pricing.`,
982
998
  stop: z2.array(z2.string()).max(4).optional().describe("Up to 4 stop sequences; generation halts when any is produced"),
983
999
  thinking: z2.object({
984
1000
  type: z2.literal("enabled"),
985
- budget_tokens: z2.number().min(1).describe("Tokens Claude may spend reasoning before answering. max_tokens is auto-raised above this if needed.")
1001
+ budget_tokens: z2.number().int().min(1).max(1e5).describe("Tokens Claude may spend reasoning before answering (1\u2013100000). max_tokens is auto-raised above this if needed; counts toward the budget reserve.")
986
1002
  }).optional().describe("Anthropic extended thinking. Only honored for anthropic/claude-* models \u2014 these go direct to the native /v1/messages endpoint and the response includes verbatim type:'thinking' blocks with their original signature. Ignored for non-Claude models (no native thinking channel)."),
987
1003
  agent_id: z2.string().optional().describe("Agent identifier. If a budget was delegated for this agent_id via blockrun_wallet action:'delegate', spending is tracked and enforced. The agent is hard-stopped when its budget is exhausted."),
988
1004
  messages: z2.array(z2.object({
@@ -998,9 +1014,9 @@ Run blockrun_models to see all available models with pricing.`,
998
1014
  }
999
1015
  },
1000
1016
  async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, response_format, stop, thinking, agent_id, messages }) => {
1001
- const llm = getClient();
1017
+ const llm = buildClient();
1002
1018
  const responseFormat = response_format ? { type: response_format } : void 0;
1003
- const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
1019
+ const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile, thinking?.budget_tokens);
1004
1020
  const gate = reserveBudget(budget, agent_id, estimatedCost);
1005
1021
  if (!gate.allowed) {
1006
1022
  return {
@@ -1215,6 +1231,41 @@ ${lines.join("\n")}` }],
1215
1231
  // src/tools/image.ts
1216
1232
  import { z as z4 } from "zod";
1217
1233
  import { PaymentError } from "@blockrun/llm";
1234
+
1235
+ // src/utils/ssrf.ts
1236
+ function ipv4Blocked(host) {
1237
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
1238
+ if (!m) return null;
1239
+ const o = m.slice(1).map(Number);
1240
+ if (o.some((n) => n > 255)) return true;
1241
+ const [a, b] = o;
1242
+ return a === 0 || // 0.0.0.0/8
1243
+ a === 127 || // loopback
1244
+ a === 10 || // private
1245
+ a === 172 && b >= 16 && b <= 31 || // private
1246
+ a === 192 && b === 168 || // private
1247
+ a === 169 && b === 254 || // link-local (incl. metadata)
1248
+ a === 100 && b >= 64 && b <= 127;
1249
+ }
1250
+ function isBlockedFetchHost(hostname) {
1251
+ let host = hostname.trim().toLowerCase();
1252
+ if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
1253
+ if (!host) return true;
1254
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
1255
+ if (host.endsWith(".internal") || host.endsWith(".local")) return true;
1256
+ const v4 = ipv4Blocked(host);
1257
+ if (v4 !== null) return v4;
1258
+ if (host.includes(":")) {
1259
+ if (host === "::1" || host === "::") return true;
1260
+ if (host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80")) return true;
1261
+ const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(host);
1262
+ if (mapped) return ipv4Blocked(mapped[1]) === true;
1263
+ return false;
1264
+ }
1265
+ return false;
1266
+ }
1267
+
1268
+ // src/tools/image.ts
1218
1269
  import { readFile } from "fs/promises";
1219
1270
  var REFERENCE_IMAGE_MAX_BYTES = 4e6;
1220
1271
  var IMAGE_EXT_MIME = {
@@ -1230,7 +1281,22 @@ async function toImageDataUri(ref) {
1230
1281
  const ctrl = new AbortController();
1231
1282
  const timeout = setTimeout(() => ctrl.abort(), 3e4);
1232
1283
  try {
1233
- const res = await fetch(ref, { signal: ctrl.signal });
1284
+ let url = ref;
1285
+ let res;
1286
+ for (let hop = 0; ; hop++) {
1287
+ const host = new URL(url).hostname;
1288
+ if (isBlockedFetchHost(host)) {
1289
+ throw new Error(`refusing to fetch a private/loopback/link-local address: ${host}`);
1290
+ }
1291
+ res = await fetch(url, { signal: ctrl.signal, redirect: "manual" });
1292
+ const location = res.headers.get("location");
1293
+ if (res.status >= 300 && res.status < 400 && location) {
1294
+ if (hop >= 5) throw new Error("too many redirects");
1295
+ url = new URL(location, url).toString();
1296
+ continue;
1297
+ }
1298
+ break;
1299
+ }
1234
1300
  if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
1235
1301
  const mime2 = (res.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
1236
1302
  if (!mime2.startsWith("image/")) throw new Error(`URL returned non-image content-type: ${mime2 || "(none)"}`);
@@ -2012,6 +2078,13 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
2012
2078
  const paymentRequired = parsePaymentRequired3(prHeader);
2013
2079
  const details = extractPaymentDetails3(paymentRequired);
2014
2080
  const settledUsd = amountToUsd(details.amount);
2081
+ if (settledUsd !== null && settledUsd > estimatedCost) {
2082
+ gate?.release();
2083
+ gate = reserveBudget(budget, agent_id, settledUsd);
2084
+ if (!gate.allowed) {
2085
+ return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2086
+ }
2087
+ }
2015
2088
  const paymentPayload = await createPaymentPayload3(
2016
2089
  privateKey,
2017
2090
  account.address,
@@ -2088,7 +2161,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
2088
2161
  const lines = [
2089
2162
  `\u{1F3AC} Video ready!`,
2090
2163
  `URL: ${completed.url}`,
2091
- `Duration: ${completed.duration_seconds ? `${completed.duration_seconds}s` : "8s"}`,
2164
+ `Duration: ${completed.duration_seconds ?? billedSeconds}s`,
2092
2165
  `Model: ${completed.modelReturned || selectedModel}`,
2093
2166
  ...completed.backed_up ? [`Backed up to BlockRun storage (URL is permanent)`] : completed.source_url ? [`Source URL: ${completed.source_url}`] : [],
2094
2167
  ...completed.request_id ? [`Request ID: ${completed.request_id}`] : [],
@@ -2887,7 +2960,7 @@ Examples:
2887
2960
  isError: true
2888
2961
  };
2889
2962
  }
2890
- const response = await fetch(url);
2963
+ const response = await fetchWithTimeout(url, {}, 8e3);
2891
2964
  if (!response.ok) {
2892
2965
  throw new Error(`DexScreener API error: ${response.status}`);
2893
2966
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
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",