@blockrun/mcp 0.19.2 → 0.21.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 +8 -4
  2. package/dist/index.js +293 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -132,10 +132,10 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
132
132
 
133
133
  | Tool | Data source | Cost |
134
134
  |------|-------------|------|
135
- | `blockrun_chat` | 55+ LLMs (GPT, Claude, Gemini, DeepSeek, Kimi K2.6, GLM, NVIDIA free tier, ...) with `mode` tier routing | per token |
136
- | `blockrun_image` | DALL-E 3, GPT Image 1/2, Grok Imagine, Flux, CogView-4, Nano Banana — generation + editing | $0.015–0.12 |
137
- | `blockrun_video` | xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast (720p + audio defaults); RealFace asset → real-person video | $0.05–0.298/sec |
138
- | `blockrun_realface` | Enroll a real person (phone liveness `ta_xxxx` asset) for Seedance 2.0 real-person video | free; $0.01 to enroll |
135
+ | `blockrun_chat` | 66+ LLMs (GPT, Claude, Gemini, DeepSeek, Kimi K2.6, GLM, NVIDIA free tier, ...) with `mode` tier routing | per token |
136
+ | `blockrun_image` | GPT Image 1/2, Nano Banana / Pro (up to 4K), Grok Imagine, CogView-4 — generation + img2img editing | $0.015–0.15 |
137
+ | `blockrun_video` | Sora 2 + xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast (720p + audio defaults); RealFace asset → real-person video | $0.05–0.30/sec |
138
+ | `blockrun_realface` | Enroll a real person (phone liveness) or an AI character (Virtual Portrait, no liveness) as a `ta_xxxx` asset for Seedance 2.0 video | free; $0.01 to enroll |
139
139
  | `blockrun_music` | MiniMax music generation | per track |
140
140
  | `blockrun_speech` | ElevenLabs text-to-speech (Flash/Turbo/Multilingual/v3, 8 voice aliases) + cinematic sound effects; free voice listing | $0.05–0.10/1k chars; $0.0525/effect |
141
141
  | `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
@@ -144,6 +144,10 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
144
144
  | `blockrun_exa` | Neural web search (Exa) — research, competitors, papers, URL content | $0.01/query |
145
145
  | `blockrun_search` | Grok Live Search — web + news with citations | $0.025 × max_results (default 10) |
146
146
  | `blockrun_dex` | Live DEX prices via DexScreener | free |
147
+ | `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 |
148
+ | `blockrun_defi` | DefiLlama — protocol TVL, chain TVL, yield pools (APY), token prices | $0.001–0.005/call |
149
+ | `blockrun_modal` | Isolated code execution in a BlockRun-hosted Modal sandbox — disposable container, optional GPU (T4 → H100) | $0.01 create; $0.001/op |
150
+ | `blockrun_phone` | Outbound AI voice calls (Bland) + wallet-owned US/CA numbers (Twilio), carrier + fraud lookups | $0.54/call; $5/number |
147
151
  | `blockrun_models` | Live catalogue of every LLM/image/video/music model + pricing | free |
148
152
  | `blockrun_wallet` | Balance, spending, agent budgets, setup QR | free |
149
153
 
package/dist/index.js CHANGED
@@ -1092,13 +1092,39 @@ var GENERATE_MODEL_COST = {
1092
1092
  "zai/cogview-4": 0.015,
1093
1093
  "xai/grok-imagine-image": 0.02,
1094
1094
  "xai/grok-imagine-image-pro": 0.07,
1095
+ "openai/gpt-image-1": 0.02,
1096
+ "openai/gpt-image-2": 0.06,
1097
+ "google/nano-banana": 0.05,
1098
+ "google/nano-banana-pro": 0.1
1099
+ };
1100
+ var LARGE_SIZE_COST = {
1095
1101
  "openai/gpt-image-1": 0.04,
1096
1102
  "openai/gpt-image-2": 0.12,
1097
- "openai/dall-e-3": 0.08,
1098
- "google/nano-banana": 0.05,
1099
- "together/flux-schnell": 3e-3
1103
+ "google/nano-banana-pro": 0.15
1104
+ // 4096x4096 tier
1100
1105
  };
