@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.
- package/README.md +220 -290
- package/dist/chunk-H2DXT5AJ.js +961 -0
- package/dist/http-server.d.ts +20 -0
- package/dist/http-server.js +59 -0
- package/dist/index.js +19 -1135
- package/package.json +4 -4
|
@@ -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
|
+
};
|