@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.
Files changed (3) hide show
  1. package/README.md +56 -4
  2. package/dist/index.js +179 -117
  3. 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
- - Base Solana: `echo solana > ~/.blockrun/.chain`, then set `SOLANA_WALLET_KEY` or create `~/.blockrun/.solana-session`
265
- - Solana → Base: `echo base > ~/.blockrun/.chain` (the existing `.session` is reused, so it's the same Base wallet)
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 they fail before creating or charging a Base wallet.
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
- const llm = getClient();
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 (errMsg.includes("abort") || errMsg.includes("timeout") || errMsg.includes("Timeout")) {
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 fetchWithTimeout2(endpoint, {
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 fetchWithTimeout2(endpoint, {
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 (errMsg.includes("abort") || errMsg.includes("timeout") || errMsg.includes("Timeout")) {
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 fetchWithTimeout2(`${BLOCKRUN_API2}/v1/audio/voices`, { method: "GET" }, VOICES_TIMEOUT);
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 fetchWithTimeout3(submitUrl, {
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 fetchWithTimeout3(submitUrl, {
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 fetchWithTimeout3(pollAbsoluteUrl, {
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 (errMsg.includes("abort") || errMsg.includes("timeout") || errMsg.includes("Timeout") || errMsg.includes("timed out") || errMsg.includes("did not complete within")) {
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 fetchWithTimeout4(url, {
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 fetchWithTimeout4(url, {
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 fetchWithTimeout4(`${BLOCKRUN_API4}/v1/realface/init`, {
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 fetchWithTimeout4(`${BLOCKRUN_API4}/v1/realface/status?groupId=${encodeURIComponent(group_id)}`, {
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
- fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/realfaces`, { method: "GET" }, 3e4),
2071
- fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/portraits`, { method: "GET" }, 3e4).catch(() => null)
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 fetchWithTimeout4(enrollUrl, {
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 fetchWithTimeout4(enrollUrl, {
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
- registerWalletTool(server, budget);
3041
- registerChatTool(server, budget);
3042
- registerModelsTool(server, modelCache);
3043
- registerImageTool(server, budget);
3044
- registerMusicTool(server, budget);
3045
- registerSpeechTool(server, budget);
3046
- registerVideoTool(server, budget);
3047
- registerRealfaceTool(server, budget);
3048
- registerSearchTool(server, budget);
3049
- registerExaTool(server, budget);
3050
- registerMarketsTool(server, budget);
3051
- registerPriceTool(server, budget);
3052
- registerDexTool(server);
3053
- registerModalTool(server, budget);
3054
- registerPhoneTool(server, budget);
3055
- registerSurfTool(server, budget);
3056
- registerRpcTool(server, budget);
3057
- registerDefiTool(server, budget);
3058
- server.registerResource(
3059
- "wallet",
3060
- "blockrun://wallet",
3061
- { description: "Wallet address and status", mimeType: "application/json" },
3062
- async () => {
3063
- const info = await getWalletInfo();
3064
- return {
3065
- contents: [{
3066
- uri: "blockrun://wallet",
3067
- mimeType: "application/json",
3068
- text: JSON.stringify(info, null, 2)
3069
- }]
3070
- };
3071
- }
3072
- );
3073
- server.registerResource(
3074
- "models",
3075
- "blockrun://models",
3076
- { description: "Available AI models with pricing", mimeType: "application/json" },
3077
- async () => {
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
- return {
3086
- contents: [{
3087
- uri: "blockrun://models",
3088
- mimeType: "application/json",
3089
- text: JSON.stringify(modelCache.models, null, 2)
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
- console.error(`BlockRun MCP Server started (v${VERSION}) \u2014 stdio transport`);
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.21.4",
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": [