@blockrun/mcp 0.3.0 → 0.4.1

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 (2) hide show
  1. package/dist/index.js +292 -506
  2. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -4,13 +4,19 @@
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
21
  fast: ["google/gemini-2.5-flash", "openai/gpt-4o-mini", "deepseek/deepseek-chat"],
16
22
  balanced: ["openai/gpt-4o", "anthropic/claude-sonnet-4", "google/gemini-2.5-pro"],
@@ -21,9 +27,9 @@ var MODEL_TIERS = {
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,219 @@ 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 isPaymentError = message.toLowerCase().includes("payment") || message.toLowerCase().includes("402") || message.toLowerCase().includes("balance") || message.toLowerCase().includes("insufficient");
173
+ let errorText = `Error: ${message}`;
174
+ if (isPaymentError) {
175
+ errorText += `
176
+
177
+ This error usually means your wallet needs funding.
178
+ Run blockrun_wallet with action: "setup" to get funding instructions.
179
+
180
+ Quick fix: Send USDC to your wallet on Base network.`;
181
+ }
182
+ return errorText;
183
+ }
178
184
  var server = new McpServer({
179
185
  name: "blockrun-mcp",
180
- version: "0.3.0"
186
+ version: "0.4.0"
181
187
  });
182
188
  server.registerTool(
183
- "blockrun_chat",
189
+ "blockrun_wallet",
184
190
  {
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.
187
-
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
191
+ description: `Manage your BlockRun wallet - check status, get funding instructions, open QR code, or manage session budget.
194
192
 
195
- Use blockrun_models to see all available models with pricing.`,
193
+ Actions:
194
+ - status: Show wallet address, balance, and basescan link (default)
195
+ - setup: Full funding instructions with QR code
196
+ - qr: Generate and open QR code for easy funding
197
+ - budget: Manage session spending limit
198
+
199
+ Examples:
200
+ blockrun_wallet() -> status + balance
201
+ blockrun_wallet({ action: "setup" }) -> funding instructions + QR
202
+ blockrun_wallet({ action: "qr" }) -> open QR code
203
+ blockrun_wallet({ action: "budget", budget_action: "set", budget_amount: 1.00 })`,
196
204
  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")
205
+ action: z.enum(["status", "setup", "qr", "budget"]).optional().default("status").describe("What to do"),
206
+ budget_action: z.enum(["set", "check", "clear"]).optional().describe("Budget action (for action='budget')"),
207
+ budget_amount: z.number().optional().describe("Budget limit in USD (for budget_action='set')")
202
208
  }
203
209
  },
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);
210
+ async ({ action, budget_action, budget_amount }) => {
211
+ const info = getWalletInfo();
212
+ const address = info.address;
213
+ if (action === "budget") {
214
+ const budgetAct = budget_action || "check";
215
+ if (budgetAct === "set") {
216
+ if (budget_amount === void 0 || budget_amount <= 0) {
217
+ return {
218
+ content: [{ type: "text", text: "Error: Provide a positive budget_amount (e.g., 1.00 for $1.00)" }],
219
+ isError: true
220
+ };
221
+ }
222
+ sessionBudget.limit = budget_amount;
223
+ } else if (budgetAct === "clear") {
224
+ sessionBudget.limit = null;
225
+ }
226
+ const remaining = sessionBudget.limit !== null ? sessionBudget.limit - sessionBudget.spent : null;
227
+ const limitStr = sessionBudget.limit !== null ? `$${sessionBudget.limit.toFixed(2)}` : "Unlimited";
228
+ const remainingStr = remaining !== null ? `$${remaining.toFixed(4)}` : "N/A";
215
229
  return {
216
- content: [{ type: "text", text: formatError(errorMessage) }],
217
- isError: true
230
+ 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" : ""}` }],
231
+ structuredContent: {
232
+ limit: sessionBudget.limit,
233
+ spent: sessionBudget.spent,
234
+ calls: sessionBudget.calls,
235
+ remaining
236
+ }
218
237
  };
219
238
  }
