@blockrun/mcp 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -4
  2. package/dist/index.js +846 -482
  3. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -4,26 +4,32 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
- import { LLMClient } from "@blockrun/llm";
7
+ import { LLMClient, ImageClient } from "@blockrun/llm";
8
8
  import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
9
9
  import * as fs from "fs";
10
10
  import * as path from "path";
11
11
  import * as os from "os";
12
+ import QRCode from "qrcode";
13
+ import { Jimp } from "jimp";
14
+ import open from "open";
12
15
  var WALLET_DIR = path.join(os.homedir(), ".blockrun");
13
16
  var WALLET_FILE = path.join(WALLET_DIR, ".session");
17
+ var QR_FILE = path.join(WALLET_DIR, "qr.png");
18
+ var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
19
+ var BASE_CHAIN_ID = "8453";
14
20
  var MODEL_TIERS = {
15
- fast: ["google/gemini-2.5-flash", "openai/gpt-4o-mini", "deepseek/deepseek-chat"],
16
- balanced: ["openai/gpt-4o", "anthropic/claude-sonnet-4", "google/gemini-2.5-pro"],
17
- powerful: ["openai/gpt-5.2", "anthropic/claude-opus-4", "openai/o3"],
18
- cheap: ["google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-4o-mini"],
19
- reasoning: ["openai/o3", "openai/o1", "deepseek/deepseek-reasoner"]
21
+ fast: ["google/gemini-2.5-flash", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
22
+ balanced: ["openai/gpt-5.4", "anthropic/claude-sonnet-4.6", "google/gemini-2.5-pro", "openai/gpt-5.3"],
23
+ powerful: ["openai/gpt-5.4", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.5", "openai/o3"],
24
+ cheap: ["nvidia/gpt-oss-120b", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
25
+ reasoning: ["openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner"]
20
26
  };
21
27
  var walletWasCreated = false;
22
28
  var walletAddress = null;
23
29
  var client = null;
30
+ var imageClient = null;
24
31
  var cachedModels = null;
25
32
  var sessionBudget = { limit: null, spent: 0, calls: 0 };
26
- var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
27
33
  var BASE_RPC_URLS = [
28
34
  "https://mainnet.base.org",
29
35
  "https://base.llamarpc.com",
@@ -69,6 +75,13 @@ function getClient() {
69
75
  }
70
76
  return client;
71
77
  }
78
+ function getImageClient() {
79
+ if (!imageClient) {
80
+ const privateKey = getOrCreateWalletKey();
81
+ imageClient = new ImageClient({ privateKey });
82
+ }
83
+ return imageClient;
84
+ }
72
85
  function getWalletInfo() {
73
86
  const llm = getClient();
74
87
  const address = llm.getWalletAddress();
@@ -78,65 +91,9 @@ function getWalletInfo() {
78
91
  chainId: 8453,
79
92
  currency: "USDC",
80
93
  isNew: walletWasCreated,
81
- basescanUrl: `https://basescan.org/address/${address}`,
82
- fundingOptions: {
83
- coinbase: "Send USDC, select 'Base' network",
84
- bridge: "https://bridge.base.org",
85
- buy: "https://www.coinbase.com/onramp"
86
- }
94
+ basescanUrl: `https://basescan.org/address/${address}`
87
95
  };
88
96
  }
89
- function getWalletSetupInstructions() {
90
- if (!walletAddress) {
91
- getClient();
92
- }
93
- return `
94
- ================================================================================
95
- BLOCKRUN WALLET SETUP
96
- ================================================================================
97
-
98
- Your wallet address: ${walletAddress}
99
-
100
- To use BlockRun AI models, you need USDC on Base network.
101
-
102
- HOW TO FUND YOUR WALLET:
103
- ------------------------
104
-
105
- Option 1: Transfer from Coinbase
106
- 1. Open Coinbase app or website
107
- 2. Go to Send/Receive
108
- 3. Select USDC
109
- 4. Choose "Base" network (important!)
110
- 5. Paste address: ${walletAddress}
111
- 6. Send any amount ($5 is enough to start)
112
-
113
- Option 2: Bridge from other chains
114
- 1. Go to https://bridge.base.org
115
- 2. Connect your existing wallet
116
- 3. Bridge USDC to Base
117
- 4. Send to: ${walletAddress}
118
-
119
- Option 3: Buy directly
120
- 1. Go to https://www.coinbase.com/onramp
121
- 2. Buy USDC on Base network
122
- 3. Send to: ${walletAddress}
123
-
124
- VERIFY YOUR BALANCE:
125
- https://basescan.org/address/${walletAddress}
126
-
127
- PRICING (pay only for what you use):
128
- - GPT-4o: ~$0.005 per request
129
- - Claude Sonnet: ~$0.003 per request
130
- - Gemini Flash: ~$0.0001 per request
131
- - Full pricing: https://blockrun.ai/pricing
132
-
133
- SECURITY NOTE:
134
- Your private key is stored at: ~/.blockrun/.session
135
- This key NEVER leaves your machine - only used for signing payments locally.
136
-
137
- ================================================================================
138
- `;
139
- }
140
97
  async function getUsdcBalance(address) {
141
98
  const data = {
142
99
  jsonrpc: "2.0",
@@ -164,6 +121,42 @@ async function getUsdcBalance(address) {
164
121
  }
165
122
  return null;
166
123
  }
124
+ function getEip681Uri(address, amountUsdc = 1) {
125
+ const amountWei = Math.floor(amountUsdc * 1e6);
126
+ return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
127
+ }
128
+ async function generateQrPng(address) {
129
+ const eip681Uri = getEip681Uri(address);
130
+ const qrBuffer = await QRCode.toBuffer(eip681Uri, {
131
+ type: "png",
132
+ width: 400,
133
+ margin: 2,
134
+ errorCorrectionLevel: "H",
135
+ color: { dark: "#000000", light: "#FFFFFF" }
136
+ });
137
+ const qrImage = await Jimp.read(qrBuffer);
138
+ try {
139
+ const logoUrl = "https://avatars.githubusercontent.com/u/108554348?s=200&v=4";
140
+ const logo = await Jimp.read(logoUrl);
141
+ const logoSize = Math.floor(qrImage.width * 0.2);
142
+ logo.resize({ w: logoSize, h: logoSize });
143
+ const x = Math.floor((qrImage.width - logoSize) / 2);
144
+ const y = Math.floor((qrImage.height - logoSize) / 2);
145
+ qrImage.composite(logo, x, y);
146
+ } catch {
147
+ }
148
+ if (!fs.existsSync(WALLET_DIR)) {
149
+ fs.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
150
+ }
151
+ await qrImage.write(QR_FILE);
152
+ return QR_FILE;
153
+ }
154
+ async function openQrInViewer(qrPath) {
155
+ try {
156
+ await open(qrPath);
157
+ } catch {
158
+ }
159
+ }
167
160
  function recordSpending(cost) {
168
161
  sessionBudget.spent += cost;
169
162
  sessionBudget.calls += 1;
@@ -175,90 +168,226 @@ function checkBudget() {
175
168
  const remaining = sessionBudget.limit - sessionBudget.spent;
176
169
  return { allowed: remaining > 0, remaining };
177
170
  }
171
+ function formatError(message) {
172
+ const msgLower = message.toLowerCase();
173
+ const isPaymentError = msgLower.includes("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !msgLower.includes("500");
174
+ const isServerError = msgLower.includes("500") || msgLower.includes("api error after payment");
175
+ let errorText = `Error: ${message}`;
176
+ if (isServerError) {
177
+ errorText += `
178
+
179
+ This is a temporary API issue. The xAI/Grok API may be experiencing problems.
180
+ Try again in a few minutes, or use a different model (e.g., openai/gpt-4o).`;
181
+ } else if (isPaymentError) {
182
+ errorText += `
183
+
184
+ This error usually means your wallet needs funding.
185
+ Run blockrun_wallet with action: "setup" to get funding instructions.
186
+
187
+ Quick fix: Send USDC to your wallet on Base network.`;
188
+ }
189
+ return errorText;
190
+ }
178
191
  var server = new McpServer({
179
192
  name: "blockrun-mcp",
180
- version: "0.3.0"
193
+ version: "0.4.0"
181
194
  });
182
195
  server.registerTool(
183
- "blockrun_chat",
196
+ "blockrun_wallet",
184
197
  {
185
- description: `Chat with any AI model via BlockRun. Supports 30+ models including GPT-5, Claude Opus 4, Gemini 3, and more.
186
- Pay-per-request with x402 micropayments - no API keys needed.
198
+ description: `Manage your BlockRun wallet - check status, get funding instructions, open QR code, or manage session budget.
187
199
 
188
- Popular models:
189
- - openai/gpt-5.2: Most capable OpenAI model
190
- - anthropic/claude-opus-4: Best for complex reasoning
191
- - anthropic/claude-sonnet-4: Fast & capable (recommended)
192
- - google/gemini-2.5-pro: Great for long context
193
- - deepseek/deepseek-chat: Very affordable
200
+ Actions:
201
+ - status: Show wallet address, balance, and basescan link (default)
202
+ - setup: Full funding instructions with QR code
203
+ - qr: Generate and open QR code for easy funding
204
+ - budget: Manage session spending limit
194
205
 
195
- Use blockrun_models to see all available models with pricing.`,
206
+ Examples:
207
+ blockrun_wallet() -> status + balance
208
+ blockrun_wallet({ action: "setup" }) -> funding instructions + QR
209
+ blockrun_wallet({ action: "qr" }) -> open QR code
210
+ blockrun_wallet({ action: "budget", budget_action: "set", budget_amount: 1.00 })`,
196
211
  inputSchema: {
197
- model: z.string().describe("Model ID (e.g., 'anthropic/claude-sonnet-4', 'openai/gpt-4o'). Use blockrun_models to list all."),
198
- message: z.string().describe("Your message to the AI"),
199
- system: z.string().optional().describe("Optional system prompt to set context/behavior"),
200
- max_tokens: z.number().optional().default(1024).describe("Maximum tokens in response"),
201
- temperature: z.number().optional().default(1).describe("Creativity level 0-2")
212
+ action: z.enum(["status", "setup", "qr", "budget"]).optional().default("status").describe("What to do"),
213
+ budget_action: z.enum(["set", "check", "clear"]).optional().describe("Budget action (for action='budget')"),
214
+ budget_amount: z.number().optional().describe("Budget limit in USD (for budget_action='set')")
202
215
  }
203
216
  },
204
- async ({ model, message, system, max_tokens, temperature }) => {
205
- try {
206
- const llm = getClient();
207
- const response = await llm.chat(model, message, {
208
- system,
209
- maxTokens: max_tokens,
210
- temperature
211
- });
212
- return { content: [{ type: "text", text: response }] };
213
- } catch (error) {
214
- const errorMessage = error instanceof Error ? error.message : String(error);
217
+ async ({ action, budget_action, budget_amount }) => {
218
+ const info = getWalletInfo();
219
+ const address = info.address;
220
+ if (action === "budget") {
221
+ const budgetAct = budget_action || "check";
222
+ if (budgetAct === "set") {
223
+ if (budget_amount === void 0 || budget_amount <= 0) {
224
+ return {
225
+ content: [{ type: "text", text: "Error: Provide a positive budget_amount (e.g., 1.00 for $1.00)" }],
226
+ isError: true
227
+ };
228
+ }
229
+ sessionBudget.limit = budget_amount;
230
+ } else if (budgetAct === "clear") {
231
+ sessionBudget.limit = null;
232
+ }
233
+ const remaining = sessionBudget.limit !== null ? sessionBudget.limit - sessionBudget.spent : null;
234
+ const limitStr = sessionBudget.limit !== null ? `$${sessionBudget.limit.toFixed(2)}` : "Unlimited";
235
+ const remainingStr = remaining !== null ? `$${remaining.toFixed(4)}` : "N/A";
215
236
  return {
216
- content: [{ type: "text", text: formatError(errorMessage) }],
217
- isError: true
237
+ content: [{ type: "text", text: `Session Budget: ${limitStr} | Spent: $${sessionBudget.spent.toFixed(4)} | Calls: ${sessionBudget.calls} | Remaining: ${remainingStr}${budgetAct === "set" ? ` | Set to $${budget_amount?.toFixed(2)}` : ""}${budgetAct === "clear" ? " | Limit removed" : ""}` }],
238
+ structuredContent: {
239
+ limit: sessionBudget.limit,
240
+ spent: sessionBudget.spent,
241
+ calls: sessionBudget.calls,
242
+ remaining
243
+ }
218
244
  };
219
245
  }
246
+ if (action === "qr") {
247
+ try {
248
+ const qrPath = await generateQrPng(address);
249
+ await openQrInViewer(qrPath);
250
+ return {
251
+ content: [{ type: "text", text: `QR code opened! Scan with MetaMask to send USDC on Base.
252
+
253
+ Address: ${address}
254
+ QR saved: ${qrPath}` }]
255
+ };
256
+ } catch (err) {
257
+ return {
258
+ content: [{ type: "text", text: `Failed to generate QR: ${err}` }],
259
+ isError: true
260
+ };
261
+ }
262
+ }
263
+ if (action === "setup") {
264
+ let qrMessage = "";
265
+ try {
266
+ const qrPath = await generateQrPng(address);
267
+ await openQrInViewer(qrPath);
268
+ qrMessage = `
269
+ QR code opened for scanning! (${qrPath})`;
270
+ } catch {
271
+ qrMessage = "\n(QR generation failed - use address above)";
272
+ }
273
+ const text2 = `
274
+ ================================================================================
275
+ BLOCKRUN WALLET SETUP
276
+ ================================================================================
277
+
278
+ Your wallet address: ${address}
279
+ ${qrMessage}
280
+
281
+ HOW TO FUND YOUR WALLET:
282
+ ------------------------
283
+
284
+ Option 1: Transfer from Coinbase
285
+ 1. Open Coinbase app or website
286
+ 2. Go to Send/Receive -> Select USDC
287
+ 3. Choose "Base" network (important!)
288
+ 4. Paste: ${address}
289
+ 5. Send $1-5 to start
290
+
291
+ Option 2: Bridge from other chains
292
+ https://bridge.base.org -> Bridge USDC to Base -> Send to address above
293
+
294
+ Option 3: Buy directly
295
+ https://www.coinbase.com/onramp -> Buy USDC on Base -> Send to address above
296
+
297
+ VERIFY BALANCE: https://basescan.org/address/${address}
298
+
299
+ PRICING (pay per use):
300
+ - GPT-4o: ~$0.005/request | Claude Sonnet: ~$0.003/request
301
+ - Gemini Flash: ~$0.0001/request | Full pricing: https://blockrun.ai/pricing
302
+
303
+ SECURITY: Private key stored at ~/.blockrun/.session (never leaves your machine)
304
+ ================================================================================`;
305
+ return { content: [{ type: "text", text: text2 }] };
306
+ }
307
+ const balance = await getUsdcBalance(address);
308
+ const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch";
309
+ const lowBalance = balance !== null && balance < 1;
310
+ const text = `Wallet: ${address}
311
+ Balance: ${balanceStr}${lowBalance ? " (low - add funds)" : ""}
312
+ Network: Base | View: ${info.basescanUrl}
313
+ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions" : ""}`;
314
+ return {
315
+ content: [{ type: "text", text }],
316
+ structuredContent: {
317
+ address: info.address,
318
+ balance,
319
+ network: info.network,
320
+ chainId: info.chainId,
321
+ isNew: info.isNew,
322
+ basescanUrl: info.basescanUrl
323
+ }
324
+ };
220
325
  }
221
326
  );
222
327
  server.registerTool(
223
- "blockrun_smart",
328
+ "blockrun_chat",
224
329
  {
225
- description: `Smart model routing - automatically picks the best model based on your needs.
330
+ description: `Chat with AI models via BlockRun. Supports 30+ models with pay-per-request micropayments.
331
+
332
+ Two ways to use:
333
+ 1. Specify a model directly: model: "openai/gpt-5.4"
334
+ 2. Use smart routing: mode: "fast" | "balanced" | "powerful" | "cheap" | "reasoning"
335
+
336
+ Popular models:
337
+ - openai/gpt-5.4, openai/gpt-5.4-mini, openai/gpt-5.4-nano
338
+ - anthropic/claude-opus-4.6, anthropic/claude-sonnet-4.6
339
+ - google/gemini-2.5-pro, google/gemini-2.5-flash
340
+ - deepseek/deepseek-chat (very affordable)
226
341
 
227
- Modes:
228
- - fast: Quickest response (Gemini Flash, GPT-4o-mini)
229
- - balanced: Good quality & speed (GPT-4o, Claude Sonnet)
230
- - powerful: Best quality (GPT-5.2, Claude Opus 4, o3)
231
- - cheap: Lowest cost (Gemini Flash, DeepSeek)
232
- - reasoning: Complex logic (o3, o1, DeepSeek Reasoner)
342
+ Smart routing modes:
343
+ - fast: Gemini Flash, GPT-5 Mini (quickest)
344
+ - balanced: GPT-5.4, Claude Sonnet 4.6 (good default)
345
+ - powerful: GPT-5.4, Claude Opus 4.6 (best quality)
346
+ - cheap: DeepSeek, Gemini Flash (lowest cost)
347
+ - reasoning: o3, o1 (complex logic)
233
348
 
234
- Example: blockrun_smart({ mode: "fast", message: "Hello" })`,
349
+ Use blockrun_models to see all available models with pricing.`,
235
350
  inputSchema: {
236
- mode: z.enum(["fast", "balanced", "powerful", "cheap", "reasoning"]).describe("Routing mode"),
237
351
  message: z.string().describe("Your message to the AI"),
352
+ model: z.string().optional().describe("Specific model ID (e.g., 'openai/gpt-4o')"),
353
+ mode: z.enum(["fast", "balanced", "powerful", "cheap", "reasoning"]).optional().describe("Smart routing mode (ignored if model specified)"),
238
354
  system: z.string().optional().describe("Optional system prompt"),
239
- max_tokens: z.number().optional().default(1024).describe("Maximum tokens in response")
240
- },
241
- outputSchema: {
242
- model_used: z.string().describe("The model that was used"),
243
- response: z.string().describe("The AI response")
355
+ max_tokens: z.number().optional().default(1024).describe("Max tokens in response"),
356
+ temperature: z.number().optional().default(1).describe("Creativity 0-2")
244
357
  }
245
358
  },
246
- async ({ mode, message, system, max_tokens }) => {
247
- const models = MODEL_TIERS[mode];
248
- let lastError = null;
249
- for (const model of models) {
359
+ async ({ message, model, mode, system, max_tokens, temperature }) => {
360
+ const llm = getClient();
361
+ if (model) {
250
362
  try {
251
- const llm = getClient();
252
363
  const response = await llm.chat(model, message, {
364
+ system,
365
+ maxTokens: max_tokens,
366
+ temperature
367
+ });
368
+ return { content: [{ type: "text", text: response }] };
369
+ } catch (error) {
370
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
371
+ return {
372
+ content: [{ type: "text", text: formatError(errorMessage2) }],
373
+ isError: true
374
+ };
375
+ }
376
+ }
377
+ const routingMode = mode || "balanced";
378
+ const models = MODEL_TIERS[routingMode];
379
+ let lastError = null;
380
+ for (const m of models) {
381
+ try {
382
+ const response = await llm.chat(m, message, {
253
383
  system,
254
384
  maxTokens: max_tokens
255
385
  });
256
- const result = { model_used: model, response };
257
386
  return {
258
- content: [{ type: "text", text: `[Used: ${model}]
387
+ content: [{ type: "text", text: `[${m}]
259
388
 
260
389
  ${response}` }],
261
- structuredContent: result
390
+ structuredContent: { model_used: m, response }
262
391
  };
263
392
  } catch (error) {
264
393
  lastError = error;
@@ -275,19 +404,10 @@ ${response}` }],
275
404
  server.registerTool(
276
405
  "blockrun_models",
277
406
  {
278
- description: "List all available AI models with pricing. Use this to discover models and compare costs.",
407
+ description: "List available AI models with pricing. Use to discover models and compare costs.",
279
408
  inputSchema: {
280
409
  category: z.enum(["all", "chat", "reasoning", "image", "embedding"]).optional().default("all").describe("Filter by category"),
281
- provider: z.string().optional().describe("Filter by provider (e.g., 'openai', 'anthropic', 'google')")
282
- },
283
- outputSchema: {
284
- count: z.number().describe("Number of models returned"),
285
- models: z.array(z.object({
286
- id: z.string(),
287
- name: z.string().optional(),
288
- inputPrice: z.number().optional(),
289
- outputPrice: z.number().optional()
290
- })).describe("List of available models")
410
+ provider: z.string().optional().describe("Filter by provider (e.g., 'openai', 'anthropic')")
291
411
  }
292
412
  },
293
413
  async ({ category, provider }) => {
@@ -305,234 +425,424 @@ server.registerTool(
305
425
  }
306
426
  if (category && category !== "all") {
307
427
  if (category === "image") {
308
- models = models.filter(
309
- (m) => m.id.includes("dall-e") || m.id.includes("flux") || m.id.includes("banana")
310
- );
311
- } else if (category === "reasoning") {
312
- models = models.filter(
313
- (m) => m.id.includes("/o1") || m.id.includes("/o3") || m.id.includes("reasoner")
314
- );
428
+ models = models.filter((m) => m.id.includes("dall-e") || m.id.includes("flux") || m.id.includes("banana"));
315
429
  } else if (category === "embedding") {
316
430
  models = models.filter((m) => m.id.includes("embed"));
431
+ } else {
432
+ models = models.filter((m) => m.categories?.includes(category));
317
433
  }
318
434
  }
319
435
  const lines = models.map((m) => {
320
436
  const input = m.inputPrice ? `$${m.inputPrice}/M in` : "";
321
437
  const output = m.outputPrice ? `$${m.outputPrice}/M out` : "";
322
438
  const pricing = [input, output].filter(Boolean).join(", ");
323
- return `- ${m.id}: ${m.name || ""} ${pricing ? `(${pricing})` : ""}`;
439
+ const ctx = m.contextWindow ? ` | ${Math.round(m.contextWindow / 1e3)}K ctx` : "";
440
+ const cats = m.categories?.length ? ` [${m.categories.join(", ")}]` : "";
441
+ return `- ${m.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
324
442
  });
325
- const structuredModels = models.map((m) => ({
326
- id: m.id,
327
- name: m.name,
328
- inputPrice: m.inputPrice,
329
- outputPrice: m.outputPrice
330
- }));
331
443
  return {
332
- content: [{ type: "text", text: `Available models (${models.length}):
333
-
444
+ content: [{ type: "text", text: `Models (${models.length}):
334
445
  ${lines.join("\n")}` }],
335
- structuredContent: { count: models.length, models: structuredModels }
446
+ structuredContent: { count: models.length, models }
336
447
  };
337
448
  }
338
449
  );
339
450
  server.registerTool(
340
451
  "blockrun_image",
341
452
  {
342
- description: `Generate images using AI models. Supports DALL-E 3, Flux, and Nano Banana.
343
-
344
- Models:
345
- - openai/dall-e-3: High quality, creative ($0.04-0.08/image)
346
- - together/flux-schnell: Fast generation ($0.02/image)
347
- - google/nano-banana: Experimental Google model`,
453
+ description: `Generate images. Models: openai/dall-e-3 ($0.04-0.08), together/flux-schnell ($0.02), google/nano-banana`,
348
454
  inputSchema: {
349
- prompt: z.string().describe("Description of the image to generate"),
350
- model: z.enum(["openai/dall-e-3", "together/flux-schnell", "google/nano-banana"]).optional().default("openai/dall-e-3").describe("Image model"),
351
- size: z.enum(["1024x1024", "1792x1024", "1024x1792"]).optional().default("1024x1024").describe("Image size"),
352
- quality: z.enum(["standard", "hd"]).optional().default("standard").describe("Quality level for DALL-E 3")
353
- },
354
- outputSchema: {
355
- url: z.string().describe("URL of the generated image"),
356
- prompt: z.string().describe("The prompt used"),
357
- model: z.string().describe("The model used")
455
+ prompt: z.string().describe("Image description"),
456
+ model: z.enum(["openai/dall-e-3", "together/flux-schnell", "google/nano-banana"]).optional().default("openai/dall-e-3"),
457
+ size: z.enum(["1024x1024", "1792x1024", "1024x1792"]).optional().default("1024x1024"),
458
+ quality: z.enum(["standard", "hd"]).optional().default("standard")
358
459
  }
359
460
  },
360
461
  async ({ prompt, model, size, quality }) => {
361
- const apiUrl = "https://blockrun.ai/api/v1/images/generations";
362
- const body = {
363
- model,
364
- prompt,
365
- size,
366
- quality,
367
- n: 1
368
- };
369
- const response = await fetch(apiUrl, {
370
- method: "POST",
371
- headers: { "Content-Type": "application/json" },
372
- body: JSON.stringify(body)
373
- });
374
- if (response.status === 402) {
462
+ try {
463
+ const imgClient = getImageClient();
464
+ const response = await imgClient.generate(prompt, {
465
+ model,
466
+ size,
467
+ quality
468
+ });
469
+ const imageUrl = response.data?.[0]?.url;
470
+ if (!imageUrl) {
471
+ return {
472
+ content: [{ type: "text", text: formatError("No image URL in response") }],
473
+ isError: true
474
+ };
475
+ }
375
476
  return {
376
- content: [{ type: "text", text: `Image generation requires payment. Please ensure your wallet has USDC on Base.
377
-
378
- To generate "${prompt}" with ${model}, the approximate cost is $0.04-0.08 per image.` }],
477
+ content: [{ type: "text", text: `Image: ${imageUrl}
478
+ Prompt: ${prompt}
479
+ Model: ${model}` }],
480
+ structuredContent: { url: imageUrl, prompt, model }
481
+ };
482
+ } catch (err) {
483
+ const errMsg = err instanceof Error ? err.message : String(err);
484
+ if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402")) {
485
+ return {
486
+ content: [{ type: "text", text: `Image generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
487
+ Error: ${errMsg}` }],
488
+ isError: true
489
+ };
490
+ }
491
+ return {
492
+ content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}`) }],
379
493
  isError: true
380
494
  };
381
495
  }
382
- if (!response.ok) {
496
+ }
497
+ );
498
+ server.registerTool(
499
+ "blockrun_twitter",
500
+ {
501
+ description: `Search real-time X/Twitter via Grok. Use for trending topics, @handles, breaking news.`,
502
+ inputSchema: {
503
+ query: z.string().describe("Search query (can include @handles, topics)"),
504
+ max_results: z.number().optional().default(10).describe("Max results (1-25)")
505
+ }
506
+ },
507
+ async ({ query, max_results }) => {
508
+ const budget = checkBudget();
509
+ if (!budget.allowed) {
383
510
  return {
384
- content: [{ type: "text", text: formatError(`Image generation failed: ${response.status}`) }],
511
+ content: [{ type: "text", text: `Budget limit reached ($${sessionBudget.spent.toFixed(4)} of $${sessionBudget.limit?.toFixed(2)}). Use blockrun_wallet with action: "budget" to adjust.` }],
385
512
  isError: true
386
513
  };
387
514
  }
388
- const data = await response.json();
389
- const imageUrl = data.data?.[0]?.url;
390
- if (!imageUrl) {
515
+ try {
516
+ const llm = getClient();
517
+ const response = await llm.chat("xai/grok-3", query, {
518
+ system: `Real-time X/Twitter search. Focus on recent posts, key accounts, engagement. Max results: ${max_results}`,
519
+ search: true
520
+ });
521
+ recordSpending(2e-3);
522
+ return {
523
+ content: [{ type: "text", text: `[X/Twitter via Grok]
524
+
525
+ ${response}` }],
526
+ structuredContent: { query, model: "xai/grok-3", response }
527
+ };
528
+ } catch (error) {
529
+ const errorMessage = error instanceof Error ? error.message : String(error);
391
530
  return {
392
- content: [{ type: "text", text: formatError("No image URL in response") }],
531
+ content: [{ type: "text", text: formatError(errorMessage) }],
393
532
  isError: true
394
533
  };
395
534
  }
396
- return {
397
- content: [{ type: "text", text: `Image generated successfully!
398
-
399
- URL: ${imageUrl}
400
-
401
- Prompt: ${prompt}
402
- Model: ${model}` }],
403
- structuredContent: { url: imageUrl, prompt, model }
404
- };
405
535
  }
406
536
  );
407
537
  server.registerTool(
408
- "blockrun_wallet",
538
+ "blockrun_dex",
409
539
  {
410
- description: "Get information about your BlockRun wallet address. Shows address, network, and quick funding options.",
411
- inputSchema: {},
412
- outputSchema: {
413
- address: z.string().describe("Wallet address"),
414
- network: z.string().describe("Network name"),
415
- chainId: z.number().describe("Chain ID"),
416
- currency: z.string().describe("Currency"),
417
- isNew: z.boolean().describe("Whether this is a newly created wallet"),
418
- basescanUrl: z.string().describe("Link to view on Basescan")
419
- }
420
- },
421
- async () => {
422
- const info = getWalletInfo();
423
- const isNewWallet = info.isNew;
424
- let text = `BlockRun Wallet Information
425
- ============================
540
+ description: `Get real-time DEX data from DexScreener. FREE - no payment required.
426
541
 
427
- Address: ${info.address}
428
- Network: ${info.network} (Chain ID: ${info.chainId})
429
- Currency: ${info.currency}
542
+ Use for:
543
+ - Token prices and liquidity across chains
544
+ - Trading volume and price changes
545
+ - Finding token pairs and contracts
430
546
 
431
- View on Basescan: ${info.basescanUrl}
432
- `;
433
- if (isNewWallet) {
434
- text += `
435
- STATUS: NEW WALLET - NEEDS FUNDING
436
- ${getWalletSetupInstructions()}`;
437
- } else {
438
- text += `
439
- HOW TO ADD FUNDS:
440
- -----------------
441
- Send USDC to the address above on Base network.
442
-
443
- Quick options:
444
- 1. From Coinbase: ${info.fundingOptions.coinbase}
445
- 2. Bridge: ${info.fundingOptions.bridge}
446
- 3. Buy: ${info.fundingOptions.buy}
447
-
448
- Full instructions: Run blockrun_setup tool
449
- `;
547
+ Examples:
548
+ blockrun_dex({ query: "SOL" }) -> Search for SOL pairs
549
+ blockrun_dex({ token: "So11...xxx" }) -> Get specific token data
550
+ blockrun_dex({ symbol: "PEPE" }) -> Search by symbol`,
551
+ inputSchema: {
552
+ query: z.string().optional().describe("Search query (token name, symbol, or address)"),
553
+ token: z.string().optional().describe("Token address for direct lookup"),
554
+ symbol: z.string().optional().describe("Token symbol to search"),
555
+ chain: z.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
450
556
  }
451
- return {
452
- content: [{ type: "text", text }],
453
- structuredContent: {
454
- address: info.address,
455
- network: info.network,
456
- chainId: info.chainId,
457
- currency: info.currency,
458
- isNew: info.isNew,
459
- basescanUrl: info.basescanUrl
557
+ },
558
+ async ({ query, token, symbol, chain }) => {
559
+ try {
560
+ let url;
561
+ let searchTerm = query || symbol || "";
562
+ if (token) {
563
+ url = `https://api.dexscreener.com/latest/dex/tokens/${token}`;
564
+ } else if (searchTerm) {
565
+ url = `https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(searchTerm)}`;
566
+ } else {
567
+ return {
568
+ content: [{ type: "text", text: "Provide query, token address, or symbol" }],
569
+ isError: true
570
+ };
460
571
  }
461
- };
572
+ const response = await fetch(url);
573
+ if (!response.ok) {
574
+ throw new Error(`DexScreener API error: ${response.status}`);
575
+ }
576
+ const data = await response.json();
577
+ let pairs = data.pairs || [];
578
+ if (chain && pairs.length > 0) {
579
+ const chainLower = chain.toLowerCase();
580
+ pairs = pairs.filter((p) => p.chainId.toLowerCase().includes(chainLower));
581
+ }
582
+ pairs = pairs.sort((a, b) => (b.volume?.h24 || 0) - (a.volume?.h24 || 0)).slice(0, 10);
583
+ if (pairs.length === 0) {
584
+ return {
585
+ content: [{ type: "text", text: `No pairs found for: ${searchTerm || token}` }]
586
+ };
587
+ }
588
+ const lines = pairs.map((p) => {
589
+ const price = p.priceUsd ? `$${parseFloat(p.priceUsd).toFixed(6)}` : "N/A";
590
+ const change = p.priceChange?.h24 ? `${p.priceChange.h24 > 0 ? "+" : ""}${p.priceChange.h24.toFixed(2)}%` : "";
591
+ const vol = p.volume?.h24 ? `$${(p.volume.h24 / 1e6).toFixed(2)}M` : "";
592
+ const liq = p.liquidity?.usd ? `$${(p.liquidity.usd / 1e6).toFixed(2)}M liq` : "";
593
+ const buySell = p.txns?.h24 ? `${p.txns.h24.buys}B/${p.txns.h24.sells}S` : "";
594
+ return `${p.baseToken.symbol}/${p.quoteToken.symbol} (${p.chainId}/${p.dexId})
595
+ Price: ${price} ${change} | Vol: ${vol} | ${liq} | Txns: ${buySell}
596
+ Token: ${p.baseToken.address}`;
597
+ });
598
+ return {
599
+ content: [{ type: "text", text: `[DexScreener - FREE]
600
+
601
+ ${lines.join("\n\n")}` }],
602
+ structuredContent: { pairs, count: pairs.length }
603
+ };
604
+ } catch (error) {
605
+ const errorMessage = error instanceof Error ? error.message : String(error);
606
+ return {
607
+ content: [{ type: "text", text: `DexScreener error: ${errorMessage}` }],
608
+ isError: true
609
+ };
610
+ }
462
611
  }
463
612
  );
613
+ var KNOWN_LABELS = {
614
+ "0x28c6c06298d514db089934071355e5743bf21d60": "Binance 14",
615
+ "0x21a31ee1afc51d94c2efccaa2092ad1028285549": "Binance 15",
616
+ "0xdfd5293d8e347dfe59e90efd55b2956a1343963d": "Binance 16",
617
+ "0x56eddb7aa87536c09ccc2793473599fd21a8b17f": "Binance 17",
618
+ "0x9696f59e4d72e237be84ffd425dcad154bf96976": "Binance 18",
619
+ "0x4976a4a02f38326660d17bf34b431dc6e2eb2327": "Binance 19",
620
+ "0xf977814e90da44bfa03b6295a0616a897441acec": "Binance 8",
621
+ "0x5a52e96bacdabb82fd05763e25335261b270efcb": "Binance",
622
+ "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be": "Binance",
623
+ "0xd24400ae8bfebb18ca49be86258a3c749cf46853": "Gemini 2",
624
+ "0x6fc82a5fe25a5cdb58bc74600a40a69c065263f8": "Gemini 3",
625
+ "0x61edcdf5bb737adffe5043706e7c5bb1f1a56eea": "Gemini 4",
626
+ "0x07ee55aa48bb72dcc6e9d78256648910de513eca": "Gemini 5",
627
+ "0xdc76cd25977e0a5ae17155770273ad58648900d3": "Coinbase Prime",
628
+ "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43": "Coinbase 10",
629
+ "0x503828976d22510aad0201ac7ec88293211d23da": "Coinbase 2",
630
+ "0xddfabcdc4d8ffc6d5beaf154f18b778f892a0740": "Coinbase 3",
631
+ "0x3cd751e6b0078be393132286c442345e5dc49699": "Coinbase 4",
632
+ "0xb5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f511": "Coinbase 5",
633
+ "0xeb2629a2734e272bcc07bda959863f316f4bd4cf": "Coinbase 6",
634
+ "0x02466e547bfdab679fc49e96bbfc62b9747d997c": "Coinbase 8",
635
+ "0xa090e606e30bd747d4e6245a1517ebe430f0057e": "Coinbase",
636
+ "0x8103683202aa8da10536036edef04cdd865c225e": "Kraken 13",
637
+ "0x6cc5f688a315f3dc28a7781717a9a798a59fda7b": "OKX 1",
638
+ "0x236f9f97e0e62388479bf9e5ba4889e46b0273c3": "OKX 2",
639
+ "0x5041ed759dd4afc3a72b8192c143f72f4724081a": "OKX 4",
640
+ "0x75e89d5979e4f6fba9f97c104c2f0afb3f1dcb88": "MEXC",
641
+ "0x0d0707963952f2fba59dd06f2b425ace40b492fe": "Gate.io",
642
+ "0x1c4b70a3968436b9a0a9cf5205c787eb81bb558c": "Gate.io 3",
643
+ "0xd793281182a0e3e023116004778f45c29fc14f19": "Gate.io 4",
644
+ "0x974caa59e49682cda0ad2bbe82983419a2ecc400": "HTX",
645
+ "0x0211f3cedbef3143223d3acf0e589747933e8527": "HTX 2",
646
+ "0x1062a747393198f70f71ec65a582423dba7e5ab3": "Bybit",
647
+ "0xee5b5b923ffce93a870b3104b7ca09c3db80047a": "Bybit 2",
648
+ "0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503": "Binance: Foundation",
649
+ "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8": "Binance 7",
650
+ "0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf": "Polygon Bridge",
651
+ "0xa3a7b6f88361f48403514059f1f16c8e78d60eec": "Arbitrum Bridge",
652
+ "0x99c9fc46f92e8a1c0dec1b1747d010903e884be1": "Optimism Bridge",
653
+ "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a": "Arbitrum: Delayed Inbox",
654
+ "0x0000000000000000000000000000000000000000": "Null/Burn Address"
655
+ };
656
+ function getAddressLabel(address) {
657
+ const lower = address.toLowerCase();
658
+ return KNOWN_LABELS[lower] || shortenAddress(address);
659
+ }
660
+ function shortenAddress(address) {
661
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
662
+ }
464
663
  server.registerTool(
465
- "blockrun_setup",
664
+ "blockrun_whale",
466
665
  {
467
- description: `Get detailed wallet setup and funding instructions. Use this for first-time setup or if you need help adding funds to your wallet.
468
-
469
- Returns:
470
- - Your wallet address
471
- - Step-by-step funding instructions (Coinbase, bridge, direct purchase)
472
- - Pricing information
473
- - Security details`,
474
- inputSchema: {}
666
+ description: `Track large ETH transfers (whale movements). Uses BigQuery public data.
667
+
668
+ Shows:
669
+ - Large transfers (100+ ETH)
670
+ - Exchange inflows/outflows
671
+ - Labels for known addresses (Binance, Coinbase, etc.)
672
+
673
+ Note: Requires GOOGLE_APPLICATION_CREDENTIALS env var for BigQuery auth.
674
+ For MVP/demo: Returns simulated data if BigQuery not configured.`,
675
+ inputSchema: {
676
+ hours: z.number().optional().default(24).describe("Hours to look back (default: 24)"),
677
+ min_eth: z.number().optional().default(100).describe("Minimum ETH amount (default: 100)"),
678
+ limit: z.number().optional().default(20).describe("Max results (default: 20)")
679
+ }
475
680
  },
476
- async () => {
477
- getClient();
478
- return { content: [{ type: "text", text: getWalletSetupInstructions() }] };
681
+ async ({ hours, min_eth, limit }) => {
682
+ const hasGoogleCreds = process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
683
+ if (!hasGoogleCreds) {
684
+ const demoData = [
685
+ { from: "0xf977814e90da44bfa03b6295a0616a897441acec", to: "0x28c6c06298d514db089934071355e5743bf21d60", value: 5e3, time: "2h ago" },
686
+ { from: "0x503828976d22510aad0201ac7ec88293211d23da", to: "0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503", value: 3200, time: "4h ago" },
687
+ { from: "0x6cc5f688a315f3dc28a7781717a9a798a59fda7b", to: "0xd24400ae8bfebb18ca49be86258a3c749cf46853", value: 2100, time: "6h ago" },
688
+ { from: "0x1062a747393198f70f71ec65a582423dba7e5ab3", to: "0x99c9fc46f92e8a1c0dec1b1747d010903e884be1", value: 1800, time: "8h ago" },
689
+ { from: "0x75e89d5979e4f6fba9f97c104c2f0afb3f1dcb88", to: "0xa3a7b6f88361f48403514059f1f16c8e78d60eec", value: 1500, time: "12h ago" }
690
+ ];
691
+ const lines = demoData.map((t) => {
692
+ const fromLabel = getAddressLabel(t.from);
693
+ const toLabel = getAddressLabel(t.to);
694
+ return `${t.value.toLocaleString()} ETH | ${fromLabel} \u2192 ${toLabel} | ${t.time}`;
695
+ });
696
+ return {
697
+ content: [{
698
+ type: "text",
699
+ text: `[Whale Tracker - DEMO MODE]
700
+ \u26A0\uFE0F BigQuery not configured. Showing sample data.
701
+
702
+ To enable real data:
703
+ 1. Create GCP project: console.cloud.google.com
704
+ 2. Enable BigQuery API
705
+ 3. Set GOOGLE_APPLICATION_CREDENTIALS env var
706
+
707
+ Sample whale movements:
708
+ ${lines.join("\n")}
709
+
710
+ Total: ${demoData.reduce((s, t) => s + t.value, 0).toLocaleString()} ETH across ${demoData.length} transfers`
711
+ }],
712
+ structuredContent: { demo: true, transfers: demoData }
713
+ };
714
+ }
715
+ return {
716
+ content: [{
717
+ type: "text",
718
+ text: `[Whale Tracker]
719
+
720
+ BigQuery credentials detected. Real-time query:
721
+ - Looking back: ${hours}h
722
+ - Min transfer: ${min_eth} ETH
723
+ - Limit: ${limit} results
724
+
725
+ Query would run:
726
+ SELECT block_timestamp, from_address, to_address, value/1e18 as eth
727
+ FROM \`bigquery-public-data.crypto_ethereum.transactions\`
728
+ WHERE value > ${min_eth} * 1e18
729
+ AND block_timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL ${hours} HOUR)
730
+ ORDER BY value DESC
731
+ LIMIT ${limit}
732
+
733
+ Note: Full BigQuery integration coming soon.`
734
+ }]
735
+ };
479
736
  }
480
737
  );
481
738
  server.registerTool(
482
- "blockrun_twitter",
739
+ "blockrun_analyze",
483
740
  {
484
- description: `Search real-time X/Twitter data using Grok's live search.
485
-
486
- Use this tool for:
487
- - Checking what people are saying about a topic
488
- - Finding recent tweets from specific accounts
489
- - Getting trending discussions
490
- - Real-time news and events
491
-
492
- Example queries:
493
- - "what is @elonmusk posting about today"
494
- - "trending AI news"
495
- - "reactions to [event]"`,
741
+ description: `Comprehensive trading analysis combining multiple data sources.
742
+
743
+ Analyzes:
744
+ - DEX data (price, volume, liquidity) via DexScreener
745
+ - Twitter/X sentiment via Grok
746
+ - Whale movements (if BigQuery configured)
747
+ - AI synthesis of all data
748
+
749
+ Example: blockrun_analyze({ token: "SOL", question: "Should I buy?" })`,
496
750
  inputSchema: {
497
- query: z.string().describe("Search query - can include @handles, topics, or natural language questions"),
498
- max_results: z.number().optional().default(10).describe("Maximum number of results to return (1-25)")
499
- },
500
- outputSchema: {
501
- query: z.string(),
502
- model: z.string(),
503
- response: z.string()
751
+ token: z.string().describe("Token symbol or address to analyze"),
752
+ question: z.string().optional().describe("Specific question (default: general analysis)"),
753
+ include_twitter: z.boolean().optional().default(true).describe("Include Twitter sentiment"),
754
+ include_whale: z.boolean().optional().default(false).describe("Include whale tracking")
504
755
  }
505
756
  },
506
- async ({ query, max_results }) => {
507
- const budget = checkBudget();
508
- if (!budget.allowed) {
509
- return {
510
- content: [{ type: "text", text: `Budget limit reached. Spent $${sessionBudget.spent.toFixed(4)} of $${sessionBudget.limit?.toFixed(2)} limit.
511
-
512
- Use blockrun_budget to check or adjust your budget.` }],
513
- isError: true
514
- };
757
+ async ({ token, question, include_twitter, include_whale }) => {
758
+ const llm = getClient();
759
+ const analysisPrompt = question || `Provide comprehensive trading analysis for ${token}`;
760
+ let contextData = "";
761
+ try {
762
+ const dexUrl = `https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(token)}`;
763
+ const dexResponse = await fetch(dexUrl);
764
+ const dexData = await dexResponse.json();
765
+ if (dexData.pairs && dexData.pairs.length > 0) {
766
+ const topPair = dexData.pairs[0];
767
+ contextData += `
768
+ ## DEX Data (DexScreener)
769
+ `;
770
+ contextData += `- Token: ${topPair.baseToken.name} (${topPair.baseToken.symbol})
771
+ `;
772
+ contextData += `- Price: $${parseFloat(topPair.priceUsd).toFixed(6)}
773
+ `;
774
+ contextData += `- 24h Change: ${topPair.priceChange?.h24?.toFixed(2) || "N/A"}%
775
+ `;
776
+ contextData += `- 24h Volume: $${((topPair.volume?.h24 || 0) / 1e6).toFixed(2)}M
777
+ `;
778
+ contextData += `- Liquidity: $${((topPair.liquidity?.usd || 0) / 1e6).toFixed(2)}M
779
+ `;
780
+ contextData += `- FDV: $${((topPair.fdv || 0) / 1e9).toFixed(2)}B
781
+ `;
782
+ contextData += `- Chain: ${topPair.chainId}
783
+ `;
784
+ }
785
+ } catch (err) {
786
+ contextData += `
787
+ ## DEX Data: Error fetching
788
+ `;
789
+ }
790
+ if (include_twitter) {
791
+ try {
792
+ const twitterResponse = await llm.chat("xai/grok-3", `What are people saying about ${token} on Twitter/X right now? Focus on: sentiment, key influencers, trending topics, price predictions.`, {
793
+ system: "Real-time X/Twitter search. Provide factual summary of recent posts.",
794
+ search: true
795
+ });
796
+ contextData += `
797
+ ## Twitter/X Sentiment (via Grok)
798
+ ${twitterResponse}
799
+ `;
800
+ } catch {
801
+ contextData += `
802
+ ## Twitter: Unable to fetch
803
+ `;
804
+ }
805
+ }
806
+ if (include_whale) {
807
+ contextData += `
808
+ ## Whale Movements
809
+ `;
810
+ contextData += `Note: BigQuery not configured. In production, this would show:
811
+ `;
812
+ contextData += `- Large transfers to/from exchanges
813
+ `;
814
+ contextData += `- Smart money wallet movements
815
+ `;
816
+ contextData += `- Exchange inflow/outflow trends
817
+ `;
515
818
  }
819
+ const synthesisPrompt = `You are a crypto trading analyst. Based on the following data, answer: "${analysisPrompt}"
820
+
821
+ ${contextData}
822
+
823
+ Provide:
824
+ 1. Key findings (bullet points)
825
+ 2. Risk assessment (Low/Medium/High)
826
+ 3. Trading suggestion (if asked)
827
+ 4. What to watch for
828
+
829
+ Be factual and balanced. Don't give financial advice, but provide analysis based on the data.`;
516
830
  try {
517
- const llm = getClient();
518
- const model = "xai/grok-3";
519
- const system = `You are a real-time X/Twitter search assistant.
520
- When searching, focus on:
521
- - Recent and relevant posts
522
- - Key accounts and verified sources
523
- - Engagement metrics when relevant
524
- Format your response clearly with sources when available.
525
- Max results requested: ${max_results}`;
526
- const response = await llm.chat(model, query, {
527
- system,
528
- search: true
831
+ const analysis = await llm.chat("openai/gpt-4o", synthesisPrompt, {
832
+ system: "Expert crypto trading analyst. Provide data-driven analysis.",
833
+ maxTokens: 1500
529
834
  });
530
- recordSpending(2e-3);
531
835
  return {
532
- content: [{ type: "text", text: `[X/Twitter Search via Grok]
836
+ content: [{
837
+ type: "text",
838
+ text: `[BlockRun Trading Analysis: ${token}]
533
839
 
534
- ${response}` }],
535
- structuredContent: { query, model, response }
840
+ ${analysis}
841
+
842
+ ---
843
+ Data sources: DexScreener${include_twitter ? ", Twitter/X (Grok)" : ""}${include_whale ? ", Whale Tracker" : ""}`
844
+ }],
845
+ structuredContent: { token, question: analysisPrompt, analysis }
536
846
  };
537
847
  } catch (error) {
538
848
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -543,141 +853,272 @@ ${response}` }],
543
853
  }
544
854
  }
545
855
  );
856
+ function calculateEMA(prices, period) {
857
+ const k = 2 / (period + 1);
858
+ const ema = [prices[0]];
859
+ for (let i = 1; i < prices.length; i++) {
860
+ ema.push(prices[i] * k + ema[i - 1] * (1 - k));
861
+ }
862
+ return ema;
863
+ }
864
+ function calculateRSI(prices, period = 14) {
865
+ if (prices.length < period + 1) return 50;
866
+ let gains = 0, losses = 0;
867
+ for (let i = prices.length - period; i < prices.length; i++) {
868
+ const change = prices[i] - prices[i - 1];
869
+ if (change > 0) gains += change;
870
+ else losses -= change;
871
+ }
872
+ const avgGain = gains / period;
873
+ const avgLoss = losses / period;
874
+ if (avgLoss === 0) return 100;
875
+ const rs = avgGain / avgLoss;
876
+ return 100 - 100 / (1 + rs);
877
+ }
878
+ function calculateMACD(prices) {
879
+ const ema12 = calculateEMA(prices, 12);
880
+ const ema26 = calculateEMA(prices, 26);
881
+ const macdLine = ema12.map((v, i) => v - ema26[i]);
882
+ const signalLine = calculateEMA(macdLine.slice(-9), 9);
883
+ const macd = macdLine[macdLine.length - 1];
884
+ const signal = signalLine[signalLine.length - 1];
885
+ return { macd, signal, histogram: macd - signal };
886
+ }
546
887
  server.registerTool(
547
- "blockrun_balance",
888
+ "blockrun_signal",
548
889
  {
549
- description: `Check your on-chain USDC balance on Base network.
890
+ description: `Generate trading signals using RSI + MACD + EMA strategy.
891
+
892
+ Strategy (from freqtrade-strategies):
893
+ - BUY when: RSI < 40 (oversold) + MACD > Signal + Price > EMA200
894
+ - SELL when: RSI > 70 (overbought) or take profit/stop loss
550
895
 
551
- Returns:
552
- - Current USDC balance
553
- - Wallet address
554
- - Link to view on Basescan
896
+ Returns: BUY / SELL / HOLD signal with confidence level.
555
897
 
556
- Use this to see how much funding you have available for BlockRun API calls.`,
557
- inputSchema: {},
558
- outputSchema: {
559
- address: z.string(),
560
- balance: z.number().nullable(),
561
- network: z.string(),
562
- basescanUrl: z.string()
898
+ Example: blockrun_signal({ symbol: "BTCUSDT" })`,
899
+ inputSchema: {
900
+ symbol: z.string().describe("Trading pair (e.g., BTCUSDT, ETHUSDT, SOLUSDT)"),
901
+ timeframe: z.enum(["5m", "15m", "1h", "4h"]).optional().default("1h").describe("Candle timeframe")
563
902
  }
564
903
  },
565
- async () => {
566
- const llm = getClient();
567
- const address = llm.getWalletAddress();
568
- const balance = await getUsdcBalance(address);
569
- const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch (try again)";
570
- const text = `BlockRun Wallet Balance
571
- =======================
904
+ async ({ symbol, timeframe }) => {
905
+ try {
906
+ const url = `https://api.binance.com/api/v3/klines?symbol=${symbol.toUpperCase()}&interval=${timeframe}&limit=250`;
907
+ const response = await fetch(url);
908
+ if (!response.ok) {
909
+ throw new Error(`Binance API error: ${response.status}`);
910
+ }
911
+ const candles = await response.json();
912
+ const closes = candles.map((c) => parseFloat(c[4]));
913
+ const currentPrice = closes[closes.length - 1];
914
+ const rsi = calculateRSI(closes);
915
+ const { macd, signal, histogram } = calculateMACD(closes);
916
+ const ema200 = calculateEMA(closes, 200);
917
+ const currentEMA200 = ema200[ema200.length - 1];
918
+ const ema50 = calculateEMA(closes, 50);
919
+ const currentEMA50 = ema50[ema50.length - 1];
920
+ let signalType = "HOLD";
921
+ let confidence = 0;
922
+ let reasons = [];
923
+ const rsiOversold = rsi < 40;
924
+ const macdBullish = macd > signal;
925
+ const aboveEMA200 = currentPrice > currentEMA200;
926
+ const aboveEMA50 = currentPrice > currentEMA50;
927
+ const rsiOverbought = rsi > 70;
928
+ const macdBearish = macd < signal;
929
+ const belowEMA200 = currentPrice < currentEMA200;
930
+ if (rsiOversold && macdBullish && aboveEMA200) {
931
+ signalType = "BUY";
932
+ confidence = 80;
933
+ reasons.push("RSI oversold (<40)");
934
+ reasons.push("MACD bullish crossover");
935
+ reasons.push("Price above EMA200 (uptrend)");
936
+ if (aboveEMA50) {
937
+ confidence += 10;
938
+ reasons.push("Price above EMA50 (strong)");
939
+ }
940
+ } else if (rsiOverbought || macdBearish && belowEMA200) {
941
+ signalType = "SELL";
942
+ confidence = rsiOverbought ? 75 : 60;
943
+ if (rsiOverbought) reasons.push("RSI overbought (>70)");
944
+ if (macdBearish) reasons.push("MACD bearish");
945
+ if (belowEMA200) reasons.push("Price below EMA200 (downtrend)");
946
+ } else {
947
+ signalType = "HOLD";
948
+ confidence = 50;
949
+ reasons.push("No clear signal");
950
+ if (rsi < 50 && macdBullish) reasons.push("Slight bullish bias");
951
+ if (rsi > 50 && macdBearish) reasons.push("Slight bearish bias");
952
+ }
953
+ const stopLoss = signalType === "BUY" ? currentPrice * 0.9 : null;
954
+ const takeProfit = signalType === "BUY" ? currentPrice * 1.2 : null;
955
+ const result = `[Trading Signal: ${symbol}]
572
956
 
573
- Address: ${address}
574
- Balance: ${balanceStr}
575
- Network: Base (Chain ID: 8453)
957
+ Signal: ${signalType} (${confidence}% confidence)
958
+ Price: $${currentPrice.toFixed(2)}
576
959
 
577
- View on Basescan: https://basescan.org/address/${address}
960
+ Indicators:
961
+ - RSI (14): ${rsi.toFixed(1)} ${rsi < 30 ? "\u{1F7E2} Oversold" : rsi > 70 ? "\u{1F534} Overbought" : "\u26AA Neutral"}
962
+ - MACD: ${macd.toFixed(4)} | Signal: ${signal.toFixed(4)} | ${histogram > 0 ? "\u{1F7E2} Bullish" : "\u{1F534} Bearish"}
963
+ - EMA 50: $${currentEMA50.toFixed(2)} ${currentPrice > currentEMA50 ? "\u{1F7E2} Above" : "\u{1F534} Below"}
964
+ - EMA 200: $${currentEMA200.toFixed(2)} ${currentPrice > currentEMA200 ? "\u{1F7E2} Above" : "\u{1F534} Below"}
578
965
 
579
- ${balance !== null && balance < 1 ? "\u26A0\uFE0F Low balance. Consider adding funds to continue using BlockRun." : ""}`;
580
- return {
581
- content: [{ type: "text", text }],
582
- structuredContent: {
583
- address,
584
- balance,
585
- network: "Base",
586
- basescanUrl: `https://basescan.org/address/${address}`
587
- }
588
- };
966
+ Reasons:
967
+ ${reasons.map((r) => `\u2022 ${r}`).join("\n")}
968
+ ${signalType === "BUY" ? `
969
+ Suggested:
970
+ \u2022 Stop Loss: $${stopLoss?.toFixed(2)} (-10%)
971
+ \u2022 Take Profit: $${takeProfit?.toFixed(2)} (+20%)` : ""}
972
+
973
+ Strategy: RSI + MACD + EMA (freqtrade-strategies)
974
+ Timeframe: ${timeframe}`;
975
+ return {
976
+ content: [{ type: "text", text: result }],
977
+ structuredContent: {
978
+ symbol,
979
+ signal: signalType,
980
+ confidence,
981
+ price: currentPrice,
982
+ indicators: { rsi, macd, signal, ema50: currentEMA50, ema200: currentEMA200 },
983
+ stopLoss,
984
+ takeProfit,
985
+ reasons
986
+ }
987
+ };
988
+ } catch (error) {
989
+ const errorMessage = error instanceof Error ? error.message : String(error);
990
+ return {
991
+ content: [{ type: "text", text: `Signal error: ${errorMessage}` }],
992
+ isError: true
993
+ };
994
+ }
589
995
  }
590
996
  );
997
+ var ZERO_X_API = "https://api.0x.org/swap/v1";
998
+ var BASE_CHAIN_ID_NUM = 8453;
999
+ var BASE_TOKENS = {
1000
+ "ETH": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
1001
+ "WETH": "0x4200000000000000000000000000000000000006",
1002
+ "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
1003
+ "USDbC": "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
1004
+ "DAI": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
1005
+ "cbETH": "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22"
1006
+ };
591
1007
  server.registerTool(
592
- "blockrun_budget",
1008
+ "blockrun_swap",
593
1009
  {
594
- description: `Manage your session spending budget.
1010
+ description: `Execute token swaps on Base network using 0x aggregator.
595
1011
 
596
- Actions:
597
- - check: View current spending and budget status
598
- - set: Set a spending limit (e.g., $1.00)
599
- - clear: Remove spending limit (unlimited)
1012
+ \u26A0\uFE0F REAL MONEY - requires user confirmation before execution.
600
1013
 
601
- Use this to control how much you spend per session on BlockRun API calls.`,
1014
+ Example: blockrun_swap({ from: "USDC", to: "ETH", amount: 10 })`,
602
1015
  inputSchema: {
603
- action: z.enum(["check", "set", "clear"]).describe("Budget action to perform"),
604
- amount: z.number().optional().describe("Budget limit in USD (required for 'set' action)")
605
- },
606
- outputSchema: {
607
- limit: z.number().nullable(),
608
- spent: z.number(),
609
- calls: z.number(),
610
- remaining: z.number().nullable()
1016
+ from: z.string().describe("Token to sell (USDC, ETH, WETH, etc.)"),
1017
+ to: z.string().describe("Token to buy"),
1018
+ amount: z.number().describe("Amount in 'from' token"),
1019
+ slippage: z.number().optional().default(0.5).describe("Max slippage % (default 0.5)"),
1020
+ execute: z.boolean().optional().default(false).describe("Set true to execute (requires confirmation)")
611
1021
  }
612
1022
  },
613
- async ({ action, amount }) => {
614
- switch (action) {
615
- case "set":
616
- if (amount === void 0 || amount <= 0) {
617
- return {
618
- content: [{ type: "text", text: "Error: Please provide a positive amount for the budget limit (e.g., amount: 1.00 for $1.00)" }],
619
- isError: true
620
- };
621
- }
622
- sessionBudget.limit = amount;
623
- break;
624
- case "clear":
625
- sessionBudget.limit = null;
626
- break;
627
- case "check":
628
- default:
629
- break;
630
- }
631
- const remaining = sessionBudget.limit !== null ? sessionBudget.limit - sessionBudget.spent : null;
632
- const limitStr = sessionBudget.limit !== null ? `$${sessionBudget.limit.toFixed(2)}` : "Unlimited";
633
- const remainingStr = remaining !== null ? `$${remaining.toFixed(4)}` : "N/A";
634
- const text = `BlockRun Session Budget
635
- =======================
636
-
637
- Limit: ${limitStr}
638
- Spent: $${sessionBudget.spent.toFixed(4)}
639
- Calls: ${sessionBudget.calls}
640
- Remaining: ${remainingStr}
641
-
642
- ${action === "set" ? `\u2705 Budget set to $${amount?.toFixed(2)}` : ""}
643
- ${action === "clear" ? "\u2705 Budget limit removed (unlimited spending)" : ""}
644
- ${remaining !== null && remaining < 0.01 ? "\u26A0\uFE0F Budget nearly exhausted!" : ""}`;
645
- return {
646
- content: [{ type: "text", text }],
647
- structuredContent: {
648
- limit: sessionBudget.limit,
649
- spent: sessionBudget.spent,
650
- calls: sessionBudget.calls,
651
- remaining
1023
+ async ({ from, to, amount, slippage, execute }) => {
1024
+ const fromUpper = from.toUpperCase();
1025
+ const toUpper = to.toUpperCase();
1026
+ const fromToken = BASE_TOKENS[fromUpper];
1027
+ const toToken = BASE_TOKENS[toUpper];
1028
+ if (!fromToken) {
1029
+ return {
1030
+ content: [{ type: "text", text: `Unknown token: ${from}. Supported: ${Object.keys(BASE_TOKENS).join(", ")}` }],
1031
+ isError: true
1032
+ };
1033
+ }
1034
+ if (!toToken) {
1035
+ return {
1036
+ content: [{ type: "text", text: `Unknown token: ${to}. Supported: ${Object.keys(BASE_TOKENS).join(", ")}` }],
1037
+ isError: true
1038
+ };
1039
+ }
1040
+ const decimals = fromUpper === "USDC" || fromUpper === "USDbC" ? 6 : 18;
1041
+ const amountWei = BigInt(Math.floor(amount * 10 ** decimals));
1042
+ try {
1043
+ const quoteUrl = `${ZERO_X_API}/quote?` + new URLSearchParams({
1044
+ sellToken: fromToken,
1045
+ buyToken: toToken,
1046
+ sellAmount: amountWei.toString(),
1047
+ slippagePercentage: (slippage / 100).toString(),
1048
+ chainId: BASE_CHAIN_ID_NUM.toString()
1049
+ });
1050
+ const estimatedOutput = fromUpper === "USDC" ? amount / 3300 : amount * 3300;
1051
+ const quoteResult = `[Swap Quote: ${fromUpper} \u2192 ${toUpper}]
1052
+
1053
+ Sell: ${amount} ${fromUpper}
1054
+ Buy (est): ~${estimatedOutput.toFixed(6)} ${toUpper}
1055
+ Slippage: ${slippage}%
1056
+ Network: Base
1057
+
1058
+ ${execute ? "\u26A0\uFE0F EXECUTION REQUESTED" : "\u{1F4A1} Set execute: true to swap"}
1059
+
1060
+ Note: Full 0x integration requires API key.
1061
+ For demo, this shows the quote flow.
1062
+
1063
+ To execute:
1064
+ 1. User confirms the swap
1065
+ 2. Wallet signs transaction
1066
+ 3. Swap executes on-chain
1067
+ 4. Returns tx hash`;
1068
+ if (execute) {
1069
+ return {
1070
+ content: [{
1071
+ type: "text",
1072
+ text: `\u26A0\uFE0F SWAP EXECUTION DISABLED FOR SAFETY
1073
+
1074
+ To enable real swaps:
1075
+ 1. Add 0x API key
1076
+ 2. Implement transaction signing
1077
+ 3. Add confirmation flow
1078
+
1079
+ This is a demo. The swap would:
1080
+ \u2022 Sell ${amount} ${fromUpper}
1081
+ \u2022 Buy ~${estimatedOutput.toFixed(6)} ${toUpper}
1082
+ \u2022 Gas: ~$0.01 on Base`
1083
+ }]
1084
+ };
652
1085
  }
653
- };
1086
+ return {
1087
+ content: [{ type: "text", text: quoteResult }],
1088
+ structuredContent: {
1089
+ from: fromUpper,
1090
+ to: toUpper,
1091
+ sellAmount: amount,
1092
+ buyAmount: estimatedOutput,
1093
+ slippage,
1094
+ execute: false
1095
+ }
1096
+ };
1097
+ } catch (error) {
1098
+ const errorMessage = error instanceof Error ? error.message : String(error);
1099
+ return {
1100
+ content: [{ type: "text", text: `Swap error: ${errorMessage}` }],
1101
+ isError: true
1102
+ };
1103
+ }
654
1104
  }
655
1105
  );
656
1106
  server.registerResource(
657
1107
  "wallet",
658
1108
  "blockrun://wallet",
659
- {
660
- description: "Your BlockRun wallet address and status",
661
- mimeType: "application/json"
662
- },
663
- async () => {
664
- const info = getWalletInfo();
665
- return {
666
- contents: [{
667
- uri: "blockrun://wallet",
668
- mimeType: "application/json",
669
- text: JSON.stringify(info, null, 2)
670
- }]
671
- };
672
- }
1109
+ { description: "Wallet address and status", mimeType: "application/json" },
1110
+ async () => ({
1111
+ contents: [{
1112
+ uri: "blockrun://wallet",
1113
+ mimeType: "application/json",
1114
+ text: JSON.stringify(getWalletInfo(), null, 2)
1115
+ }]
1116
+ })
673
1117
  );
674
1118
  server.registerResource(
675
1119
  "models",
676
1120
  "blockrun://models",
677
- {
678
- description: "List of all available AI models with pricing",
679
- mimeType: "application/json"
680
- },
1121
+ { description: "Available AI models with pricing", mimeType: "application/json" },
681
1122
  async () => {
682
1123
  const llm = getClient();
683
1124
  if (!cachedModels) {
@@ -695,87 +1136,10 @@ server.registerResource(
695
1136
  };
696
1137
  }
697
1138
  );
698
- server.registerPrompt(
699
- "quick_chat",
700
- {
701
- description: "Start a quick chat with a recommended model",
702
- argsSchema: {
703
- message: z.string().describe("Your message"),
704
- style: z.enum(["concise", "detailed", "creative"]).optional().default("concise").describe("Response style")
705
- }
706
- },
707
- async ({ message, style }) => {
708
- const systemPrompts = {
709
- concise: "Be concise and direct. Give short, focused answers.",
710
- detailed: "Provide thorough, comprehensive answers with examples.",
711
- creative: "Be creative and imaginative in your responses."
712
- };
713
- return {
714
- messages: [
715
- {
716
- role: "user",
717
- content: {
718
- type: "text",
719
- text: `[System: ${systemPrompts[style || "concise"]}]
720
-
721
- ${message}`
722
- }
723
- }
724
- ]
725
- };
726
- }
727
- );
728
- server.registerPrompt(
729
- "code_review",
730
- {
731
- description: "Get a code review from a powerful model",
732
- argsSchema: {
733
- code: z.string().describe("The code to review"),
734
- language: z.string().optional().describe("Programming language"),
735
- focus: z.enum(["bugs", "performance", "style", "all"]).optional().default("all").describe("What to focus on")
736
- }
737
- },
738
- async ({ code, language, focus }) => {
739
- const focusInstructions = {
740
- bugs: "Focus on potential bugs, errors, and edge cases.",
741
- performance: "Focus on performance issues and optimization opportunities.",
742
- style: "Focus on code style, readability, and best practices.",
743
- all: "Review for bugs, performance, and style."
744
- };
745
- return {
746
- messages: [
747
- {
748
- role: "user",
749
- content: {
750
- type: "text",
751
- text: `Please review this ${language || ""} code. ${focusInstructions[focus || "all"]}
752
-
753
- \`\`\`${language || ""}
754
- ${code}
755
- \`\`\``
756
- }
757
- }
758
- ]
759
- };
760
- }
761
- );
762
- function formatError(message) {
763
- const isPaymentError = message.toLowerCase().includes("payment") || message.toLowerCase().includes("402") || message.toLowerCase().includes("balance") || message.toLowerCase().includes("insufficient");
764
- let errorText = `Error: ${message}`;
765
- if (isPaymentError) {
766
- errorText += `
767
-
768
- This error usually means your wallet needs funding.
769
- Run the blockrun_setup tool to get your wallet address and funding instructions.
770
-
771
- Quick fix: Send USDC to your wallet on Base network.`;
772
- }
773
- return errorText;
774
- }
775
1139
  async function main() {
776
1140
  const transport = new StdioServerTransport();
777
1141
  await server.connect(transport);
778
- console.error("BlockRun MCP Server started (v0.1.0)");
1142
+ console.error("BlockRun MCP Server started (v0.4.0)");
779
1143
  }
780
1144
  main().catch((error) => {
781
1145
  console.error("Fatal error:", error);