@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.
- package/README.md +4 -4
- package/dist/index.js +90 -17
- 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 ≥
|
|
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)
|
|
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 (
|
|
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:
|
|
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:
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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",
|