239
+ if (action === "qr") {
240
+ try {
241
+ const qrPath = await generateQrPng(address);
242
+ await openQrInViewer(qrPath);
243
+ return {
244
+ content: [{ type: "text", text: `QR code opened! Scan with MetaMask to send USDC on Base.
245
+
246
+ Address: ${address}
247
+ QR saved: ${qrPath}` }]
248
+ };
249
+ } catch (err) {
250
+ return {
251
+ content: [{ type: "text", text: `Failed to generate QR: ${err}` }],
252
+ isError: true
253
+ };
254
+ }
255
+ }
256
+ if (action === "setup") {
257
+ let qrMessage = "";
258
+ try {
259
+ const qrPath = await generateQrPng(address);
260
+ await openQrInViewer(qrPath);
261
+ qrMessage = `
262
+ QR code opened for scanning! (${qrPath})`;
263
+ } catch {
264
+ qrMessage = "\n(QR generation failed - use address above)";
265
+ }
266
+ const text2 = `
267
+ ================================================================================
268
+ BLOCKRUN WALLET SETUP
269
+ ================================================================================
270
+
271
+ Your wallet address: ${address}
272
+ ${qrMessage}
273
+
274
+ HOW TO FUND YOUR WALLET:
275
+ ------------------------
276
+
277
+ Option 1: Transfer from Coinbase
278
+ 1. Open Coinbase app or website
279
+ 2. Go to Send/Receive -> Select USDC
280
+ 3. Choose "Base" network (important!)
281
+ 4. Paste: ${address}
282
+ 5. Send $1-5 to start
283
+
284
+ Option 2: Bridge from other chains
285
+ https://bridge.base.org -> Bridge USDC to Base -> Send to address above
286
+
287
+ Option 3: Buy directly
288
+ https://www.coinbase.com/onramp -> Buy USDC on Base -> Send to address above
289
+
290
+ VERIFY BALANCE: https://basescan.org/address/${address}
291
+
292
+ PRICING (pay per use):
293
+ - GPT-4o: ~$0.005/request | Claude Sonnet: ~$0.003/request
294
+ - Gemini Flash: ~$0.0001/request | Full pricing: https://blockrun.ai/pricing
295
+
296
+ SECURITY: Private key stored at ~/.blockrun/.session (never leaves your machine)
297
+ ================================================================================`;
298
+ return { content: [{ type: "text", text: text2 }] };
299
+ }
300
+ const balance = await getUsdcBalance(address);
301
+ const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch";
302
+ const lowBalance = balance !== null && balance < 1;
303
+ const text = `Wallet: ${address}
304
+ Balance: ${balanceStr}${lowBalance ? " (low - add funds)" : ""}
305
+ Network: Base | View: ${info.basescanUrl}
306
+ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions" : ""}`;
307
+ return {
308
+ content: [{ type: "text", text }],
309
+ structuredContent: {
310
+ address: info.address,
311
+ balance,
312
+ network: info.network,
313
+ chainId: info.chainId,
314
+ isNew: info.isNew,
315
+ basescanUrl: info.basescanUrl
316
+ }
317
+ };
220
318
  }
221
319
  );
222
320
  server.registerTool(
223
- "blockrun_smart",
321
+ "blockrun_chat",
224
322
  {
225
- description: `Smart model routing - automatically picks the best model based on your needs.
323
+ description: `Chat with AI models via BlockRun. Supports 30+ models with pay-per-request micropayments.
226
324
 
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)
325
+ Two ways to use:
326
+ 1. Specify a model directly: model: "openai/gpt-4o"
327
+ 2. Use smart routing: mode: "fast" | "balanced" | "powerful" | "cheap" | "reasoning"
233
328
 
234
- Example: blockrun_smart({ mode: "fast", message: "Hello" })`,
329
+ Popular models:
330
+ - openai/gpt-5.2, openai/gpt-4o, openai/gpt-4o-mini
331
+ - anthropic/claude-opus-4, anthropic/claude-sonnet-4
332
+ - google/gemini-2.5-pro, google/gemini-2.5-flash
333
+ - deepseek/deepseek-chat (very affordable)
334
+
335
+ Smart routing modes:
336
+ - fast: Gemini Flash, GPT-4o-mini (quickest)
337
+ - balanced: GPT-4o, Claude Sonnet (good default)
338
+ - powerful: GPT-5.2, Claude Opus 4 (best quality)
339
+ - cheap: DeepSeek, Gemini Flash (lowest cost)
340
+ - reasoning: o3, o1 (complex logic)
341
+
342
+ Use blockrun_models to see all available models with pricing.`,
235
343
  inputSchema: {
236
- mode: z.enum(["fast", "balanced", "powerful", "cheap", "reasoning"]).describe("Routing mode"),
237
344
  message: z.string().describe("Your message to the AI"),
345
+ model: z.string().optional().describe("Specific model ID (e.g., 'openai/gpt-4o')"),
346
+ mode: z.enum(["fast", "balanced", "powerful", "cheap", "reasoning"]).optional().describe("Smart routing mode (ignored if model specified)"),
238
347
  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")
348
+ max_tokens: z.number().optional().default(1024).describe("Max tokens in response"),
349
+ temperature: z.number().optional().default(1).describe("Creativity 0-2")
244
350
  }
245
351
  },
