@blockrun/mcp 0.21.4 → 0.22.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.
- package/README.md +56 -4
- package/dist/index.js +179 -117
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -14,6 +14,15 @@ Wallet auto-created. Fund with $5 USDC. Ask Claude anything.
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## Documentation
|
|
18
|
+
|
|
19
|
+
Full docs: **https://blockrun.ai/docs**
|
|
20
|
+
|
|
21
|
+
- MCP tools & setup: https://blockrun.ai/docs/mcp/blockrun-mcp
|
|
22
|
+
- All BlockRun SDKs & APIs: https://blockrun.ai/docs
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
17
26
|
## What changes
|
|
18
27
|
|
|
19
28
|
Before BlockRun, Claude can't answer:
|
|
@@ -84,6 +93,25 @@ claude mcp add blockrun -s user -- npx -y @blockrun/mcp@latest
|
|
|
84
93
|
The `-s user` flag installs globally (available in every project). The `--` separator
|
|
85
94
|
ensures `-y` is passed to `npx`, not parsed by `claude mcp add`.
|
|
86
95
|
|
|
96
|
+
**Tool profiles** — expose a trimmed tool set so the client loads fewer schemas into
|
|
97
|
+
context. Pass `--profile <name>` (or set `BLOCKRUN_MCP_PROFILE`); omit it for the full set.
|
|
98
|
+
|
|
99
|
+
| Profile | Tools |
|
|
100
|
+
|---------|-------|
|
|
101
|
+
| `full` *(default)* | everything |
|
|
102
|
+
| `media` | `wallet` `models` `image` `video` `realface` `music` `speech` |
|
|
103
|
+
| `trading` | `wallet` `price` `dex` `markets` `surf` `defi` `rpc` |
|
|
104
|
+
| `research` | `wallet` `models` `chat` `search` `exa` `surf` |
|
|
105
|
+
| `chat` | `wallet` `models` `chat` |
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# e.g. a media-only install
|
|
109
|
+
claude mcp add blockrun-media -s user -- npx -y @blockrun/mcp@latest --profile media
|
|
110
|
+
# or a trading-only install
|
|
111
|
+
claude mcp add blockrun-trading -s user -- npx -y @blockrun/mcp@latest --profile trading
|
|
112
|
+
```
|
|
113
|
+
Equivalent via env: `BLOCKRUN_MCP_PROFILE=trading`. An unknown profile name falls back to `full`. `modal` and `phone` are available in the `full` profile only.
|
|
114
|
+
|
|
87
115
|
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
88
116
|
```json
|
|
89
117
|
{
|
|
@@ -117,7 +145,7 @@ ensures `-y` is passed to `npx`, not parsed by `claude mcp add`.
|
|
|
117
145
|
|
|
118
146
|
## Fund your wallet
|
|
119
147
|
|
|
120
|
-
Run `blockrun_wallet` to see your address. Send USDC on Base
|
|
148
|
+
Run `blockrun_wallet` to see your address. The server pays on **Base** by default. Send USDC on Base:
|
|
121
149
|
|
|
122
150
|
| Method | Steps |
|
|
123
151
|
|--------|-------|
|
|
@@ -126,6 +154,24 @@ Run `blockrun_wallet` to see your address. Send USDC on Base.
|
|
|
126
154
|
|
|
127
155
|
$5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~10 Seedance 1.5-pro clips (5s @ 720p+audio, ~$0.46 each).
|
|
128
156
|
|
|
157
|
+
### Pay on Solana
|
|
158
|
+
|
|
159
|
+
Prefer to pay in USDC on Solana? It's two tool calls — no env vars, no file editing, no restart:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
blockrun_wallet action:"chain" chain:"solana" # provisions + activates the Solana wallet
|
|
163
|
+
blockrun_wallet action:"setup" # shows the Solana address + funding QR
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Then send USDC (SPL) on the **Solana** network — from Coinbase (pick "Solana"), Phantom, Solflare, or Backpack. Switch back anytime with `blockrun_wallet action:"chain" chain:"base"`. The server keeps both wallets; switching just changes which one pays.
|
|
167
|
+
|
|
168
|
+
**Base-only — these fall back to Base regardless of active chain:**
|
|
169
|
+
|
|
170
|
+
- 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.
|
|
171
|
+
- `blockrun_chat routing:"smart"` (ClawRouter) and native Anthropic (`claude-*`) passthrough — on Solana, pass `model:` or `mode:` explicitly.
|
|
172
|
+
|
|
173
|
+
> 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.
|
|
174
|
+
|
|
129
175
|
---
|
|
130
176
|
|
|
131
177
|
## Tools
|
|
@@ -261,10 +307,16 @@ Chain selection priority (see `src/utils/wallet.ts`):
|
|
|
261
307
|
|
|
262
308
|
**Switching chains:**
|
|
263
309
|
|
|
264
|
-
|
|
265
|
-
|
|
310
|
+
The recommended way is the tool itself — it provisions both wallets, applies instantly (no restart), and shows the funding QR:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
blockrun_wallet action:"chain" chain:"solana" # or chain:"base"
|
|
314
|
+
blockrun_wallet action:"setup" # funding instructions for the active chain
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
*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.
|
|
266
318
|
|
|
267
|
-
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. In Solana mode
|
|
319
|
+
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.
|
|
268
320
|
|
|
269
321
|
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.
|
|
270
322
|
|
package/dist/index.js
CHANGED
|
@@ -209,6 +209,18 @@ async function getChainBalance(chain, address) {
|
|
|
209
209
|
return chain === "solana" ? getSolanaUsdcBalance() : getBaseUsdcBalance(address);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// src/utils/model-cache.ts
|
|
213
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
214
|
+
async function loadModels(llm, cache) {
|
|
215
|
+
if (!cache.models) {
|
|
216
|
+
cache.models = llm.listAllModels ? await llm.listAllModels() : await llm.listModels();
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
cache.models = null;
|
|
219
|
+
}, CACHE_TTL_MS).unref();
|
|
220
|
+
}
|
|
221
|
+
return cache.models;
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
// src/tools/wallet.ts
|
|
213
225
|
import { z } from "zod";
|
|
214
226
|
|
|
@@ -376,6 +388,14 @@ Call this to set spending limits before spawning child agents.
|
|
|
376
388
|
|
|
377
389
|
The server holds TWO wallets \u2014 one on Base, one on Solana \u2014 but pays on ONE
|
|
378
390
|
active chain at a time. status shows both addresses/balances and which is active.
|
|
391
|
+
Default chain is Base.
|
|
392
|
+
|
|
393
|
+
To pay on Solana (no env vars, no file editing, no restart):
|
|
394
|
+
1. action:"chain" chain:"solana" \u2192 provisions + activates the Solana wallet
|
|
395
|
+
2. action:"setup" \u2192 Solana address + funding QR (send USDC SPL on Solana)
|
|
396
|
+
Switch back with action:"chain" chain:"base". Base-only \u2014 these ignore Solana and
|
|
397
|
+
need Base: blockrun_image, blockrun_music, blockrun_speech, blockrun_video, paid
|
|
398
|
+
blockrun_price, blockrun_chat routing:"smart", and native Anthropic (claude-*).
|
|
379
399
|
|
|
380
400
|
Actions:
|
|
381
401
|
- status (default): Both wallet addresses + USDC balances, active chain, session spending
|
|
@@ -1043,14 +1063,7 @@ function registerModelsTool(server, modelCache) {
|
|
|
1043
1063
|
}
|
|
1044
1064
|
},
|
|
1045
1065
|
async ({ category, provider }) => {
|
|
1046
|
-
|
|
1047
|
-
if (!modelCache.models) {
|
|
1048
|
-
modelCache.models = "listAllModels" in llm ? await llm.listAllModels() : await llm.listModels();
|
|
1049
|
-
setTimeout(() => {
|
|
1050
|
-
modelCache.models = null;
|
|
1051
|
-
}, 5 * 60 * 1e3);
|
|
1052
|
-
}
|
|
1053
|
-
let models = modelCache.models;
|
|
1066
|
+
let models = await loadModels(getClient(), modelCache);
|
|
1054
1067
|
if (provider) {
|
|
1055
1068
|
const p = provider.toLowerCase();
|
|
1056
1069
|
models = models.filter((m) => m.id.toLowerCase().startsWith(p + "/"));
|
|
@@ -1243,6 +1256,26 @@ Error: ${errMsg}` }],
|
|
|
1243
1256
|
|
|
1244
1257
|
// src/tools/music.ts
|
|
1245
1258
|
import { z as z5 } from "zod";
|
|
1259
|
+
|
|
1260
|
+
// src/utils/http.ts
|
|
1261
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
1262
|
+
const controller = new AbortController();
|
|
1263
|
+
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1264
|
+
id.unref?.();
|
|
1265
|
+
try {
|
|
1266
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
1267
|
+
} finally {
|
|
1268
|
+
clearTimeout(id);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function isTimeoutError(err) {
|
|
1272
|
+
const name = err instanceof Error ? err.name : "";
|
|
1273
|
+
if (name === "AbortError" || name === "TimeoutError") return true;
|
|
1274
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
1275
|
+
return msg.includes("abort") || msg.includes("timeout") || msg.includes("timed out") || msg.includes("did not complete within");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/tools/music.ts
|
|
1246
1279
|
import { privateKeyToAccount } from "viem/accounts";
|
|
1247
1280
|
import {
|
|
1248
1281
|
createPaymentPayload,
|
|
@@ -1372,7 +1405,7 @@ Error: ${errMsg}` }],
|
|
|
1372
1405
|
isError: true
|
|
1373
1406
|
};
|
|
1374
1407
|
}
|
|
1375
|
-
if (
|
|
1408
|
+
if (isTimeoutError(err)) {
|
|
1376
1409
|
return {
|
|
1377
1410
|
content: [{ type: "text", text: `Music generation timed out. This can happen during peak load \u2014 please try again.
|
|
1378
1411
|
Error: ${errMsg}` }],
|
|
@@ -1387,15 +1420,6 @@ Error: ${errMsg}` }],
|
|
|
1387
1420
|
}
|
|
1388
1421
|
);
|
|
1389
1422
|
}
|
|
1390
|
-
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
1391
|
-
const controller = new AbortController();
|
|
1392
|
-
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1393
|
-
try {
|
|
1394
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1395
|
-
} finally {
|
|
1396
|
-
clearTimeout(id);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
1423
|
|
|
1400
1424
|
// src/tools/speech.ts
|
|
1401
1425
|
import { z as z6 } from "zod";
|
|
@@ -1514,7 +1538,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1514
1538
|
}
|
|
1515
1539
|
const privateKey = getOrCreateWalletKey();
|
|
1516
1540
|
const account = privateKeyToAccount2(privateKey);
|
|
1517
|
-
const resp402 = await
|
|
1541
|
+
const resp402 = await fetchWithTimeout(endpoint, {
|
|
1518
1542
|
method: "POST",
|
|
1519
1543
|
headers: { "Content-Type": "application/json" },
|
|
1520
1544
|
body: JSON.stringify(body)
|
|
@@ -1540,7 +1564,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1540
1564
|
extra: details.extra
|
|
1541
1565
|
}
|
|
1542
1566
|
);
|
|
1543
|
-
const resp = await
|
|
1567
|
+
const resp = await fetchWithTimeout(endpoint, {
|
|
1544
1568
|
method: "POST",
|
|
1545
1569
|
headers: {
|
|
1546
1570
|
"Content-Type": "application/json",
|
|
@@ -1593,7 +1617,7 @@ Error: ${errMsg}` }],
|
|
|
1593
1617
|
isError: true
|
|
1594
1618
|
};
|
|
1595
1619
|
}
|
|
1596
|
-
if (
|
|
1620
|
+
if (isTimeoutError(err)) {
|
|
1597
1621
|
return {
|
|
1598
1622
|
content: [{ type: "text", text: `Speech generation timed out \u2014 please try again.
|
|
1599
1623
|
Error: ${errMsg}` }],
|
|
@@ -1610,7 +1634,7 @@ Error: ${errMsg}` }],
|
|
|
1610
1634
|
}
|
|
1611
1635
|
async function listVoices() {
|
|
1612
1636
|
try {
|
|
1613
|
-
const resp = await
|
|
1637
|
+
const resp = await fetchWithTimeout(`${BLOCKRUN_API2}/v1/audio/voices`, { method: "GET" }, VOICES_TIMEOUT);
|
|
1614
1638
|
if (resp.ok) {
|
|
1615
1639
|
const payload = await resp.json();
|
|
1616
1640
|
const voices = payload.data || [];
|
|
@@ -1637,15 +1661,6 @@ Any raw ElevenLabs voice_id also works.` }],
|
|
|
1637
1661
|
structuredContent: { voices: VOICE_ALIASES }
|
|
1638
1662
|
};
|
|
1639
1663
|
}
|
|
1640
|
-
async function fetchWithTimeout2(url, options, timeoutMs) {
|
|
1641
|
-
const controller = new AbortController();
|
|
1642
|
-
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1643
|
-
try {
|
|
1644
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1645
|
-
} finally {
|
|
1646
|
-
clearTimeout(id);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
1664
|
|
|
1650
1665
|
// src/tools/video.ts
|
|
1651
1666
|
import { z as z7 } from "zod";
|
|
@@ -1749,7 +1764,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1749
1764
|
if (image_url) body.image_url = image_url;
|
|
1750
1765
|
if (real_face_asset_id) body.real_face_asset_id = real_face_asset_id;
|
|
1751
1766
|
if (duration_seconds !== void 0) body.duration_seconds = duration_seconds;
|
|
1752
|
-
const resp402 = await
|
|
1767
|
+
const resp402 = await fetchWithTimeout(submitUrl, {
|
|
1753
1768
|
method: "POST",
|
|
1754
1769
|
headers: { "Content-Type": "application/json" },
|
|
1755
1770
|
body: JSON.stringify(body)
|
|
@@ -1777,7 +1792,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1777
1792
|
extra: details.extra
|
|
1778
1793
|
}
|
|
1779
1794
|
);
|
|
1780
|
-
const submitResp = await
|
|
1795
|
+
const submitResp = await fetchWithTimeout(submitUrl, {
|
|
1781
1796
|
method: "POST",
|
|
1782
1797
|
headers: {
|
|
1783
1798
|
"Content-Type": "application/json",
|
|
@@ -1802,7 +1817,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1802
1817
|
let completed = null;
|
|
1803
1818
|
while (Date.now() - startedAt < TOTAL_BUDGET_MS) {
|
|
1804
1819
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1805
|
-
const pollResp = await
|
|
1820
|
+
const pollResp = await fetchWithTimeout(pollAbsoluteUrl, {
|
|
1806
1821
|
method: "GET",
|
|
1807
1822
|
headers: { "PAYMENT-SIGNATURE": paymentPayload }
|
|
1808
1823
|
}, 9e4);
|
|
@@ -1866,7 +1881,7 @@ Error: ${errMsg}` }],
|
|
|
1866
1881
|
isError: true
|
|
1867
1882
|
};
|
|
1868
1883
|
}
|
|
1869
|
-
if (
|
|
1884
|
+
if (isTimeoutError(err)) {
|
|
1870
1885
|
return {
|
|
1871
1886
|
content: [{ type: "text", text: `Video generation timed out. The upstream async job didn't complete in time \u2014 please try again.
|
|
1872
1887
|
Error: ${errMsg}` }],
|
|
@@ -1881,15 +1896,6 @@ Error: ${errMsg}` }],
|
|
|
1881
1896
|
}
|
|
1882
1897
|
);
|
|
1883
1898
|
}
|
|
1884
|
-
async function fetchWithTimeout3(url, options, timeoutMs) {
|
|
1885
|
-
const controller = new AbortController();
|
|
1886
|
-
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1887
|
-
try {
|
|
1888
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1889
|
-
} finally {
|
|
1890
|
-
clearTimeout(id);
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
1899
|
|
|
1894
1900
|
// src/tools/realface.ts
|
|
1895
1901
|
import { z as z8 } from "zod";
|
|
@@ -1901,19 +1907,10 @@ import {
|
|
|
1901
1907
|
} from "@blockrun/llm";
|
|
1902
1908
|
var BLOCKRUN_API4 = "https://blockrun.ai/api";
|
|
1903
1909
|
var ENROLLMENT_PRICE_USD = 0.01;
|
|
1904
|
-
async function fetchWithTimeout4(url, options, timeoutMs) {
|
|
1905
|
-
const controller = new AbortController();
|
|
1906
|
-
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1907
|
-
try {
|
|
1908
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1909
|
-
} finally {
|
|
1910
|
-
clearTimeout(id);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
1910
|
async function payAndPostJson(url, reqBody, fallbackDescription) {
|
|
1914
1911
|
const privateKey = getOrCreateWalletKey();
|
|
1915
1912
|
const account = privateKeyToAccount4(privateKey);
|
|
1916
|
-
const resp402 = await
|
|
1913
|
+
const resp402 = await fetchWithTimeout(url, {
|
|
1917
1914
|
method: "POST",
|
|
1918
1915
|
headers: { "Content-Type": "application/json" },
|
|
1919
1916
|
body: reqBody
|
|
@@ -1939,7 +1936,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
|
|
|
1939
1936
|
extra: details.extra
|
|
1940
1937
|
}
|
|
1941
1938
|
);
|
|
1942
|
-
const resp = await
|
|
1939
|
+
const resp = await fetchWithTimeout(url, {
|
|
1943
1940
|
method: "POST",
|
|
1944
1941
|
headers: {
|
|
1945
1942
|
"Content-Type": "application/json",
|
|
@@ -1988,7 +1985,7 @@ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, na
|
|
|
1988
1985
|
}
|
|
1989
1986
|
const body = { name };
|
|
1990
1987
|
if (group_id) body.groupId = group_id;
|
|
1991
|
-
const resp = await
|
|
1988
|
+
const resp = await fetchWithTimeout(`${BLOCKRUN_API4}/v1/realface/init`, {
|
|
1992
1989
|
method: "POST",
|
|
1993
1990
|
headers: { "Content-Type": "application/json" },
|
|
1994
1991
|
body: JSON.stringify(body)
|
|
@@ -2037,7 +2034,7 @@ QR opened for scanning (${qrPath}).`;
|
|
|
2037
2034
|
if (!group_id) {
|
|
2038
2035
|
return { content: [{ type: "text", text: formatError('group_id is required for action:"status".') }], isError: true };
|
|
2039
2036
|
}
|
|
2040
|
-
const resp = await
|
|
2037
|
+
const resp = await fetchWithTimeout(`${BLOCKRUN_API4}/v1/realface/status?groupId=${encodeURIComponent(group_id)}`, {
|
|
2041
2038
|
method: "GET"
|
|
2042
2039
|
}, 3e4);
|
|
2043
2040
|
const data = await resp.json().catch(() => ({}));
|
|
@@ -2067,8 +2064,8 @@ QR opened for scanning (${qrPath}).`;
|
|
|
2067
2064
|
if (action === "list") {
|
|
2068
2065
|
const account = privateKeyToAccount4(getOrCreateWalletKey());
|
|
2069
2066
|
const [rfResp, vpResp] = await Promise.all([
|
|
2070
|
-
|
|
2071
|
-
|
|
2067
|
+
fetchWithTimeout(`${BLOCKRUN_API4}/v1/wallet/${account.address}/realfaces`, { method: "GET" }, 3e4),
|
|
2068
|
+
fetchWithTimeout(`${BLOCKRUN_API4}/v1/wallet/${account.address}/portraits`, { method: "GET" }, 3e4).catch(() => null)
|
|
2072
2069
|
]);
|
|
2073
2070
|
const data = await rfResp.json().catch(() => ({}));
|
|
2074
2071
|
if (!rfResp.ok) {
|
|
@@ -2165,7 +2162,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2165
2162
|
const account = privateKeyToAccount4(privateKey);
|
|
2166
2163
|
const enrollUrl = `${BLOCKRUN_API4}/v1/realface/enroll`;
|
|
2167
2164
|
const reqBody = JSON.stringify({ name, image_url, group_id });
|
|
2168
|
-
const resp402 = await
|
|
2165
|
+
const resp402 = await fetchWithTimeout(enrollUrl, {
|
|
2169
2166
|
method: "POST",
|
|
2170
2167
|
headers: { "Content-Type": "application/json" },
|
|
2171
2168
|
body: reqBody
|
|
@@ -2191,7 +2188,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2191
2188
|
extra: details.extra
|
|
2192
2189
|
}
|
|
2193
2190
|
);
|
|
2194
|
-
const resp = await
|
|
2191
|
+
const resp = await fetchWithTimeout(enrollUrl, {
|
|
2195
2192
|
method: "POST",
|
|
2196
2193
|
headers: {
|
|
2197
2194
|
"Content-Type": "application/json",
|
|
@@ -3033,64 +3030,127 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
|
|
|
3033
3030
|
);
|
|
3034
3031
|
}
|
|
3035
3032
|
|
|
3033
|
+
// src/profiles.ts
|
|
3034
|
+
var ALL_TOOLS = [
|
|
3035
|
+
"wallet",
|
|
3036
|
+
"chat",
|
|
3037
|
+
"models",
|
|
3038
|
+
"image",
|
|
3039
|
+
"music",
|
|
3040
|
+
"speech",
|
|
3041
|
+
"video",
|
|
3042
|
+
"realface",
|
|
3043
|
+
"search",
|
|
3044
|
+
"exa",
|
|
3045
|
+
"markets",
|
|
3046
|
+
"price",
|
|
3047
|
+
"dex",
|
|
3048
|
+
"modal",
|
|
3049
|
+
"phone",
|
|
3050
|
+
"surf",
|
|
3051
|
+
"rpc",
|
|
3052
|
+
"defi"
|
|
3053
|
+
];
|
|
3054
|
+
var PROFILES = {
|
|
3055
|
+
full: "all",
|
|
3056
|
+
// Generative media: image/video/realface plus the other media-generation
|
|
3057
|
+
// tools (music, speech), with wallet (funding/balance — media calls cost
|
|
3058
|
+
// USDC) and models (discover what's available).
|
|
3059
|
+
media: ["wallet", "models", "image", "video", "realface", "music", "speech"],
|
|
3060
|
+
// Markets & on-chain data: prediction markets, realtime prices, DEX/CEX
|
|
3061
|
+
// data, DeFi metrics, and raw RPC, plus the wallet for balance/funding.
|
|
3062
|
+
trading: ["wallet", "price", "dex", "markets", "surf", "defi", "rpc"],
|
|
3063
|
+
// Web research & analysis: live search, neural search, Surf's news/SQL,
|
|
3064
|
+
// and chat for synthesis, plus wallet and the model catalogue.
|
|
3065
|
+
research: ["wallet", "models", "chat", "search", "exa", "surf"],
|
|
3066
|
+
// Minimal LLM gateway: just chat + model discovery + wallet.
|
|
3067
|
+
chat: ["wallet", "models", "chat"]
|
|
3068
|
+
};
|
|
3069
|
+
var DEFAULT_PROFILE = "full";
|
|
3070
|
+
function resolveProfileName(argv = process.argv.slice(2), env = process.env) {
|
|
3071
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3072
|
+
const arg = argv[i];
|
|
3073
|
+
if (arg === "--profile") return (argv[i + 1] ?? DEFAULT_PROFILE).toLowerCase();
|
|
3074
|
+
if (arg.startsWith("--profile=")) return arg.slice("--profile=".length).toLowerCase();
|
|
3075
|
+
}
|
|
3076
|
+
if (env.BLOCKRUN_MCP_PROFILE) return env.BLOCKRUN_MCP_PROFILE.toLowerCase();
|
|
3077
|
+
return DEFAULT_PROFILE;
|
|
3078
|
+
}
|
|
3079
|
+
function resolveTools(argv, env) {
|
|
3080
|
+
const requested = resolveProfileName(argv, env);
|
|
3081
|
+
const spec = PROFILES[requested];
|
|
3082
|
+
if (!spec) {
|
|
3083
|
+
return { profile: DEFAULT_PROFILE, tools: new Set(ALL_TOOLS) };
|
|
3084
|
+
}
|
|
3085
|
+
return {
|
|
3086
|
+
profile: requested,
|
|
3087
|
+
tools: new Set(spec === "all" ? ALL_TOOLS : spec)
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3036
3091
|
// src/mcp-handler.ts
|
|
3037
|
-
function initializeMcpServer(server) {
|
|
3092
|
+
function initializeMcpServer(server, profileArgs) {
|
|
3038
3093
|
const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
|
|
3039
3094
|
const modelCache = { models: null };
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
const llm = getClient();
|
|
3079
|
-
if (!modelCache.models) {
|
|
3080
|
-
modelCache.models = "listAllModels" in llm ? await llm.listAllModels() : await llm.listModels();
|
|
3081
|
-
setTimeout(() => {
|
|
3082
|
-
modelCache.models = null;
|
|
3083
|
-
}, 5 * 60 * 1e3);
|
|
3095
|
+
const { profile, tools } = resolveTools(profileArgs?.argv, profileArgs?.env);
|
|
3096
|
+
const registrars = {
|
|
3097
|
+
wallet: () => registerWalletTool(server, budget),
|
|
3098
|
+
chat: () => registerChatTool(server, budget),
|
|
3099
|
+
models: () => registerModelsTool(server, modelCache),
|
|
3100
|
+
image: () => registerImageTool(server, budget),
|
|
3101
|
+
music: () => registerMusicTool(server, budget),
|
|
3102
|
+
speech: () => registerSpeechTool(server, budget),
|
|
3103
|
+
video: () => registerVideoTool(server, budget),
|
|
3104
|
+
realface: () => registerRealfaceTool(server, budget),
|
|
3105
|
+
search: () => registerSearchTool(server, budget),
|
|
3106
|
+
exa: () => registerExaTool(server, budget),
|
|
3107
|
+
markets: () => registerMarketsTool(server, budget),
|
|
3108
|
+
price: () => registerPriceTool(server, budget),
|
|
3109
|
+
dex: () => registerDexTool(server),
|
|
3110
|
+
modal: () => registerModalTool(server, budget),
|
|
3111
|
+
phone: () => registerPhoneTool(server, budget),
|
|
3112
|
+
surf: () => registerSurfTool(server, budget),
|
|
3113
|
+
rpc: () => registerRpcTool(server, budget),
|
|
3114
|
+
defi: () => registerDefiTool(server, budget)
|
|
3115
|
+
};
|
|
3116
|
+
for (const [name, register] of Object.entries(registrars)) {
|
|
3117
|
+
if (tools.has(name)) register();
|
|
3118
|
+
}
|
|
3119
|
+
if (tools.has("wallet")) {
|
|
3120
|
+
server.registerResource(
|
|
3121
|
+
"wallet",
|
|
3122
|
+
"blockrun://wallet",
|
|
3123
|
+
{ description: "Wallet address and status", mimeType: "application/json" },
|
|
3124
|
+
async () => {
|
|
3125
|
+
const info = await getWalletInfo();
|
|
3126
|
+
return {
|
|
3127
|
+
contents: [{
|
|
3128
|
+
uri: "blockrun://wallet",
|
|
3129
|
+
mimeType: "application/json",
|
|
3130
|
+
text: JSON.stringify(info, null, 2)
|
|
3131
|
+
}]
|
|
3132
|
+
};
|
|
3084
3133
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
if (tools.has("models")) {
|
|
3137
|
+
server.registerResource(
|
|
3138
|
+
"models",
|
|
3139
|
+
"blockrun://models",
|
|
3140
|
+
{ description: "Available AI models with pricing", mimeType: "application/json" },
|
|
3141
|
+
async () => {
|
|
3142
|
+
const models = await loadModels(getClient(), modelCache);
|
|
3143
|
+
return {
|
|
3144
|
+
contents: [{
|
|
3145
|
+
uri: "blockrun://models",
|
|
3146
|
+
mimeType: "application/json",
|
|
3147
|
+
text: JSON.stringify(models, null, 2)
|
|
3148
|
+
}]
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
return { profile, tools: [...tools] };
|
|
3094
3154
|
}
|
|
3095
3155
|
|
|
3096
3156
|
// src/utils/key-leak-scanner.ts
|
|
@@ -3190,6 +3250,7 @@ function printHelp() {
|
|
|
3190
3250
|
"Options:",
|
|
3191
3251
|
" -h, --help Show this help message",
|
|
3192
3252
|
" -v, --version Print the package version",
|
|
3253
|
+
` --profile <name> Tool profile to expose: ${Object.keys(PROFILES).join(" | ")} (default: full)`,
|
|
3193
3254
|
"",
|
|
3194
3255
|
"When no metadata flag is provided, the server starts on stdio for MCP clients.",
|
|
3195
3256
|
""
|
|
@@ -3228,10 +3289,11 @@ async function main() {
|
|
|
3228
3289
|
name: "blockrun-mcp",
|
|
3229
3290
|
version: VERSION
|
|
3230
3291
|
});
|
|
3231
|
-
initializeMcpServer(server);
|
|
3292
|
+
const { profile, tools } = initializeMcpServer(server);
|
|
3232
3293
|
const transport = new StdioServerTransport();
|
|
3233
3294
|
await server.connect(transport);
|
|
3234
|
-
|
|
3295
|
+
const profileNote = profile === "full" ? `${tools.length} tools` : `profile "${profile}" \u2014 ${tools.length} tools: ${tools.join(", ")}`;
|
|
3296
|
+
console.error(`BlockRun MCP Server started (v${VERSION}) \u2014 stdio transport \u2014 ${profileNote}`);
|
|
3235
3297
|
checkForUpdate();
|
|
3236
3298
|
}
|
|
3237
3299
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dev": "tsx watch src/index.ts",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "tsx --test test/*.test.ts",
|
|
21
22
|
"prepublishOnly": "npm run build"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|