1101
- var EDIT_MODELS = /* @__PURE__ */ new Set(["openai/gpt-image-1", "openai/gpt-image-2"]);
1106
+ var EDIT_MODELS = /* @__PURE__ */ new Set([
1107
+ "openai/gpt-image-1",
1108
+ "openai/gpt-image-2",
1109
+ "google/nano-banana",
1110
+ "google/nano-banana-pro"
1111
+ ]);
1112
+ var IMAGE_MODELS = [
1113
+ "zai/cogview-4",
1114
+ "google/nano-banana",
1115
+ "google/nano-banana-pro",
1116
+ "openai/gpt-image-1",
1117
+ "openai/gpt-image-2",
1118
+ "xai/grok-imagine-image",
1119
+ "xai/grok-imagine-image-pro"
1120
+ ];
1121
+ function estimateCost(model, size) {
1122
+ const base = GENERATE_MODEL_COST[model] ?? 0.06;
1123
+ if (size !== "1024x1024" && LARGE_SIZE_COST[model]) {
1124
+ return LARGE_SIZE_COST[model];
1125
+ }
1126
+ return base;
1127
+ }
1102
1128
  function registerImageTool(server, budget) {
1103
1129
  server.registerTool(
1104
1130
  "blockrun_image",
@@ -1109,21 +1135,22 @@ Actions:
1109
1135
  - generate (default): Create image from text prompt
1110
1136
  - edit: Transform an existing image using img2img
1111
1137
 
1112
- Generation models:
1113
- - zai/cogview-4 ($0.015) \u2014 Zhipu CogView-4, photorealistic, great for detailed scenes
1114
- - xai/grok-imagine-image ($0.02) \u2014 xAI Grok Imagine, stylized, fast
1115
- - xai/grok-imagine-image-pro ($0.07) \u2014 xAI Grok Imagine Pro, higher quality
1116
- - openai/gpt-image-1 ($0.02-0.04) \u2014 GPT native image generation
1117
- - openai/gpt-image-2 ($0.06-0.12) \u2014 ChatGPT Images 2.0, reasoning-driven, multilingual text rendering + character consistency
1118
- - openai/dall-e-3 ($0.04-0.08) \u2014 High quality, prompt adherence
1119
- - google/nano-banana ($0.05) \u2014 Google image model
1120
- Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
1138
+ Generation models (1024x1024 base price; larger sizes cost more on gpt-image-*):
1139
+ - openai/gpt-image-2 ($0.06\u20130.12) \u2014 flagship, reasoning-driven, multilingual on-image text + character consistency (default)
1140
+ - openai/gpt-image-1 ($0.02\u20130.04) \u2014 GPT native image generation
1141
+ - google/nano-banana ($0.05) \u2014 Gemini-family image model
1142
+ - google/nano-banana-pro ($0.10; $0.15 at 4096px) \u2014 up to 4K, strongest photorealism
1143
+ - xai/grok-imagine-image ($0.02) \u2014 stylized, fast
1144
+ - xai/grok-imagine-image-pro ($0.07) \u2014 higher quality Grok Imagine
1145
+ - zai/cogview-4 ($0.015) \u2014 cheapest, photorealistic detailed scenes
1146
+
1147
+ Edit (img2img) models: openai/gpt-image-2 (default), openai/gpt-image-1, google/nano-banana, google/nano-banana-pro`,
1121
1148
  inputSchema: {
1122
1149
  prompt: z4.string().describe("Image description or edit instructions"),
1123
1150
  action: z4.enum(["generate", "edit"]).optional().default("generate").describe("generate: create from text; edit: transform existing image"),
1124
- model: z4.enum(["zai/cogview-4", "openai/dall-e-3", "together/flux-schnell", "google/nano-banana", "openai/gpt-image-1", "openai/gpt-image-2", "xai/grok-imagine-image", "xai/grok-imagine-image-pro"]).optional().describe("Model to use (default: dall-e-3 for generate, gpt-image-2 for edit). xai/grok-imagine-image is stylized and fast; xai/grok-imagine-image-pro is higher quality; gpt-image-2 is the newest edit-capable model with stronger instruction following."),
1151
+ model: z4.enum(IMAGE_MODELS).optional().describe("Model to use (default: openai/gpt-image-2 for both generate and edit). gpt-image-2 renders on-image text best; nano-banana-pro for 4K photorealism; cogview-4 / grok-imagine-image for cheap drafts."),
1125
1152
  image: z4.string().optional().describe("Source image for edit action: base64-encoded image or URL"),
1126
- size: z4.enum(["1024x1024", "1792x1024", "1024x1792"]).optional().default("1024x1024"),
1153
+ size: z4.string().optional().default("1024x1024").describe("Image size. Common values: 1024x1024 (all models), 1536x1024 / 1024x1536 (gpt-image-*), 2048x2048 / 4096x4096 (nano-banana-pro)"),
1127
1154
  quality: z4.enum(["standard", "hd"]).optional().default("standard"),
1128
1155
  agent_id: z4.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1129
1156
  }
@@ -1136,6 +1163,7 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
1136
1163
  isError: true
1137
1164
  };
1138
1165
  }
1166
+ const selectedModel = model || "openai/gpt-image-2";
1139
1167
  let response;
1140
1168
  if (action === "edit") {
1141
1169
  if (!image) {
@@ -1144,14 +1172,13 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
1144
1172
  isError: true
1145
1173
  };
1146
1174
  }
1147
- const selectedModel = model || "openai/gpt-image-2";
1148
1175
  if (!EDIT_MODELS.has(selectedModel)) {
1149
1176
  return {
1150
- content: [{ type: "text", text: formatError("Image edits support only openai/gpt-image-1 or openai/gpt-image-2") }],
1177
+ content: [{ type: "text", text: formatError("Image edits support openai/gpt-image-1, openai/gpt-image-2, google/nano-banana, or google/nano-banana-pro") }],
1151
1178
  isError: true
1152
1179
  };
1153
1180
  }
1154
- const estimatedCost = selectedModel === "openai/gpt-image-2" ? 0.12 : 0.04;
1181
+ const estimatedCost = estimateCost(selectedModel, size);
1155
1182
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1156
1183
  if (!budgetCheck.allowed) {
1157
1184
  return {
@@ -1165,8 +1192,7 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
1165
1192
  });
1166
1193
  recordSpending(budget, estimatedCost, agent_id);
1167
1194
  } else {
1168
- const selectedModel = model || "openai/dall-e-3";
1169
- const estimatedCost = GENERATE_MODEL_COST[selectedModel] ?? 0.05;
1195
+ const estimatedCost = estimateCost(selectedModel, size);
1170
1196
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1171
1197
  if (!budgetCheck.allowed) {
1172
1198
  return {
@@ -1191,8 +1217,8 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
1191
1217
  return {
1192
1218
  content: [{ type: "text", text: `Image: ${imageUrl}
1193
1219
  Prompt: ${prompt}
1194
- Model: ${action === "edit" ? model || "openai/gpt-image-2" : model || "openai/dall-e-3"}` }],
1195
- structuredContent: { url: imageUrl, prompt, model: action === "edit" ? model || "openai/gpt-image-2" : model || "openai/dall-e-3" }
1220
+ Model: ${selectedModel}` }],
1221
+ structuredContent: { url: imageUrl, prompt, model: selectedModel }
1196
1222
  };