246
- async ({ mode, message, system, max_tokens }) => {
247
- const models = MODEL_TIERS[mode];
248
- let lastError = null;
249
- for (const model of models) {
352
+ async ({ message, model, mode, system, max_tokens, temperature }) => {
353
+ const llm = getClient();
354
+ if (model) {
250
355
  try {
251
- const llm = getClient();
252
356
  const response = await llm.chat(model, message, {
357
+ system,
358
+ maxTokens: max_tokens,
359
+ temperature
360
+ });
361
+ return { content: [{ type: "text", text: response }] };
362
+ } catch (error) {
363
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
364
+ return {
365
+ content: [{ type: "text", text: formatError(errorMessage2) }],
366
+ isError: true
367
+ };
368
+ }
369
+ }
370
+ const routingMode = mode || "balanced";
371
+ const models = MODEL_TIERS[routingMode];
372
+ let lastError = null;
373
+ for (const m of models) {
374
+ try {
375
+ const response = await llm.chat(m, message, {
253
376
  system,
254
377
  maxTokens: max_tokens
255
378
  });
256
- const result = { model_used: model, response };
257
379
  return {
258
- content: [{ type: "text", text: `[Used: ${model}]
380
+ content: [{ type: "text", text: `[${m}]
259
381
 
260
382
  ${response}` }],
261
- structuredContent: result
383
+ structuredContent: { model_used: m, response }
262
384
  };
263
385
  } catch (error) {
264
386
  lastError = error;
@@ -275,19 +397,10 @@ ${response}` }],
275
397
  server.registerTool(
276
398
  "blockrun_models",
277
399
  {
278
- description: "List all available AI models with pricing. Use this to discover models and compare costs.",
400
+ description: "List available AI models with pricing. Use to discover models and compare costs.",
279
401
  inputSchema: {
280
402
  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")
403
+ provider: z.string().optional().describe("Filter by provider (e.g., 'openai', 'anthropic')")
291
404
  }
292
405
  },
293
406
  async ({ category, provider }) => {
@@ -305,13 +418,9 @@ server.registerTool(
305
418
  }
306
419
  if (category && category !== "all") {
307
420
  if (category === "image") {
308
- models = models.filter(
309
- (m) => m.id.includes("dall-e") || m.id.includes("flux") || m.id.includes("banana")
310
- );
421
+ models = models.filter((m) => m.id.includes("dall-e") || m.id.includes("flux") || m.id.includes("banana"));
311
422
  } else if (category === "reasoning") {
312
- models = models.filter(
313
- (m) => m.id.includes("/o1") || m.id.includes("/o3") || m.id.includes("reasoner")
314
- );
423
+ models = models.filter((m) => m.id.includes("/o1") || m.id.includes("/o3") || m.id.includes("reasoner"));
315
424
  } else if (category === "embedding") {
316
425
  models = models.filter((m) => m.id.includes("embed"));
317
426
  }
@@ -320,219 +429,92 @@ server.registerTool(
320
429
  const input = m.inputPrice ? `$${m.inputPrice}/M in` : "";
321
430
  const output = m.outputPrice ? `$${m.outputPrice}/M out` : "";
322
431
  const pricing = [input, output].filter(Boolean).join(", ");
323
- return `- ${m.id}: ${m.name || ""} ${pricing ? `(${pricing})` : ""}`;
432
+ return `- ${m.id}${pricing ? ` (${pricing})` : ""}`;
324
433
  });
325
- const structuredModels = models.map((m) => ({
326
- id: m.id,
327
- name: m.name,
328
- inputPrice: m.inputPrice,
329
- outputPrice: m.outputPrice
330
- }));
331
434
  return {
332
- content: [{ type: "text", text: `Available models (${models.length}):
333
-
435
+ content: [{ type: "text", text: `Models (${models.length}):
334
436
  ${lines.join("\n")}` }],
335
- structuredContent: { count: models.length, models: structuredModels }
437
+ structuredContent: { count: models.length, models }
336
438
  };
337
439
  }
338
440
  );
339
441
  server.registerTool(
340
442
  "blockrun_image",
341
443
  {
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`,
444
+ description: `Generate images. Models: openai/dall-e-3 ($0.04-0.08), together/flux-schnell ($0.02), google/nano-banana`,
348
445
  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")
446
+ prompt: z.string().describe("Image description"),
447
+ model: z.enum(["openai/dall-e-3", "together/flux-schnell", "google/nano-banana"]).optional().default("openai/dall-e-3"),
448
+ size: z.enum(["1024x1024", "1792x1024", "1024x1792"]).optional().default("1024x1024"),
449
+ quality: z.enum(["standard", "hd"]).optional().default("standard")
358
450
  }
359
451
  },
360
452
  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) {
375
- 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.` }],
379
- isError: true
380
- };
381
- }
382
- if (!response.ok) {
453
+ try {
454
+ const imgClient = getImageClient();
455
+ const response = await imgClient.generate(prompt, {
456
+ model,
457
+ size,
458
+ quality
459
+ });
460
+ const imageUrl = response.data?.[0]?.url;
461
+ if (!imageUrl) {
462
+ return {
463
+ content: [{ type: "text", text: formatError("No image URL in response") }],
464
+ isError: true
465
+ };
466
+ }
383
467
  return {
384
- content: [{ type: "text", text: formatError(`Image generation failed: ${response.status}`) }],
385
- isError: true
468
+ content: [{ type: "text", text: `Image: ${imageUrl}
469
+ Prompt: ${prompt}
470
+ Model: ${model}` }],
471
+ structuredContent: { url: imageUrl, prompt, model }
386
472
  };
387
- }
388
- const data = await response.json();
389
- const imageUrl = data.data?.[0]?.url;
390
- if (!imageUrl) {
473
+ } catch (err) {
474
+ const errMsg = err instanceof Error ? err.message : String(err);
475
+ if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402")) {
476
+ return {
477
+ content: [{ type: "text", text: `Image generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
478
+ Error: ${errMsg}` }],
479
+ isError: true
480
+ };
481
+ }
391
482
  return {
392
- content: [{ type: "text", text: formatError("No image URL in response") }],
483
+ content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}`) }],
393
484
  isError: true
394
485
  };
395
486
  }
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
- }
406
- );
407
- server.registerTool(
408
- "blockrun_wallet",
409
- {
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
- ============================
426
-
427
- Address: ${info.address}
428
- Network: ${info.network} (Chain ID: ${info.chainId})
429
- Currency: ${info.currency}
430
-
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
- `;
450
- }
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
460
- }
461
- };
462
- }
463
- );
464
- server.registerTool(
465
- "blockrun_setup",
466
- {
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: {}
475
- },
476
- async () => {
477
- getClient();
478
- return { content: [{ type: "text", text: getWalletSetupInstructions() }] };
479
487
  }
480
488
  );
481
489
  server.registerTool(
482
490
  "blockrun_twitter",
483
491
  {
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]"`,
492
+ description: `Search real-time X/Twitter via Grok. Use for trending topics, @handles, breaking news.`,
496
493
  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()
494
+ query: z.string().describe("Search query (can include @handles, topics)"),
495
+ max_results: z.number().optional().default(10).describe("Max results (1-25)")
504
496
  }
505
497
  },
506
498
  async ({ query, max_results }) => {
507
499
  const budget = checkBudget();
508
500
  if (!budget.allowed) {
509
501
  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.` }],
502
+ content: [{ type: "text", text: `Budget limit reached ($${sessionBudget.spent.toFixed(4)} of $${sessionBudget.limit?.toFixed(2)}). Use blockrun_wallet with action: "budget" to adjust.` }],
513
503
  isError: true
514
504
  };
515
505
  }
516
506
  try {
517
507
  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,
508
+ const response = await llm.chat("xai/grok-3", query, {
509
+ system: `Real-time X/Twitter search. Focus on recent posts, key accounts, engagement. Max results: ${max_results}`,
528
510
  search: true
529
511
  });
530
512
  recordSpending(2e-3);
531
513
  return {
532
- content: [{ type: "text", text: `[X/Twitter Search via Grok]
514
+ content: [{ type: "text", text: `[X/Twitter via Grok]
533
515
 
534
516
  ${response}` }],
535
- structuredContent: { query, model, response }
517
+ structuredContent: { query, model: "xai/grok-3", response }
536
518
  };
537
519
  } catch (error) {
538
520
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -543,141 +525,22 @@ ${response}` }],
543
525
  }
544
526
  }
545
527
  );
546
- server.registerTool(
547
- "blockrun_balance",
548
- {
549
- description: `Check your on-chain USDC balance on Base network.
550
-
551
- Returns:
552
- - Current USDC balance
553
- - Wallet address
554
- - Link to view on Basescan
555
-
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()
563
- }
564
- },
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
- =======================
572
-
573
- Address: ${address}
574
- Balance: ${balanceStr}
575
- Network: Base (Chain ID: 8453)
576
-
577
- View on Basescan: https://basescan.org/address/${address}
578
-
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
- };
589
- }
590
- );
591
- server.registerTool(
592
- "blockrun_budget",
593
- {
594
- description: `Manage your session spending budget.
595
-
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)
600
-
601
- Use this to control how much you spend per session on BlockRun API calls.`,
602
- 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()
611
- }
612
- },
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
652
- }
653
- };
654
- }
655
- );
656
528
  server.registerResource(
657
529
  "wallet",
658
530
  "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
- }
531
+ { description: "Wallet address and status", mimeType: "application/json" },
532
+ async () => ({
533
+ contents: [{
534
+ uri: "blockrun://wallet",
535
+ mimeType: "application/json",
536
+ text: JSON.stringify(getWalletInfo(), null, 2)
537
+ }]
538
+ })
673
539
  );
