@blockrun/mcp 0.4.2 → 0.5.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.
@@ -0,0 +1,961 @@
1
+ // src/utils/wallet.ts
2
+ import { LLMClient, ImageClient } from "@blockrun/llm";
3
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
4
+ import * as fs from "fs";
5
+
6
+ // src/utils/constants.ts
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ var WALLET_DIR = path.join(os.homedir(), ".blockrun");
10
+ var WALLET_FILE = path.join(WALLET_DIR, ".session");
11
+ var QR_FILE = path.join(WALLET_DIR, "qr.png");
12
+ var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
13
+ var BASE_CHAIN_ID = "8453";
14
+ var BASE_RPC_URLS = [
15
+ "https://mainnet.base.org",
16
+ "https://base.llamarpc.com",
17
+ "https://1rpc.io/base"
18
+ ];
19
+ var MODEL_TIERS = {
20
+ fast: ["google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
21
+ balanced: ["openai/gpt-5.4", "anthropic/claude-sonnet-4.6", "google/gemini-2.5-pro", "openai/gpt-5.3", "google/gemini-3.1-pro"],
22
+ powerful: ["openai/gpt-5.4-pro", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.5", "openai/o3", "openai/gpt-5.4"],
23
+ cheap: ["nvidia/gpt-oss-120b", "nvidia/deepseek-v3.2", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
24
+ reasoning: ["openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "openai/gpt-5.3-codex"],
25
+ free: ["nvidia/gpt-oss-120b", "nvidia/deepseek-v3.2", "nvidia/nemotron-ultra-253b", "nvidia/nemotron-super-49b", "nvidia/qwen3-coder-480b", "nvidia/llama-4-maverick", "nvidia/gpt-oss-20b"],
26
+ coding: ["openai/gpt-5.3-codex", "nvidia/qwen3-coder-480b", "nvidia/devstral-2-123b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"]
27
+ };
28
+
29
+ // src/utils/wallet.ts
30
+ var _walletWasCreated = false;
31
+ var _walletAddress = null;
32
+ var _client = null;
33
+ var _imageClient = null;
34
+ function getOrCreateWalletKey() {
35
+ const envKey = process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY;
36
+ if (envKey) {
37
+ const account2 = privateKeyToAccount(envKey);
38
+ _walletAddress = account2.address;
39
+ return envKey;
40
+ }
41
+ if (fs.existsSync(WALLET_FILE)) {
42
+ try {
43
+ const savedKey = fs.readFileSync(WALLET_FILE, "utf-8").trim();
44
+ if (savedKey.startsWith("0x") && savedKey.length === 66) {
45
+ const account2 = privateKeyToAccount(savedKey);
46
+ _walletAddress = account2.address;
47
+ return savedKey;
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ const newKey = generatePrivateKey();
53
+ const account = privateKeyToAccount(newKey);
54
+ _walletAddress = account.address;
55
+ _walletWasCreated = true;
56
+ try {
57
+ if (!fs.existsSync(WALLET_DIR)) {
58
+ fs.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
59
+ }
60
+ fs.writeFileSync(WALLET_FILE, newKey, { mode: 384 });
61
+ console.error(`[BlockRun] New wallet created and saved to ${WALLET_FILE}`);
62
+ } catch (err) {
63
+ console.error(`[BlockRun] Warning: Could not save wallet to file: ${err}`);
64
+ }
65
+ return newKey;
66
+ }
67
+ function getClient() {
68
+ if (!_client) {
69
+ const privateKey = getOrCreateWalletKey();
70
+ _client = new LLMClient({ privateKey });
71
+ }
72
+ return _client;
73
+ }
74
+ function getImageClient() {
75
+ if (!_imageClient) {
76
+ const privateKey = getOrCreateWalletKey();
77
+ _imageClient = new ImageClient({ privateKey });
78
+ }
79
+ return _imageClient;
80
+ }
81
+ function getWalletInfo() {
82
+ const llm = getClient();
83
+ const address = llm.getWalletAddress();
84
+ return {
85
+ address,
86
+ network: "Base",
87
+ chainId: 8453,
88
+ currency: "USDC",
89
+ isNew: _walletWasCreated,
90
+ basescanUrl: `https://basescan.org/address/${address}`
91
+ };
92
+ }
93
+ async function getUsdcBalance(address) {
94
+ const data = {
95
+ jsonrpc: "2.0",
96
+ method: "eth_call",
97
+ params: [
98
+ {
99
+ to: USDC_ADDRESS,
100
+ data: `0x70a08231000000000000000000000000${address.slice(2)}`
101
+ },
102
+ "latest"
103
+ ],
104
+ id: 1
105
+ };
106
+ for (const rpcUrl of BASE_RPC_URLS) {
107
+ try {
108
+ const response = await fetch(rpcUrl, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify(data)
112
+ });
113
+ const result = await response.json();
114
+ if (result.result) {
115
+ return parseInt(result.result, 16) / 1e6;
116
+ }
117
+ } catch {
118
+ continue;
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+
124
+ // src/tools/wallet.ts
125
+ import { z } from "zod";
126
+
127
+ // src/utils/qr.ts
128
+ import QRCode from "qrcode";
129
+ import { Jimp } from "jimp";
130
+ import open from "open";
131
+ import * as fs2 from "fs";
132
+ function getEip681Uri(address, amountUsdc = 1) {
133
+ const amountWei = Math.floor(amountUsdc * 1e6);
134
+ return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
135
+ }
136
+ async function generateQrPng(address) {
137
+ const eip681Uri = getEip681Uri(address);
138
+ const qrBuffer = await QRCode.toBuffer(eip681Uri, {
139
+ type: "png",
140
+ width: 400,
141
+ margin: 2,
142
+ errorCorrectionLevel: "H",
143
+ color: { dark: "#000000", light: "#FFFFFF" }
144
+ });
145
+ const qrImage = await Jimp.read(qrBuffer);
146
+ try {
147
+ const logoUrl = "https://avatars.githubusercontent.com/u/108554348?s=200&v=4";
148
+ const logo = await Jimp.read(logoUrl);
149
+ const logoSize = Math.floor(qrImage.width * 0.2);
150
+ logo.resize({ w: logoSize, h: logoSize });
151
+ const x = Math.floor((qrImage.width - logoSize) / 2);
152
+ const y = Math.floor((qrImage.height - logoSize) / 2);
153
+ qrImage.composite(logo, x, y);
154
+ } catch {
155
+ }
156
+ if (!fs2.existsSync(WALLET_DIR)) {
157
+ fs2.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
158
+ }
159
+ await qrImage.write(QR_FILE);
160
+ return QR_FILE;
161
+ }
162
+ async function openQrInViewer(qrPath) {
163
+ try {
164
+ await open(qrPath);
165
+ } catch {
166
+ }
167
+ }
168
+
169
+ // src/utils/errors.ts
170
+ function formatError(message) {
171
+ const msgLower = message.toLowerCase();
172
+ const isPaymentError = msgLower.includes("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !msgLower.includes("500");
173
+ const isServerError = msgLower.includes("500") || msgLower.includes("api error after payment");
174
+ let errorText = `Error: ${message}`;
175
+ if (isServerError) {
176
+ errorText += `
177
+
178
+ This is a temporary API issue. The xAI/Grok API may be experiencing problems.
179
+ Try again in a few minutes, or use a different model (e.g., openai/gpt-4o).`;
180
+ } else if (isPaymentError) {
181
+ errorText += `
182
+
183
+ This error usually means your wallet needs funding.
184
+ Run blockrun_wallet with action: "setup" to get funding instructions.
185
+
186
+ Quick fix: Send USDC to your wallet on Base network.`;
187
+ }
188
+ return errorText;
189
+ }
190
+
191
+ // src/tools/wallet.ts
192
+ function registerWalletTool(server, budget) {
193
+ server.registerTool(
194
+ "blockrun_wallet",
195
+ {
196
+ description: `Call this tool to manage the BlockRun payment wallet and control agent spending budgets.
197
+
198
+ Call this FIRST if any other blockrun_* tool returns a payment/balance error.
199
+ Call this to check your current USDC balance before expensive operations.
200
+ Call this to set spending limits before spawning child agents.
201
+
202
+ Actions:
203
+ - status (default): Current wallet address, USDC balance, total session spending
204
+ - setup: Get funding instructions + QR code (call this when balance is 0)
205
+ - qr: Open QR code in system viewer
206
+
207
+ Budget controls:
208
+ - budget + budget_action:"set" + budget_amount:1.00 \u2192 Set global spend cap
209
+ - budget + budget_action:"clear" \u2192 Remove global spend cap
210
+
211
+ Multi-agent orchestration:
212
+ - delegate + agent_id:"research" + agent_limit:2.00 \u2192 Allocate $2 to a child agent
213
+ - revoke + agent_id:"research" \u2192 Remove a child agent's budget
214
+ - report \u2192 See per-agent spending breakdown
215
+
216
+ Usage pattern for multi-agent systems:
217
+ 1. blockrun_wallet action:"delegate" agent_id:"worker-1" agent_limit:1.00
218
+ 2. Pass agent_id:"worker-1" to all blockrun_chat/search/etc calls for that agent
219
+ 3. blockrun_wallet action:"report" to audit spending
220
+
221
+ Do NOT call this for actual AI queries \u2014 use blockrun_chat for that.`,
222
+ inputSchema: {
223
+ action: z.enum(["status", "setup", "qr", "budget", "delegate", "revoke", "report"]).optional().default("status").describe("What to do"),
224
+ budget_action: z.enum(["set", "check", "clear"]).optional().describe("Budget action (for action='budget')"),
225
+ budget_amount: z.number().optional().describe("Budget limit in USD (for budget_action='set')"),
226
+ agent_id: z.string().optional().describe("Agent identifier for delegate/revoke/report actions"),
227
+ agent_limit: z.number().optional().describe("Budget limit in USD for this agent (required for delegate action)")
228
+ }
229
+ },
230
+ async ({ action, budget_action, budget_amount, agent_id, agent_limit }) => {
231
+ const info = getWalletInfo();
232
+ const address = info.address;
233
+ if (action === "budget") {
234
+ const budgetAct = budget_action || "check";
235
+ if (budgetAct === "set") {
236
+ if (budget_amount === void 0 || budget_amount <= 0) {
237
+ return {
238
+ content: [{ type: "text", text: "Error: Provide a positive budget_amount (e.g., 1.00 for $1.00)" }],
239
+ isError: true
240
+ };
241
+ }
242
+ budget.limit = budget_amount;
243
+ } else if (budgetAct === "clear") {
244
+ budget.limit = null;
245
+ }
246
+ const remaining = budget.limit !== null ? budget.limit - budget.spent : null;
247
+ const limitStr = budget.limit !== null ? `$${budget.limit.toFixed(2)}` : "Unlimited";
248
+ const remainingStr = remaining !== null ? `$${remaining.toFixed(4)}` : "N/A";
249
+ return {
250
+ content: [{ type: "text", text: `Session Budget: ${limitStr} | Spent: $${budget.spent.toFixed(4)} | Calls: ${budget.calls} | Remaining: ${remainingStr}${budgetAct === "set" ? ` | Set to $${budget_amount?.toFixed(2)}` : ""}${budgetAct === "clear" ? " | Limit removed" : ""}` }],
251
+ structuredContent: {
252
+ limit: budget.limit,
253
+ spent: budget.spent,
254
+ calls: budget.calls,
255
+ remaining
256
+ }
257
+ };
258
+ }
259
+ if (action === "delegate") {
260
+ if (!agent_id) {
261
+ return { content: [{ type: "text", text: formatError("agent_id required for delegate action") }], isError: true };
262
+ }
263
+ if (!agent_limit || agent_limit <= 0) {
264
+ return { content: [{ type: "text", text: formatError("agent_limit (USD > 0) required for delegate action") }], isError: true };
265
+ }
266
+ budget.agents.set(agent_id, { limit: agent_limit, spent: 0, calls: 0 });
267
+ return {
268
+ content: [{ type: "text", text: `Agent "${agent_id}" allocated $${agent_limit.toFixed(2)} budget.
269
+ Pass agent_id: "${agent_id}" in any blockrun_* tool call to track and enforce this limit.` }],
270
+ structuredContent: { agent_id, limit: agent_limit, spent: 0, calls: 0 }
271
+ };
272
+ }
273
+ if (action === "revoke") {
274
+ if (!agent_id) {
275
+ return { content: [{ type: "text", text: formatError("agent_id required for revoke action") }], isError: true };
276
+ }
277
+ const existed = budget.agents.has(agent_id);
278
+ budget.agents.delete(agent_id);
279
+ return {
280
+ content: [{ type: "text", text: existed ? `Agent "${agent_id}" budget revoked.` : `Agent "${agent_id}" had no budget entry.` }],
281
+ structuredContent: { agent_id, revoked: existed }
282
+ };
283
+ }
284
+ if (action === "report") {
285
+ const agentRows = {};
286
+ for (const [id, ab] of budget.agents.entries()) {
287
+ agentRows[id] = {
288
+ limit: ab.limit,
289
+ spent: ab.spent,
290
+ calls: ab.calls,
291
+ remaining: Math.max(0, ab.limit - ab.spent)
292
+ };
293
+ }
294
+ const agentLines = Object.entries(agentRows).map(
295
+ ([id, ab]) => ` ${id}: $${ab.spent.toFixed(4)}/$${ab.limit.toFixed(2)} (${ab.calls} calls, $${ab.remaining.toFixed(4)} remaining)`
296
+ );
297
+ const lines = [
298
+ `Global: $${budget.spent.toFixed(4)} spent${budget.limit ? ` / $${budget.limit.toFixed(2)} limit` : " (no limit)"} \u2014 ${budget.calls} calls`,
299
+ ``,
300
+ `Per-agent budgets (${budget.agents.size} active):`,
301
+ ...agentLines.length > 0 ? agentLines : [" (none delegated)"]
302
+ ];
303
+ return {
304
+ content: [{ type: "text", text: lines.join("\n") }],
305
+ structuredContent: { global: { limit: budget.limit, spent: budget.spent, calls: budget.calls }, agents: agentRows }
306
+ };
307
+ }
308
+ if (action === "qr") {
309
+ try {
310
+ const qrPath = await generateQrPng(address);
311
+ await openQrInViewer(qrPath);
312
+ return {
313
+ content: [{ type: "text", text: `QR code opened! Scan with MetaMask to send USDC on Base.
314
+
315
+ Address: ${address}
316
+ QR saved: ${qrPath}` }]
317
+ };
318
+ } catch (err) {
319
+ return {
320
+ content: [{ type: "text", text: `Failed to generate QR: ${err}` }],
321
+ isError: true
322
+ };
323
+ }
324
+ }
325
+ if (action === "setup") {
326
+ let qrMessage = "";
327
+ try {
328
+ const qrPath = await generateQrPng(address);
329
+ await openQrInViewer(qrPath);
330
+ qrMessage = `
331
+ QR code opened for scanning! (${qrPath})`;
332
+ } catch {
333
+ qrMessage = "\n(QR generation failed - use address above)";
334
+ }
335
+ const text2 = `
336
+ ================================================================================
337
+ BLOCKRUN WALLET SETUP
338
+ ================================================================================
339
+
340
+ Your wallet address: ${address}
341
+ ${qrMessage}
342
+
343
+ HOW TO FUND YOUR WALLET:
344
+ ------------------------
345
+
346
+ Option 1: Transfer from Coinbase
347
+ 1. Open Coinbase app or website
348
+ 2. Go to Send/Receive -> Select USDC
349
+ 3. Choose "Base" network (important!)
350
+ 4. Paste: ${address}
351
+ 5. Send $1-5 to start
352
+
353
+ Option 2: Bridge from other chains
354
+ https://bridge.base.org -> Bridge USDC to Base -> Send to address above
355
+
356
+ Option 3: Buy directly
357
+ https://www.coinbase.com/onramp -> Buy USDC on Base -> Send to address above
358
+
359
+ VERIFY BALANCE: https://basescan.org/address/${address}
360
+
361
+ PRICING (pay per use):
362
+ - GPT-4o: ~$0.005/request | Claude Sonnet: ~$0.003/request
363
+ - Gemini Flash: ~$0.0001/request | Full pricing: https://blockrun.ai/pricing
364
+
365
+ SECURITY: Private key stored at ~/.blockrun/.session (never leaves your machine)
366
+ ================================================================================`;
367
+ return { content: [{ type: "text", text: text2 }] };
368
+ }
369
+ const balance = await getUsdcBalance(address);
370
+ const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch";
371
+ const lowBalance = balance !== null && balance < 1;
372
+ const text = `Wallet: ${address}
373
+ Balance: ${balanceStr}${lowBalance ? " (low - add funds)" : ""}
374
+ Network: Base | View: ${info.basescanUrl}
375
+ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions" : ""}`;
376
+ return {
377
+ content: [{ type: "text", text }],
378
+ structuredContent: {
379
+ address: info.address,
380
+ balance,
381
+ network: info.network,
382
+ chainId: info.chainId,
383
+ isNew: info.isNew,
384
+ basescanUrl: info.basescanUrl
385
+ }
386
+ };
387
+ }
388
+ );
389
+ }
390
+
391
+ // src/tools/chat.ts
392
+ import { z as z2 } from "zod";
393
+
394
+ // src/utils/budget.ts
395
+ function checkBudget(budget, agentId) {
396
+ if (budget.limit !== null && budget.spent >= budget.limit) {
397
+ return {
398
+ allowed: false,
399
+ reason: `Global budget limit $${budget.limit.toFixed(2)} reached ($${budget.spent.toFixed(4)} spent)`
400
+ };
401
+ }
402
+ if (agentId) {
403
+ const agentBudget = budget.agents.get(agentId);
404
+ if (agentBudget && agentBudget.spent >= agentBudget.limit) {
405
+ return {
406
+ allowed: false,
407
+ reason: `Agent "${agentId}" budget $${agentBudget.limit.toFixed(2)} exhausted ($${agentBudget.spent.toFixed(4)} spent in ${agentBudget.calls} calls)`
408
+ };
409
+ }
410
+ }
411
+ return { allowed: true };
412
+ }
413
+ function recordSpending(budget, cost, agentId) {
414
+ budget.spent += cost;
415
+ budget.calls += 1;
416
+ if (agentId) {
417
+ const agentBudget = budget.agents.get(agentId);
418
+ if (agentBudget) {
419
+ agentBudget.spent += cost;
420
+ agentBudget.calls += 1;
421
+ }
422
+ }
423
+ }
424
+
425
+ // src/tools/chat.ts
426
+ function registerChatTool(server, budget) {
427
+ server.registerTool(
428
+ "blockrun_chat",
429
+ {
430
+ description: `Chat with 41 AI models from 7 providers via BlockRun micropayments. No API keys needed.
431
+ Recommended for agents: use routing: "smart" to auto-select the optimal model via ClawRouter.
432
+ routing_profile: "free" (zero cost NVIDIA), "eco" (budget), "auto" (balanced, default), "premium" (best quality)
433
+
434
+ Three ways to use:
435
+ 1. ClawRouter (recommended): routing: "smart" \u2014 auto-selects optimal model via 14-dimension AI routing
436
+ 2. Direct model: model: "openai/gpt-5.4"
437
+ 3. Smart routing: mode: "fast"|"balanced"|"powerful"|"cheap"|"reasoning"|"free"|"coding"
438
+ 4. Multi-turn: pass messages[] array (conversation history) \u2014 "message" is appended as the final user turn
439
+
440
+ All providers (format: provider/model-id):
441
+ \u2022 OpenAI (13): gpt-5.4, gpt-5.4-pro, gpt-5.3, gpt-5.2, gpt-5.4-mini, gpt-5-mini, gpt-5.4-nano, gpt-5.2-pro, gpt-5.3-codex, o1, o1-mini, o3, o3-mini
442
+ \u2022 Anthropic (4): claude-haiku-4.5, claude-sonnet-4.6, claude-opus-4.5, claude-opus-4.6
443
+ \u2022 Google (7): gemini-3.1-pro, gemini-3-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-flash-lite, gemini-2.5-flash-lite
444
+ \u2022 DeepSeek (2): deepseek-chat, deepseek-reasoner
445
+ \u2022 NVIDIA (12, most FREE*): gpt-oss-120b*, gpt-oss-20b*, kimi-k2.5, nemotron-ultra-253b*, nemotron-3-super-120b*, nemotron-super-49b*, deepseek-v3.2*, mistral-large-3-675b*, qwen3-coder-480b*, devstral-2-123b*, glm-4.7*, llama-4-maverick*
446
+ \u2022 ZAI (2): glm-5, glm-5-turbo
447
+ \u2022 MiniMax (1): minimax-m2.7
448
+
449
+ Smart routing modes:
450
+ - fast: Gemini Flash, GPT-5 Mini (lowest latency)
451
+ - balanced: GPT-5.4, Claude Sonnet 4.6, Gemini Pro (best default)
452
+ - powerful: GPT-5.4-Pro, Claude Opus 4.6 (highest quality)
453
+ - cheap: NVIDIA free + DeepSeek (lowest cost)
454
+ - reasoning: o3, o1, DeepSeek Reasoner (complex logic)
455
+ - free: All free NVIDIA models (zero cost)
456
+ - coding: GPT-5.3-Codex, Qwen3-Coder, Devstral (code tasks)
457
+
458
+ Run blockrun_models for live pricing.`,
459
+ inputSchema: {
460
+ message: z2.string().describe("Your message to the AI"),
461
+ model: z2.string().optional().describe("Specific model ID (e.g., 'openai/gpt-4o')"),
462
+ mode: z2.enum(["fast", "balanced", "powerful", "cheap", "reasoning", "free", "coding"]).optional().describe("Smart routing mode (ignored if model specified)"),
463
+ routing: z2.enum(["smart"]).optional().describe('Set to "smart" to auto-select the optimal model via ClawRouter (14-dimension AI routing)'),
464
+ routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "free" (zero cost NVIDIA), "eco" (budget), "auto" (balanced, default), "premium" (best quality) (only applies when routing: "smart")'),
465
+ system: z2.string().optional().describe("Optional system prompt"),
466
+ max_tokens: z2.number().optional().default(1024).describe("Max tokens in response"),
467
+ temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
468
+ agent_id: z2.string().optional().describe("Agent identifier. If a budget was delegated for this agent_id via blockrun_wallet action:'delegate', spending is tracked and enforced. The agent is hard-stopped when its budget is exhausted."),
469
+ messages: z2.array(z2.object({
470
+ role: z2.enum(["user", "assistant", "system"]),
471
+ content: z2.string()
472
+ })).optional().describe("Conversation history for multi-turn context. When provided, 'message' is appended as the final user turn. Use with explicit 'model' param (defaults to 'openai/gpt-5.4' if not specified). Note: if you include a role:'system' entry in messages[], do not also pass the system param to avoid duplicate system messages.")
473
+ }
474
+ },
475
+ async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, agent_id, messages }) => {
476
+ const llm = getClient();
477
+ const budgetCheck = checkBudget(budget, agent_id);
478
+ if (!budgetCheck.allowed) {
479
+ return {
480
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
481
+ isError: true
482
+ };
483
+ }
484
+ if (routing === "smart") {
485
+ try {
486
+ const result = await llm.smartChat(message, {
487
+ system,
488
+ maxTokens: max_tokens,
489
+ temperature,
490
+ routingProfile: routing_profile
491
+ });
492
+ recordSpending(budget, result.routing.costEstimate || 1e-3, agent_id);
493
+ return {
494
+ content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
495
+
496
+ ${result.response}` }],
497
+ structuredContent: {
498
+ model_used: result.model,
499
+ response: result.response,
500
+ routing: result.routing
501
+ }
502
+ };
503
+ } catch (error) {
504
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
505
+ return { content: [{ type: "text", text: formatError(errorMessage2) }], isError: true };
506
+ }
507
+ }
508
+ if (messages && messages.length > 0) {
509
+ const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.4";
510
+ const fullMessages = [
511
+ ...system ? [{ role: "system", content: system }] : [],
512
+ ...messages,
513
+ { role: "user", content: message }
514
+ ];
515
+ try {
516
+ const result = await llm.chatCompletion(targetModel, fullMessages, {
517
+ maxTokens: max_tokens,
518
+ temperature
519
+ });
520
+ const reply = result.choices?.[0]?.message?.content || "";
521
+ recordSpending(budget, 1e-3, agent_id);
522
+ return {
523
+ content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
524
+
525
+ ${reply}` }],
526
+ structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
527
+ };
528
+ } catch (error) {
529
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
530
+ return { content: [{ type: "text", text: formatError(errorMessage2) }], isError: true };
531
+ }
532
+ }
533
+ if (model) {
534
+ try {
535
+ const response = await llm.chat(model, message, {
536
+ system,
537
+ maxTokens: max_tokens,
538
+ temperature
539
+ });
540
+ recordSpending(budget, 1e-3, agent_id);
541
+ return { content: [{ type: "text", text: response }] };
542
+ } catch (error) {
543
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
544
+ return {
545
+ content: [{ type: "text", text: formatError(errorMessage2) }],
546
+ isError: true
547
+ };
548
+ }
549
+ }
550
+ const routingMode = mode || "balanced";
551
+ const models = MODEL_TIERS[routingMode];
552
+ let lastError = null;
553
+ for (const m of models) {
554
+ try {
555
+ const response = await llm.chat(m, message, {
556
+ system,
557
+ maxTokens: max_tokens
558
+ });
559
+ recordSpending(budget, 1e-3, agent_id);
560
+ return {
561
+ content: [{ type: "text", text: `[${m}]
562
+
563
+ ${response}` }],
564
+ structuredContent: { model_used: m, response }
565
+ };
566
+ } catch (error) {
567
+ lastError = error;
568
+ continue;
569
+ }
570
+ }
571
+ const errorMessage = lastError?.message || "All models failed";
572
+ return {
573
+ content: [{ type: "text", text: formatError(errorMessage) }],
574
+ isError: true
575
+ };
576
+ }
577
+ );
578
+ }
579
+
580
+ // src/tools/models.ts
581
+ import { z as z3 } from "zod";
582
+ function registerModelsTool(server, modelCache) {
583
+ server.registerTool(
584
+ "blockrun_models",
585
+ {
586
+ description: "List available AI models with pricing. Use to discover models and compare costs.",
587
+ inputSchema: {
588
+ category: z3.enum(["all", "chat", "reasoning", "image", "embedding"]).optional().default("all").describe("Filter by category"),
589
+ provider: z3.string().optional().describe("Filter by provider (e.g., 'openai', 'anthropic')")
590
+ }
591
+ },
592
+ async ({ category, provider }) => {
593
+ const llm = getClient();
594
+ if (!modelCache.models) {
595
+ modelCache.models = await llm.listModels();
596
+ setTimeout(() => {
597
+ modelCache.models = null;
598
+ }, 5 * 60 * 1e3);
599
+ }
600
+ let models = modelCache.models;
601
+ if (provider) {
602
+ const p = provider.toLowerCase();
603
+ models = models.filter((m) => m.id.toLowerCase().startsWith(p + "/"));
604
+ }
605
+ if (category && category !== "all") {
606
+ if (category === "image") {
607
+ models = models.filter((m) => m.id.includes("dall-e") || m.id.includes("flux") || m.id.includes("banana"));
608
+ } else if (category === "embedding") {
609
+ models = models.filter((m) => m.id.includes("embed"));
610
+ } else {
611
+ models = models.filter((m) => m.categories?.includes(category));
612
+ }
613
+ }
614
+ const lines = models.map((m) => {
615
+ const input = m.inputPrice ? `$${m.inputPrice}/M in` : "";
616
+ const output = m.outputPrice ? `$${m.outputPrice}/M out` : "";
617
+ const pricing = [input, output].filter(Boolean).join(", ");
618
+ const ctx = m.contextWindow ? ` | ${Math.round(m.contextWindow / 1e3)}K ctx` : "";
619
+ const cats = m.categories?.length ? ` [${m.categories.join(", ")}]` : "";
620
+ return `- ${m.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
621
+ });
622
+ return {
623
+ content: [{ type: "text", text: `Models (${models.length}):
624
+ ${lines.join("\n")}` }],
625
+ structuredContent: { count: models.length, models }
626
+ };
627
+ }
628
+ );
629
+ }
630
+
631
+ // src/tools/image.ts
632
+ import { z as z4 } from "zod";
633
+ function registerImageTool(server) {
634
+ server.registerTool(
635
+ "blockrun_image",
636
+ {
637
+ description: `Generate or edit images via BlockRun.
638
+
639
+ Actions:
640
+ - generate (default): Create image from text prompt
641
+ - edit: Transform an existing image using img2img
642
+
643
+ Generation models: openai/dall-e-3 ($0.04-0.08), together/flux-schnell ($0.02), google/nano-banana
644
+ Edit models: openai/gpt-image-1 (default for edits)`,
645
+ inputSchema: {
646
+ prompt: z4.string().describe("Image description or edit instructions"),
647
+ action: z4.enum(["generate", "edit"]).optional().default("generate").describe("generate: create from text; edit: transform existing image"),
648
+ model: z4.enum(["openai/dall-e-3", "together/flux-schnell", "google/nano-banana", "openai/gpt-image-1"]).optional().describe("Model to use (default: dall-e-3 for generate, gpt-image-1 for edit)"),
649
+ image: z4.string().optional().describe("Source image for edit action: base64-encoded image or URL"),
650
+ size: z4.enum(["1024x1024", "1792x1024", "1024x1792"]).optional().default("1024x1024"),
651
+ quality: z4.enum(["standard", "hd"]).optional().default("standard")
652
+ }
653
+ },
654
+ async ({ prompt, action, model, image, size, quality }) => {
655
+ try {
656
+ const imgClient = getImageClient();
657
+ let response;
658
+ if (action === "edit") {
659
+ if (!image) {
660
+ return {
661
+ content: [{ type: "text", text: formatError("image parameter required for edit action (base64 or URL)") }],
662
+ isError: true
663
+ };
664
+ }
665
+ response = await imgClient.edit(prompt, image, {
666
+ model: model || "openai/gpt-image-1",
667
+ size
668
+ });
669
+ } else {
670
+ response = await imgClient.generate(prompt, {
671
+ model: model || "openai/dall-e-3",
672
+ size,
673
+ quality
674
+ });
675
+ }
676
+ const imageUrl = response.data?.[0]?.url;
677
+ if (!imageUrl) {
678
+ return {
679
+ content: [{ type: "text", text: formatError("No image URL in response") }],
680
+ isError: true
681
+ };
682
+ }
683
+ return {
684
+ content: [{ type: "text", text: `Image: ${imageUrl}
685
+ Prompt: ${prompt}
686
+ Model: ${model}` }],
687
+ structuredContent: { url: imageUrl, prompt, model }
688
+ };
689
+ } catch (err) {
690
+ const errMsg = err instanceof Error ? err.message : String(err);
691
+ if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402")) {
692
+ return {
693
+ content: [{ type: "text", text: `Image generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
694
+ Error: ${errMsg}` }],
695
+ isError: true
696
+ };
697
+ }
698
+ return {
699
+ content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}`) }],
700
+ isError: true
701
+ };
702
+ }
703
+ }
704
+ );
705
+ }
706
+
707
+ // src/tools/twitter.ts
708
+ import { z as z5 } from "zod";
709
+ function registerTwitterTool(server, budget) {
710
+ server.registerTool(
711
+ "blockrun_twitter",
712
+ {
713
+ description: `Search real-time X/Twitter via Grok. Use for trending topics, @handles, breaking news.`,
714
+ inputSchema: {
715
+ query: z5.string().describe("Search query (can include @handles, topics)"),
716
+ max_results: z5.number().optional().default(10).describe("Max results (1-25)")
717
+ }
718
+ },
719
+ async ({ query, max_results }) => {
720
+ const budgetCheck = checkBudget(budget);
721
+ if (!budgetCheck.allowed) {
722
+ return {
723
+ content: [{ type: "text", text: `Budget limit reached ($${budget.spent.toFixed(4)} of $${budget.limit?.toFixed(2)}). Use blockrun_wallet with action: "budget" to adjust.` }],
724
+ isError: true
725
+ };
726
+ }
727
+ try {
728
+ const llm = getClient();
729
+ const response = await llm.chat("xai/grok-3", query, {
730
+ system: `Real-time X/Twitter search. Focus on recent posts, key accounts, engagement. Max results: ${max_results}`,
731
+ search: true
732
+ });
733
+ recordSpending(budget, 2e-3);
734
+ return {
735
+ content: [{ type: "text", text: `[X/Twitter via Grok]
736
+
737
+ ${response}` }],
738
+ structuredContent: { query, model: "xai/grok-3", response }
739
+ };
740
+ } catch (error) {
741
+ const errorMessage = error instanceof Error ? error.message : String(error);
742
+ return {
743
+ content: [{ type: "text", text: formatError(errorMessage) }],
744
+ isError: true
745
+ };
746
+ }
747
+ }
748
+ );
749
+ }
750
+
751
+ // src/tools/search.ts
752
+ import { z as z6 } from "zod";
753
+ function registerSearchTool(server) {
754
+ server.registerTool(
755
+ "blockrun_search",
756
+ {
757
+ description: `Real-time web, X/Twitter, and news search with AI-summarized results and citations.
758
+
759
+ Sources: web, x (X/Twitter), news \u2014 defaults to all three.
760
+ Pricing: ~$0.01/search
761
+
762
+ Returns a summary with cited sources.`,
763
+ inputSchema: {
764
+ query: z6.string().describe("Search query"),
765
+ sources: z6.array(z6.enum(["web", "x", "news"])).optional().describe("Sources to search (default: web + x + news)"),
766
+ max_results: z6.number().optional().default(10).describe("Max results per source (1-20)"),
767
+ from_date: z6.string().optional().describe("Start date filter (YYYY-MM-DD)"),
768
+ to_date: z6.string().optional().describe("End date filter (YYYY-MM-DD)")
769
+ }
770
+ },
771
+ async ({ query, sources, max_results, from_date, to_date }) => {
772
+ try {
773
+ const llm = getClient();
774
+ const result = await llm.search(query, {
775
+ sources,
776
+ maxResults: max_results,
777
+ fromDate: from_date,
778
+ toDate: to_date
779
+ });
780
+ return {
781
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
782
+ structuredContent: result
783
+ };
784
+ } catch (err) {
785
+ const errMsg = err instanceof Error ? err.message : String(err);
786
+ return {
787
+ content: [{ type: "text", text: formatError(errMsg) }],
788
+ isError: true
789
+ };
790
+ }
791
+ }
792
+ );
793
+ }
794
+
795
+ // src/tools/exa.ts
796
+ import { z as z7 } from "zod";
797
+ function registerExaTool(server) {
798
+ server.registerTool(
799
+ "blockrun_exa",
800
+ {
801
+ description: `Neural web search via Exa. Understands meaning, not just keywords. Great for research.
802
+
803
+ Actions:
804
+ - search: Find semantically relevant URLs and metadata ($0.01/call)
805
+ - answer: Get a cited, hallucination-free answer grounded in real web sources ($0.01/call)
806
+ - contents: Fetch full Markdown text from URLs, ready for LLM context ($0.002/URL)
807
+ - similar: Find pages semantically similar to a given URL ($0.01/call)`,
808
+ inputSchema: {
809
+ action: z7.enum(["search", "answer", "contents", "similar"]).describe("Action to perform"),
810
+ query: z7.string().optional().describe("Natural language query (for search/answer)"),
811
+ url: z7.string().optional().describe("Reference URL to find similar pages (for similar action)"),
812
+ urls: z7.array(z7.string()).optional().describe("URLs to fetch content from (for contents action, up to 100)"),
813
+ num_results: z7.number().optional().describe("Number of results to return (default: 10)"),
814
+ category: z7.string().optional().describe("Category filter: 'news', 'research paper', 'company', 'tweet', 'github', 'pdf'"),
815
+ include_domains: z7.array(z7.string()).optional().describe("Only search within these domains"),
816
+ exclude_domains: z7.array(z7.string()).optional().describe("Exclude these domains from results")
817
+ }
818
+ },
819
+ async ({ action, query, url, urls, num_results, category, include_domains, exclude_domains }) => {
820
+ try {
821
+ const llm = getClient();
822
+ let result;
823
+ const req = llm;
824
+ switch (action) {
825
+ case "search":
826
+ if (!query) throw new Error("query required for search action");
827
+ result = await req.requestWithPaymentRaw("/v1/exa/search", {
828
+ query,
829
+ numResults: num_results,
830
+ category,
831
+ includeDomains: include_domains,
832
+ excludeDomains: exclude_domains
833
+ });
834
+ break;
835
+ case "answer":
836
+ if (!query) throw new Error("query required for answer action");
837
+ result = await req.requestWithPaymentRaw("/v1/exa/answer", { query });
838
+ break;
839
+ case "contents":
840
+ if (!urls?.length) throw new Error("urls array required for contents action");
841
+ result = await req.requestWithPaymentRaw("/v1/exa/contents", { urls });
842
+ break;
843
+ case "similar":
844
+ if (!url) throw new Error("url required for similar action");
845
+ result = await req.requestWithPaymentRaw("/v1/exa/find-similar", {
846
+ url,
847
+ numResults: num_results
848
+ });
849
+ break;
850
+ default:
851
+ throw new Error(`Unknown action: ${action}`);
852
+ }
853
+ return {
854
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
855
+ structuredContent: result
856
+ };
857
+ } catch (err) {
858
+ const errMsg = err instanceof Error ? err.message : String(err);
859
+ return {
860
+ content: [{ type: "text", text: formatError(errMsg) }],
861
+ isError: true
862
+ };
863
+ }
864
+ }
865
+ );
866
+ }
867
+
868
+ // src/tools/markets.ts
869
+ import { z as z8 } from "zod";
870
+ function registerMarketsTool(server) {
871
+ server.registerTool(
872
+ "blockrun_markets",
873
+ {
874
+ description: `Prediction market data via Predexon. Real-time data from Polymarket, Kalshi, dFlow, and Binance Futures.
875
+
876
+ Usage:
877
+ - GET: blockrun_markets with path + optional params
878
+ - POST: blockrun_markets with path + body
879
+
880
+ Example paths:
881
+ - "polymarket/events" \u2014 list active events
882
+ - "polymarket/search" + params: { q: "bitcoin" } \u2014 search events
883
+ - "kalshi/markets/KXBTC-25MAR14" \u2014 specific market
884
+ - "polymarket/query" + body: { filter: "active", limit: 10 } \u2014 structured query
885
+
886
+ Pricing: $0.001/GET, $0.005/POST`,
887
+ inputSchema: {
888
+ path: z8.string().describe("Endpoint path, e.g. 'polymarket/events', 'kalshi/markets/KXBTC-25MAR14'"),
889
+ params: z8.record(z8.string()).optional().describe("Query parameters for GET requests"),
890
+ body: z8.record(z8.unknown()).optional().describe("JSON body for POST queries (triggers pmQuery)")
891
+ }
892
+ },
893
+ async ({ path: path2, params, body }) => {
894
+ try {
895
+ const llm = getClient();
896
+ const result = body ? await llm.pmQuery(path2, body) : await llm.pm(path2, params);
897
+ return {
898
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
899
+ structuredContent: result
900
+ };
901
+ } catch (err) {
902
+ const errMsg = err instanceof Error ? err.message : String(err);
903
+ return {
904
+ content: [{ type: "text", text: formatError(errMsg) }],
905
+ isError: true
906
+ };
907
+ }
908
+ }
909
+ );
910
+ }
911
+
912
+ // src/mcp-handler.ts
913
+ function initializeMcpServer(server) {
914
+ const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
915
+ const modelCache = { models: null };
916
+ registerWalletTool(server, budget);
917
+ registerChatTool(server, budget);
918
+ registerModelsTool(server, modelCache);
919
+ registerImageTool(server);
920
+ registerTwitterTool(server, budget);
921
+ registerSearchTool(server);
922
+ registerExaTool(server);
923
+ registerMarketsTool(server);
924
+ server.registerResource(
925
+ "wallet",
926
+ "blockrun://wallet",
927
+ { description: "Wallet address and status", mimeType: "application/json" },
928
+ async () => ({
929
+ contents: [{
930
+ uri: "blockrun://wallet",
931
+ mimeType: "application/json",
932
+ text: JSON.stringify(getWalletInfo(), null, 2)
933
+ }]
934
+ })
935
+ );
936
+ server.registerResource(
937
+ "models",
938
+ "blockrun://models",
939
+ { description: "Available AI models with pricing", mimeType: "application/json" },
940
+ async () => {
941
+ const llm = getClient();
942
+ if (!modelCache.models) {
943
+ modelCache.models = await llm.listModels();
944
+ setTimeout(() => {
945
+ modelCache.models = null;
946
+ }, 5 * 60 * 1e3);
947
+ }
948
+ return {
949
+ contents: [{
950
+ uri: "blockrun://models",
951
+ mimeType: "application/json",
952
+ text: JSON.stringify(modelCache.models, null, 2)
953
+ }]
954
+ };
955
+ }
956
+ );
957
+ }
958
+
959
+ export {
960
+ initializeMcpServer
961
+ };