1197
1223
  } catch (err) {
1198
1224
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1881,6 +1907,46 @@ async function fetchWithTimeout4(url, options, timeoutMs) {
1881
1907
  clearTimeout(id);
1882
1908
  }
1883
1909
  }
1910
+ async function payAndPostJson(url, reqBody, fallbackDescription) {
1911
+ const privateKey = getOrCreateWalletKey();
1912
+ const account = privateKeyToAccount4(privateKey);
1913
+ const resp402 = await fetchWithTimeout4(url, {
1914
+ method: "POST",
1915
+ headers: { "Content-Type": "application/json" },
1916
+ body: reqBody
1917
+ }, 15e3);
1918
+ if (resp402.status !== 402) {
1919
+ const data2 = await resp402.json().catch(() => ({}));
1920
+ throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
1921
+ }
1922
+ const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1923
+ if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1924
+ const paymentRequired = parsePaymentRequired4(prHeader);
1925
+ const details = extractPaymentDetails4(paymentRequired);
1926
+ const paymentPayload = await createPaymentPayload4(
1927
+ privateKey,
1928
+ account.address,
1929
+ details.recipient,
1930
+ details.amount,
1931
+ details.network || "eip155:8453",
1932
+ {
1933
+ resourceUrl: details.resource?.url || url,
1934
+ resourceDescription: details.resource?.description || fallbackDescription,
1935
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
1936
+ extra: details.extra
1937
+ }
1938
+ );
1939
+ const resp = await fetchWithTimeout4(url, {
1940
+ method: "POST",
1941
+ headers: {
1942
+ "Content-Type": "application/json",
1943
+ "PAYMENT-SIGNATURE": paymentPayload
1944
+ },
1945
+ body: reqBody
1946
+ }, 9e4);
1947
+ const data = await resp.json().catch(() => ({}));
1948
+ return { status: resp.status, data };
1949
+ }
1884
1950
  function registerRealfaceTool(server, budget) {
1885
1951
  server.registerTool(
1886
1952
  "blockrun_realface",
@@ -1893,7 +1959,8 @@ Actions:
1893
1959
  - init: FREE. Create an asset group + a phone H5 link. The tool renders the link as a QR code and opens it; the real person scans it on their phone and completes the ~1 min liveness check. Pass group_id to refresh an expired link.
1894
1960
  - status: FREE. Poll a group until status:"active" (ready_to_finalize:true). The H5 link is valid ~120s \u2014 re-init if it expires.
1895
1961
  - enroll: PAID ($0.01 USDC, Base only). After the group is active, upload a clear front-facing photo (image_url) of the SAME person. Returns the ta_xxxx asset id.
1896
- - list: FREE. List the RealFace assets enrolled by this wallet (their ta_xxxx ids + names) so you can pick one for blockrun_video.
1962
+ - portrait: PAID ($0.01 USDC, Base only). Virtual Portrait \u2014 enroll an AI-GENERATED character from an image URL directly, NO liveness needed (one step: name + image_url \u2192 ta_xxxx). For fictional/AI characters only; for a real person use the init\u2192status\u2192enroll liveness flow.
1963
+ - list: FREE. List the RealFace + Virtual Portrait assets enrolled by this wallet (their ta_xxxx ids + names) so you can pick one for blockrun_video.
1897
1964
 
1898
1965
  Typical flow:
1899
1966
  1. blockrun_realface action:"init" name:"Alice" \u2192 scan QR on phone, do liveness
@@ -1903,10 +1970,10 @@ Typical flow:
1903
1970
 
1904
1971
  Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, name, and the photo URL you supply.`,
1905
1972
  inputSchema: {
1906
- action: z8.enum(["init", "status", "enroll", "list"]).describe("What to do"),
1907
- name: z8.string().min(1).max(64).optional().describe("Display name for the person (required for init and enroll)."),
1908
- group_id: z8.string().regex(/^legacy_rf_\d+$/).optional().describe("Asset-group id from init (required for status and enroll; pass to init to refresh an expired H5 link)."),
1909
- image_url: z8.string().url().optional().describe("Public HTTPS URL to a clear front-facing face photo (JPG/PNG/WEBP, \u226410MB). Required for enroll."),
1973
+ action: z8.enum(["init", "status", "enroll", "portrait", "list"]).describe("What to do"),
1974
+ name: z8.string().min(1).max(64).optional().describe("Display name for the person/character (required for init, enroll, and portrait)."),
1975
+ group_id: z8.string().regex(/^legacy_rf_\d+$/).optional().describe("Asset-group id from init (required for status and enroll; pass to init to refresh an expired H5 link). Not used by portrait."),
1976
+ image_url: z8.string().url().optional().describe("Public HTTPS URL to a clear front-facing face image (JPG/PNG/WEBP, \u226410MB). Required for enroll and portrait."),
1910
1977
  agent_id: z8.string().optional().describe("Agent identifier for budget tracking and enforcement (enroll only).")
1911
1978
  }
1912
1979
  },
@@ -1996,30 +2063,88 @@ QR opened for scanning (${qrPath}).`;
1996
2063
  }
1997
2064
  if (action === "list") {
1998
2065
  const account = privateKeyToAccount4(getOrCreateWalletKey());
1999
- const resp = await fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/realfaces`, {
2000
- method: "GET"
2001
- }, 3e4);
2002
- const data = await resp.json().catch(() => ({}));
2003
- if (!resp.ok) {
2004
- return { content: [{ type: "text", text: formatError(`list failed (${resp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
2066
+ const [rfResp, vpResp] = await Promise.all([
2067
+ fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/realfaces`, { method: "GET" }, 3e4),
2068
+ fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/portraits`, { method: "GET" }, 3e4).catch(() => null)
2069
+ ]);
2070
+ const data = await rfResp.json().catch(() => ({}));
2071
+ if (!rfResp.ok) {
2072
+ return { content: [{ type: "text", text: formatError(`list failed (${rfResp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
2005
2073
  }
2006
2074
  const faces = Array.isArray(data.realfaces) ? data.realfaces : [];
2007
- if (faces.length === 0) {
2075
+ let portraits = [];
2076
+ if (vpResp?.ok) {
2077
+ const vpData = await vpResp.json().catch(() => ({}));
2078
+ portraits = Array.isArray(vpData.portraits) ? vpData.portraits : [];
2079
+ }
2080
+ if (faces.length === 0 && portraits.length === 0) {
2008
2081
  return {
2009
- content: [{ type: "text", text: `No RealFace assets enrolled for ${account.address}.
2010
- Enroll one: blockrun_realface action:"init" name:"\u2026".` }],
2011
- structuredContent: { wallet: account.address, realfaces: [], count: 0 }
2082
+ content: [{ type: "text", text: `No RealFace or Virtual Portrait assets enrolled for ${account.address}.
2083
+ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or action:"portrait" name:"\u2026" image_url:"https://\u2026" (AI character).` }],
2084
+ structuredContent: { wallet: account.address, realfaces: [], portraits: [], count: 0 }
2012
2085
  };
2013
2086
  }
2087
+ const first = faces[0] ?? portraits[0];
2014
2088
  const lines = [
2015
- `RealFace assets for ${account.address} (${faces.length}):`,
2016
- ...faces.map((f) => ` \u2022 ${f.assetId} \u2014 "${f.name}"${f.createdAt ? ` (${f.createdAt})` : ""}`),
2089
+ `Assets for ${account.address} (${faces.length} RealFace, ${portraits.length} Virtual Portrait):`,
2090
+ ...faces.map((f) => ` \u2022 ${f.assetId} \u2014 "${f.name}" [realface]${f.createdAt ? ` (${f.createdAt})` : ""}`),
2091
+ ...portraits.map((p) => ` \u2022 ${p.assetId} \u2014 "${p.name}" [portrait]${p.createdAt ? ` (${p.createdAt})` : ""}`),
2017
2092
  ``,
2018
- `Use one: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${faces[0].assetId}" prompt:"\u2026".`
2093
+ `Use one: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${first.assetId}" prompt:"\u2026".`
2019
2094
  ];
2020
2095
  return {
2021
2096
  content: [{ type: "text", text: lines.join("\n") }],
2022
- structuredContent: { wallet: account.address, realfaces: faces, count: faces.length }
2097
+ structuredContent: { wallet: account.address, realfaces: faces, portraits, count: faces.length + portraits.length }
2098
+ };
2099
+ }
2100
+ if (action === "portrait") {
2101
+ if (getChain() !== "base") {
2102
+ return { content: [{ type: "text", text: formatError("blockrun_realface portrait settles on Base only. Switch BlockRun to Base (run blockrun_wallet with action:chain chain:base) and fund the Base wallet with USDC.") }], isError: true };
2103
+ }
2104
+ if (!name || !image_url) {
2105
+ return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
2106
+ }
2107
+ const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2108
+ if (!budgetCheck.allowed) {
2109
+ return { content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2110
+ }
2111
+ const { status, data } = await payAndPostJson(
2112
+ `${BLOCKRUN_API4}/v1/portrait/enroll`,
2113
+ JSON.stringify({ name, image_url }),
2114
+ "BlockRun Virtual Portrait enrollment"
2115
+ );
2116
+ if (status === 402) {
2117
+ throw new Error("Payment rejected. Check your wallet balance.");
2118
+ }
2119
+ if (status === 422) {
2120
+ return { content: [{ type: "text", text: formatError(`Portrait rejected \u2014 ${data.hint || data.message || "use a clear front-facing character image"}. No payment taken.`) }], isError: true };
2121
+ }
2122
+ if (status < 200 || status >= 300) {
2123
+ throw new Error(`Portrait enroll error ${status}: ${data.error || JSON.stringify(data)}`);
2124
+ }
2125
+ const assetId = data.asset_id;
2126
+ if (!assetId) throw new Error(`Portrait response missing asset_id: ${JSON.stringify(data)}`);
2127
+ recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
2128
+ const txHash = data.settlement?.tx_hash || void 0;
2129
+ const lines = [
2130
+ `\u2705 Virtual Portrait enrolled!`,
2131
+ `Asset ID: ${assetId}`,
2132
+ `Name: ${data.name || name}`,
2133
+ `Cost: $${ENROLLMENT_PRICE_USD.toFixed(2)} USDC`,
2134
+ ...txHash ? [`Tx: ${txHash}`] : [],
2135
+ ``,
2136
+ `Use it: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${assetId}" prompt:"\u2026".`
2137
+ ];
2138
+ return {
2139
+ content: [{ type: "text", text: lines.join("\n") }],
2140
+ structuredContent: {
2141
+ asset_id: assetId,
2142
+ group_id: data.group_id,
2143
+ name: data.name || name,
2144
+ image_url: data.image_url,
2145
+ price_usd: ENROLLMENT_PRICE_USD,
2146
+ ...txHash ? { txHash } : {}
2147
+ }
2023
2148
  };
2024
2149
  }
2025
2150
  if (action === "enroll") {
@@ -2780,6 +2905,131 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
2780
2905
  );
2781
2906
  }
2782
2907
 
2908
+ // src/tools/rpc.ts
2909
+ import { z as z17 } from "zod";
2910
+ var RPC_PRICE_USD = 2e-3;
2911
+ function registerRpcTool(server, budget) {
2912
+ server.registerTool(
2913
+ "blockrun_rpc",
2914
+ {
2915
+ description: `Raw JSON-RPC against 40+ blockchains \u2014 one endpoint, no node, no API key. $0.002 per call (batch charges per element).
2916
+
2917
+ Use when you need data the higher-level tools don't cover: contract reads (eth_call), balances, blocks, txs, logs, gas, or any chain-native RPC method.
2918
+
2919
+ Networks (full catalog in the rpc skill): ethereum, base, arbitrum, optimism, polygon, bsc, avalanche, solana, bitcoin, sui, near, ripple, polkadot, dogecoin, litecoin, monad, berachain, unichain, hyperevm, sonic, and 20+ more.
2920
+
2921
+ Examples:
2922
+ blockrun_rpc({ network: "ethereum", method: "eth_blockNumber" })
2923
+ blockrun_rpc({ network: "base", method: "eth_getBalance", params: ["0xabc...", "latest"] })
2924
+ blockrun_rpc({ network: "solana", method: "getSlot" })
2925
+ blockrun_rpc({ network: "bitcoin", method: "getblockchaininfo" })
2926
+ blockrun_rpc({ network: "ethereum", body: [{jsonrpc:"2.0",id:1,method:"eth_blockNumber"},{...}] }) // batch
2927
+
2928
+ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_surf (labeled/aggregated data) when they cover the question \u2014 this tool is for raw chain access.`,
2929
+ inputSchema: {
2930
+ network: z17.string().describe("Chain key, e.g. 'ethereum', 'base', 'solana', 'bitcoin', 'arbitrum', 'polygon'. Unknown slugs pass through to the Tatum gateway."),
2931
+ method: z17.string().optional().describe("JSON-RPC method, e.g. 'eth_blockNumber', 'eth_call', 'getSlot' (Solana), 'getblockchaininfo' (Bitcoin). Required unless 'body' is set."),
2932
+ params: z17.any().optional().describe("JSON-RPC params array for the method, e.g. ['0xabc...', 'latest']."),
2933
+ body: z17.any().optional().describe("Full JSON-RPC 2.0 body or an array of them (batch). Overrides method/params when set."),
2934
+ agent_id: z17.string().optional().describe("Agent identifier for budget tracking and enforcement.")
2935
+ }
2936
+ },
2937
+ async ({ network, method, params, body, agent_id }) => {
2938
+ try {
2939
+ body = coerceBody(body);
2940
+ if (body === void 0) {
2941
+ if (!method) {
2942
+ return {
2943
+ content: [{ type: "text", text: formatError("Provide either 'method' (with optional 'params') or a full JSON-RPC 'body'.") }],
2944
+ isError: true
2945
+ };
2946
+ }
2947
+ body = { jsonrpc: "2.0", id: 1, method, params: params ?? [] };
2948
+ }
2949
+ const batchCount = Array.isArray(body) ? Math.max(body.length, 1) : 1;
2950
+ const estimatedCost = RPC_PRICE_USD * batchCount;
2951
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2952
+ if (!budgetCheck.allowed) {
2953
+ return {
2954
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2955
+ isError: true
2956
+ };
2957
+ }
2958
+ const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
2959
+ const client = getClient();
2960
+ const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
2961
+ recordSpending(budget, estimatedCost, agent_id);
2962
+ return {
2963
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2964
+ structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
2965
+ };
2966
+ } catch (err) {
2967
+ return {
2968
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
2969
+ isError: true
2970
+ };
2971
+ }
2972
+ }
2973
+ );
2974
+ }
2975
+
2976
+ // src/tools/defi.ts
2977
+ import { z as z18 } from "zod";
2978
+ function estimateDefiCost(path5) {
2979
+ return path5.startsWith("prices") ? 1e-3 : 5e-3;
2980
+ }
2981
+ function registerDefiTool(server, budget) {
2982
+ server.registerTool(
2983
+ "blockrun_defi",
2984
+ {
2985
+ description: `DeFi fundamentals via DefiLlama \u2014 protocol TVL, chain TVL, yield pools (APY), token prices. Pays per call in USDC, no API key.
2986
+
2987
+ Paths (GET only):
2988
+ - protocols ($0.005) \u2014 all DeFi protocols ranked by TVL
2989
+ - protocol/{slug} ($0.005) \u2014 one protocol's TVL history + chain breakdown, e.g. protocol/aave-v3
2990
+ - chains ($0.005) \u2014 TVL by chain
2991
+ - yields ($0.005) \u2014 yield pools with APY + TVL (large; filter client-side)
2992
+ - prices/{coins} ($0.001) \u2014 token prices, coins like 'base:0x833589...,coingecko:ethereum'
2993
+
2994
+ Examples:
2995
+ blockrun_defi({ path: "protocol/uniswap-v3" })
2996
+ blockrun_defi({ path: "prices/coingecko:bitcoin,coingecko:ethereum" })
2997
+ blockrun_defi({ path: "chains" })
2998
+
2999
+ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pairs, blockrun_surf for labeled on-chain data \u2014 this tool is for protocol/TVL/yield fundamentals.`,
3000
+ inputSchema: {
3001
+ path: z18.string().describe("Endpoint under /v1/defillama/, e.g. 'protocols', 'protocol/aave-v3', 'chains', 'yields', 'prices/coingecko:ethereum'"),
3002
+ agent_id: z18.string().optional().describe("Agent identifier for budget tracking and enforcement.")
3003
+ }
3004
+ },
3005
+ async ({ path: path5, agent_id }) => {
3006
+ try {
3007
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/defillama\//, "").replace(/^api\/v1\/defillama\//, "");
3008
+ const estimatedCost = estimateDefiCost(cleanPath);
3009
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3010
+ if (!budgetCheck.allowed) {
3011
+ return {
3012
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3013
+ isError: true
3014
+ };
3015
+ }
3016
+ const client = getClient();
3017
+ const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
3018
+ recordSpending(budget, estimatedCost, agent_id);
3019
+ return {
3020
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3021
+ structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3022
+ };
3023
+ } catch (err) {
3024
+ return {
3025
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
3026
+ isError: true
3027
+ };
3028
+ }
3029
+ }
3030
+ );
3031
+ }
3032
+
2783
3033
  // src/mcp-handler.ts
2784
3034
  function initializeMcpServer(server) {
2785
3035
  const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
@@ -2800,6 +3050,8 @@ function initializeMcpServer(server) {
2800
3050
  registerModalTool(server, budget);
2801
3051
  registerPhoneTool(server, budget);
2802
3052
  registerSurfTool(server, budget);
3053
+ registerRpcTool(server, budget);
3054
+ registerDefiTool(server, budget);
2803
3055
  server.registerResource(
2804
3056
  "wallet",
2805
3057
  "blockrun://wallet",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.19.2",
3
+ "version": "0.21.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",