674
540
  server.registerResource(
675
541
  "models",
676
542
  "blockrun://models",
677
- {
678
- description: "List of all available AI models with pricing",
679
- mimeType: "application/json"
680
- },
543
+ { description: "Available AI models with pricing", mimeType: "application/json" },
681
544
  async () => {
682
545
  const llm = getClient();
683
546
  if (!cachedModels) {
@@ -695,87 +558,10 @@ server.registerResource(
695
558
  };
696
559
  }
697
560
  );
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
561
  async function main() {
776
562
  const transport = new StdioServerTransport();
777
563
  await server.connect(transport);
778
- console.error("BlockRun MCP Server started (v0.1.0)");
564
+ console.error("BlockRun MCP Server started (v0.4.0)");
779
565
  }
780
566
  main().catch((error) => {
781
567
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "mcpName": "io.github.BlockRunAI/blockrun-mcp",
5
5
  "description": "BlockRun MCP Server - Access 30+ AI models via x402 micropayments. No API keys needed.",
6
6
  "type": "module",
@@ -44,13 +44,17 @@
44
44
  "url": "https://github.com/blockrunai/blockrun-mcp/issues"
45
45
  },
46
46
  "dependencies": {
47
- "@blockrun/llm": "^0.1.1",
47
+ "@blockrun/llm": "^0.2.0",
48
48
  "@modelcontextprotocol/sdk": "^1.0.0",
49
+ "jimp": "^1.6.0",
50
+ "open": "^11.0.0",
51
+ "qrcode": "^1.5.4",
49
52
  "viem": "^2.21.0",
50
53
  "zod": "^4.3.5"
51
54
  },
52
55
  "devDependencies": {
53
56
  "@types/node": "^20.0.0",
57
+ "@types/qrcode": "^1.5.6",
54
58
  "tsup": "^8.0.0",
55
59
  "tsx": "^4.0.0",
56
60
  "typescript": "^5.0.0"