@valeo-mcp/server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/valeo-mcp.js +2 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +1254 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1216 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import bs58 from "bs58";
|
|
3
|
+
function loadConfig() {
|
|
4
|
+
const privateKey = process.env.VALEO_PRIVATE_KEY;
|
|
5
|
+
if (!privateKey) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"VALEO_PRIVATE_KEY is required.\n For Base/EVM: set a 0x-prefixed hex private key (66 chars)\n For Solana: set a base58-encoded secret key\n Example: VALEO_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
const chain = process.env.VALEO_CHAIN || "base";
|
|
11
|
+
const validChains = ["base", "base-sepolia", "solana", "solana-devnet"];
|
|
12
|
+
if (!validChains.includes(chain)) {
|
|
13
|
+
throw new Error(`VALEO_CHAIN must be one of: ${validChains.join(", ")}. Got: "${chain}"`);
|
|
14
|
+
}
|
|
15
|
+
if (chain.startsWith("base")) {
|
|
16
|
+
if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`EVM private key must start with 0x and be 66 characters. Got ${privateKey.length} chars.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
try {
|
|
23
|
+
const decoded = bs58.decode(privateKey);
|
|
24
|
+
if (decoded.length !== 64) {
|
|
25
|
+
throw new Error(`Solana secret key must decode to 64 bytes. Got ${decoded.length}.`);
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
29
|
+
throw new Error(`Invalid Solana private key (base58 decode failed): ${err.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
privateKey,
|
|
34
|
+
chain,
|
|
35
|
+
aceApiKey: process.env.VALEO_ACE_API_KEY || void 0,
|
|
36
|
+
aceBaseUrl: process.env.VALEO_ACE_BASE_URL || "https://api.agentcreditengine.com",
|
|
37
|
+
sentinelApiKey: process.env.VALEO_SENTINEL_API_KEY || void 0,
|
|
38
|
+
sentinelBaseUrl: process.env.VALEO_SENTINEL_BASE_URL || "https://sentinel.valeocash.com",
|
|
39
|
+
four02mdBaseUrl: process.env.VALEO_402MD_BASE_URL || "https://402md.valeoprotocol.io",
|
|
40
|
+
facilitatorUrl: process.env.VALEO_FACILITATOR_URL || "https://x402.org/facilitator",
|
|
41
|
+
defaultMaxPerCall: parseFloat(process.env.VALEO_MAX_PER_CALL || "1.00"),
|
|
42
|
+
defaultDailyLimit: parseFloat(process.env.VALEO_DAILY_LIMIT || "50.00"),
|
|
43
|
+
defaultSessionLimit: parseFloat(process.env.VALEO_SESSION_LIMIT || "10.00")
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/budget/manager.ts
|
|
48
|
+
var BudgetManager = class {
|
|
49
|
+
config;
|
|
50
|
+
sessionSpent = 0;
|
|
51
|
+
dailySpent = 0;
|
|
52
|
+
dailyResetAt;
|
|
53
|
+
constructor(valeoConfig) {
|
|
54
|
+
this.config = {
|
|
55
|
+
maxPerCall: valeoConfig.defaultMaxPerCall,
|
|
56
|
+
dailyLimit: valeoConfig.defaultDailyLimit,
|
|
57
|
+
sessionLimit: valeoConfig.defaultSessionLimit,
|
|
58
|
+
blockedEndpoints: [],
|
|
59
|
+
allowedEndpoints: []
|
|
60
|
+
};
|
|
61
|
+
this.dailyResetAt = this.getNextMidnightUTC();
|
|
62
|
+
}
|
|
63
|
+
getNextMidnightUTC() {
|
|
64
|
+
const now = /* @__PURE__ */ new Date();
|
|
65
|
+
const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
|
66
|
+
return tomorrow.getTime();
|
|
67
|
+
}
|
|
68
|
+
checkDailyReset() {
|
|
69
|
+
if (Date.now() >= this.dailyResetAt) {
|
|
70
|
+
this.dailySpent = 0;
|
|
71
|
+
this.dailyResetAt = this.getNextMidnightUTC();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
canSpend(amount, endpoint) {
|
|
75
|
+
this.checkDailyReset();
|
|
76
|
+
if (this.config.allowedEndpoints.length > 0) {
|
|
77
|
+
const allowed = this.config.allowedEndpoints.some((pattern) => this.matchWildcard(endpoint, pattern));
|
|
78
|
+
if (!allowed) {
|
|
79
|
+
return {
|
|
80
|
+
allowed: false,
|
|
81
|
+
reason: `Endpoint "${endpoint}" is not in the allowlist. Use set_budget to update allowed_endpoints.`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (this.config.allowedEndpoints.length === 0 && this.config.blockedEndpoints.length > 0) {
|
|
86
|
+
const blocked = this.config.blockedEndpoints.some((pattern) => this.matchWildcard(endpoint, pattern));
|
|
87
|
+
if (blocked) {
|
|
88
|
+
return {
|
|
89
|
+
allowed: false,
|
|
90
|
+
reason: `Endpoint "${endpoint}" is blocked. Use set_budget to update blocked_endpoints.`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (amount > this.config.maxPerCall) {
|
|
95
|
+
return {
|
|
96
|
+
allowed: false,
|
|
97
|
+
reason: `Call costs $${amount.toFixed(4)} but max_per_call is $${this.config.maxPerCall.toFixed(2)}. Use set_budget to increase.`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (this.sessionSpent + amount > this.config.sessionLimit) {
|
|
101
|
+
return {
|
|
102
|
+
allowed: false,
|
|
103
|
+
reason: `Session limit reached. Spent $${this.sessionSpent.toFixed(4)} of $${this.config.sessionLimit.toFixed(2)}. Use set_budget to increase session_limit.`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (this.dailySpent + amount > this.config.dailyLimit) {
|
|
107
|
+
return {
|
|
108
|
+
allowed: false,
|
|
109
|
+
reason: `Daily limit reached. Spent $${this.dailySpent.toFixed(4)} of $${this.config.dailyLimit.toFixed(2)} today. Use set_budget to increase daily_limit.`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { allowed: true };
|
|
113
|
+
}
|
|
114
|
+
recordSpend(amount) {
|
|
115
|
+
this.checkDailyReset();
|
|
116
|
+
this.sessionSpent += amount;
|
|
117
|
+
this.dailySpent += amount;
|
|
118
|
+
}
|
|
119
|
+
getStatus() {
|
|
120
|
+
this.checkDailyReset();
|
|
121
|
+
return {
|
|
122
|
+
config: { ...this.config },
|
|
123
|
+
spending: {
|
|
124
|
+
session: this.sessionSpent,
|
|
125
|
+
today: this.dailySpent,
|
|
126
|
+
remainingSession: Math.max(0, this.config.sessionLimit - this.sessionSpent),
|
|
127
|
+
remainingDaily: Math.max(0, this.config.dailyLimit - this.dailySpent)
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
updateConfig(updates) {
|
|
132
|
+
if (updates.maxPerCall !== void 0) this.config.maxPerCall = updates.maxPerCall;
|
|
133
|
+
if (updates.dailyLimit !== void 0) this.config.dailyLimit = updates.dailyLimit;
|
|
134
|
+
if (updates.sessionLimit !== void 0) this.config.sessionLimit = updates.sessionLimit;
|
|
135
|
+
if (updates.blockedEndpoints !== void 0) this.config.blockedEndpoints = updates.blockedEndpoints;
|
|
136
|
+
if (updates.allowedEndpoints !== void 0) this.config.allowedEndpoints = updates.allowedEndpoints;
|
|
137
|
+
}
|
|
138
|
+
matchWildcard(value, pattern) {
|
|
139
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
140
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/receipts/store.ts
|
|
145
|
+
import { randomUUID } from "crypto";
|
|
146
|
+
var ReceiptStore = class {
|
|
147
|
+
receipts = [];
|
|
148
|
+
add(data) {
|
|
149
|
+
const receipt = {
|
|
150
|
+
...data,
|
|
151
|
+
id: randomUUID(),
|
|
152
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
153
|
+
};
|
|
154
|
+
this.receipts.push(receipt);
|
|
155
|
+
return receipt;
|
|
156
|
+
}
|
|
157
|
+
getAll(options) {
|
|
158
|
+
let results = [...this.receipts];
|
|
159
|
+
if (options?.since) {
|
|
160
|
+
const sinceTime = new Date(options.since).getTime();
|
|
161
|
+
results = results.filter((r) => new Date(r.timestamp).getTime() >= sinceTime);
|
|
162
|
+
}
|
|
163
|
+
if (options?.endpoint) {
|
|
164
|
+
const ep = options.endpoint.toLowerCase();
|
|
165
|
+
results = results.filter((r) => r.endpoint.toLowerCase().includes(ep));
|
|
166
|
+
}
|
|
167
|
+
results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
168
|
+
if (options?.limit) {
|
|
169
|
+
results = results.slice(0, options.limit);
|
|
170
|
+
}
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
getTotalSpent() {
|
|
174
|
+
return this.receipts.filter((r) => r.status === "settled").reduce((sum, r) => sum + r.amount, 0);
|
|
175
|
+
}
|
|
176
|
+
getTotalSpentToday() {
|
|
177
|
+
const startOfDayUTC = /* @__PURE__ */ new Date();
|
|
178
|
+
startOfDayUTC.setUTCHours(0, 0, 0, 0);
|
|
179
|
+
const startMs = startOfDayUTC.getTime();
|
|
180
|
+
return this.receipts.filter((r) => r.status === "settled" && new Date(r.timestamp).getTime() >= startMs).reduce((sum, r) => sum + r.amount, 0);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/clients/x402-base.ts
|
|
185
|
+
import { wrapFetchWithPayment } from "x402-fetch";
|
|
186
|
+
import { createWalletClient, createPublicClient, http, parseAbi } from "viem";
|
|
187
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
188
|
+
import { base, baseSepolia } from "viem/chains";
|
|
189
|
+
var ERC20_BALANCE_ABI = parseAbi(["function balanceOf(address owner) view returns (uint256)"]);
|
|
190
|
+
var USDC_ADDRESSES = {
|
|
191
|
+
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
192
|
+
"base-sepolia": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
193
|
+
};
|
|
194
|
+
var X402BaseClient = class {
|
|
195
|
+
walletClient;
|
|
196
|
+
publicClient;
|
|
197
|
+
walletAddress;
|
|
198
|
+
chainName;
|
|
199
|
+
constructor(privateKey, chain) {
|
|
200
|
+
this.chainName = chain;
|
|
201
|
+
const chainConfig = chain === "base" ? base : baseSepolia;
|
|
202
|
+
const account = privateKeyToAccount(privateKey);
|
|
203
|
+
this.walletAddress = account.address;
|
|
204
|
+
this.walletClient = createWalletClient({
|
|
205
|
+
account,
|
|
206
|
+
chain: chainConfig,
|
|
207
|
+
transport: http()
|
|
208
|
+
});
|
|
209
|
+
this.publicClient = createPublicClient({
|
|
210
|
+
chain: chainConfig,
|
|
211
|
+
transport: http()
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
createPaidFetch(maxUsd) {
|
|
215
|
+
const maxValue = maxUsd !== void 0 ? BigInt(Math.round(maxUsd * 1e6)) : void 0;
|
|
216
|
+
return wrapFetchWithPayment(globalThis.fetch, this.walletClient, maxValue);
|
|
217
|
+
}
|
|
218
|
+
async getUsdcBalance() {
|
|
219
|
+
const usdcAddress = USDC_ADDRESSES[this.chainName];
|
|
220
|
+
const balance = await this.publicClient.readContract({
|
|
221
|
+
address: usdcAddress,
|
|
222
|
+
abi: ERC20_BALANCE_ABI,
|
|
223
|
+
functionName: "balanceOf",
|
|
224
|
+
args: [this.walletAddress]
|
|
225
|
+
});
|
|
226
|
+
return (Number(balance) / 1e6).toFixed(6);
|
|
227
|
+
}
|
|
228
|
+
async getEthBalance() {
|
|
229
|
+
const balance = await this.publicClient.getBalance({
|
|
230
|
+
address: this.walletAddress
|
|
231
|
+
});
|
|
232
|
+
return (Number(balance) / 1e18).toFixed(6);
|
|
233
|
+
}
|
|
234
|
+
getAddress() {
|
|
235
|
+
return this.walletAddress;
|
|
236
|
+
}
|
|
237
|
+
getChain() {
|
|
238
|
+
return this.chainName;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/clients/x402-solana.ts
|
|
243
|
+
import { createX402Client } from "x402-solana";
|
|
244
|
+
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
|
245
|
+
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
|
|
246
|
+
import bs582 from "bs58";
|
|
247
|
+
var USDC_MINTS = {
|
|
248
|
+
solana: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
|
|
249
|
+
"solana-devnet": new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")
|
|
250
|
+
};
|
|
251
|
+
var X402SolanaClient = class {
|
|
252
|
+
keypair;
|
|
253
|
+
connection;
|
|
254
|
+
network;
|
|
255
|
+
constructor(privateKeyBase58, chain, rpcUrl) {
|
|
256
|
+
this.keypair = Keypair.fromSecretKey(bs582.decode(privateKeyBase58));
|
|
257
|
+
this.network = chain;
|
|
258
|
+
const defaultRpc = chain === "solana" ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com";
|
|
259
|
+
this.connection = new Connection(rpcUrl || defaultRpc);
|
|
260
|
+
}
|
|
261
|
+
createPaidFetch(maxUsd) {
|
|
262
|
+
const amount = maxUsd !== void 0 ? BigInt(Math.round(maxUsd * 1e6)) : BigInt(0);
|
|
263
|
+
const client = createX402Client({
|
|
264
|
+
wallet: {
|
|
265
|
+
address: this.keypair.publicKey.toBase58(),
|
|
266
|
+
signTransaction: async (tx) => {
|
|
267
|
+
tx.sign([this.keypair]);
|
|
268
|
+
return tx;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
network: this.network,
|
|
272
|
+
amount
|
|
273
|
+
});
|
|
274
|
+
const inner = client.fetch.bind(client);
|
|
275
|
+
return ((input, init) => inner(input, {
|
|
276
|
+
...init,
|
|
277
|
+
signal: init?.signal ?? AbortSignal.timeout(15e3)
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
async getUsdcBalance() {
|
|
281
|
+
const mint = USDC_MINTS[this.network];
|
|
282
|
+
try {
|
|
283
|
+
const ata = await getAssociatedTokenAddress(mint, this.keypair.publicKey);
|
|
284
|
+
const account = await getAccount(this.connection, ata);
|
|
285
|
+
return (Number(account.amount) / 1e6).toFixed(6);
|
|
286
|
+
} catch {
|
|
287
|
+
return "0.000000";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async getSolBalance() {
|
|
291
|
+
const balance = await this.connection.getBalance(this.keypair.publicKey);
|
|
292
|
+
return (balance / 1e9).toFixed(4);
|
|
293
|
+
}
|
|
294
|
+
getAddress() {
|
|
295
|
+
return this.keypair.publicKey.toBase58();
|
|
296
|
+
}
|
|
297
|
+
getChain() {
|
|
298
|
+
return this.network;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/clients/ace.ts
|
|
303
|
+
var ACEClient = class {
|
|
304
|
+
baseUrl;
|
|
305
|
+
apiKey;
|
|
306
|
+
constructor(baseUrl, apiKey) {
|
|
307
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
308
|
+
this.apiKey = apiKey;
|
|
309
|
+
}
|
|
310
|
+
async request(method, path, body) {
|
|
311
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
312
|
+
method,
|
|
313
|
+
headers: {
|
|
314
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
315
|
+
"Content-Type": "application/json"
|
|
316
|
+
},
|
|
317
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
318
|
+
signal: AbortSignal.timeout(15e3)
|
|
319
|
+
});
|
|
320
|
+
const text = await response.text();
|
|
321
|
+
let data;
|
|
322
|
+
try {
|
|
323
|
+
data = JSON.parse(text);
|
|
324
|
+
} catch {
|
|
325
|
+
data = { raw: text };
|
|
326
|
+
}
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
const rec = data;
|
|
329
|
+
const msg = rec?.message ?? rec?.error ?? response.statusText;
|
|
330
|
+
throw new Error(`ACE API error (${response.status}): ${String(msg)}`);
|
|
331
|
+
}
|
|
332
|
+
return data;
|
|
333
|
+
}
|
|
334
|
+
async requestCredit(amount, walletAddress, purpose) {
|
|
335
|
+
return this.request("POST", "/api/v1/credit/request", {
|
|
336
|
+
amount,
|
|
337
|
+
walletAddress,
|
|
338
|
+
purpose
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async repayLoan(loanId, amount) {
|
|
342
|
+
return this.request("POST", `/api/v1/loans/${encodeURIComponent(loanId)}/repay`, {
|
|
343
|
+
...amount !== void 0 ? { amount } : {}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async getLoans() {
|
|
347
|
+
return this.request("GET", "/api/v1/loans");
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// src/clients/sentinel.ts
|
|
352
|
+
var SentinelClient = class {
|
|
353
|
+
baseUrl;
|
|
354
|
+
apiKey;
|
|
355
|
+
constructor(baseUrl, apiKey) {
|
|
356
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
357
|
+
this.apiKey = apiKey;
|
|
358
|
+
}
|
|
359
|
+
async getTransactions(options) {
|
|
360
|
+
const params = new URLSearchParams();
|
|
361
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
362
|
+
if (options?.since) params.set("since", options.since);
|
|
363
|
+
if (options?.endpoint) params.set("endpoint", options.endpoint);
|
|
364
|
+
const qs = params.toString();
|
|
365
|
+
const response = await fetch(`${this.baseUrl}/api/v1/transactions${qs ? `?${qs}` : ""}`, {
|
|
366
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
367
|
+
signal: AbortSignal.timeout(15e3)
|
|
368
|
+
});
|
|
369
|
+
const text = await response.text();
|
|
370
|
+
let data;
|
|
371
|
+
try {
|
|
372
|
+
data = JSON.parse(text);
|
|
373
|
+
} catch {
|
|
374
|
+
data = { raw: text };
|
|
375
|
+
}
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
throw new Error(`Sentinel API error (${response.status}): ${JSON.stringify(data).slice(0, 200)}`);
|
|
378
|
+
}
|
|
379
|
+
return data;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/clients/four02md.ts
|
|
384
|
+
var Four02mdClient = class {
|
|
385
|
+
baseUrl;
|
|
386
|
+
constructor(baseUrl) {
|
|
387
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
388
|
+
}
|
|
389
|
+
async search(options) {
|
|
390
|
+
const params = new URLSearchParams();
|
|
391
|
+
if (options?.query) params.set("q", options.query);
|
|
392
|
+
if (options?.category) params.set("category", options.category);
|
|
393
|
+
if (options?.chain && options.chain !== "any") params.set("chain", options.chain);
|
|
394
|
+
const qs = params.toString();
|
|
395
|
+
const response = await fetch(`${this.baseUrl}/api/manifests${qs ? `?${qs}` : ""}`, {
|
|
396
|
+
signal: AbortSignal.timeout(15e3)
|
|
397
|
+
});
|
|
398
|
+
const text = await response.text();
|
|
399
|
+
let data;
|
|
400
|
+
try {
|
|
401
|
+
data = JSON.parse(text);
|
|
402
|
+
} catch {
|
|
403
|
+
data = { raw: text };
|
|
404
|
+
}
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error(`402.md API error (${response.status}): ${JSON.stringify(data).slice(0, 200)}`);
|
|
407
|
+
}
|
|
408
|
+
if (options?.maxPrice && data !== null && typeof data === "object" && "results" in data) {
|
|
409
|
+
const root = data;
|
|
410
|
+
const results = root.results;
|
|
411
|
+
if (Array.isArray(results)) {
|
|
412
|
+
const filtered = results.filter((manifest) => {
|
|
413
|
+
const endpoints = manifest.endpoints;
|
|
414
|
+
if (!endpoints) return true;
|
|
415
|
+
return endpoints.some((ep) => {
|
|
416
|
+
const priceStr = String(ep.price ?? "0").replace("$", "");
|
|
417
|
+
const n = parseFloat(priceStr);
|
|
418
|
+
return !Number.isNaN(n) && n <= (options.maxPrice ?? Infinity);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
root.results = filtered;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return data;
|
|
425
|
+
}
|
|
426
|
+
async getManifest(domain) {
|
|
427
|
+
const response = await fetch(`${this.baseUrl}/api/manifests/${encodeURIComponent(domain)}`, {
|
|
428
|
+
signal: AbortSignal.timeout(1e4)
|
|
429
|
+
});
|
|
430
|
+
if (!response.ok) {
|
|
431
|
+
throw new Error(`Manifest not found for ${domain}`);
|
|
432
|
+
}
|
|
433
|
+
return response.json();
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/server.ts
|
|
438
|
+
import { readFileSync } from "fs";
|
|
439
|
+
import { dirname, join } from "path";
|
|
440
|
+
import { fileURLToPath } from "url";
|
|
441
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
442
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
443
|
+
import { z } from "zod";
|
|
444
|
+
|
|
445
|
+
// src/tools/pay.ts
|
|
446
|
+
import { decodeXPaymentResponse } from "x402-fetch";
|
|
447
|
+
|
|
448
|
+
// src/tools/helpers.ts
|
|
449
|
+
function jsonResult(data) {
|
|
450
|
+
return {
|
|
451
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function jsonError(message, suggestion) {
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: "text",
|
|
459
|
+
text: JSON.stringify({ error: true, message, suggestion }, null, 2)
|
|
460
|
+
}
|
|
461
|
+
],
|
|
462
|
+
isError: true
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function getBudgetRemaining(ctx) {
|
|
466
|
+
const s = ctx.budget.getStatus();
|
|
467
|
+
return {
|
|
468
|
+
session: s.spending.remainingSession,
|
|
469
|
+
daily: s.spending.remainingDaily
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/tools/pay.ts
|
|
474
|
+
function buildRequestInit(method, headers, body) {
|
|
475
|
+
const h = new Headers();
|
|
476
|
+
h.set("Accept", "application/json, text/plain, */*");
|
|
477
|
+
if (headers) {
|
|
478
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
479
|
+
h.set(k, v);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (body !== void 0 && method !== "GET" && method !== "HEAD") {
|
|
483
|
+
if (!h.has("Content-Type")) {
|
|
484
|
+
h.set("Content-Type", "application/json");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
method,
|
|
489
|
+
headers: h,
|
|
490
|
+
...body !== void 0 && method !== "GET" && method !== "HEAD" ? { body } : {}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function fetchWithRetry(url, base2) {
|
|
494
|
+
const run = () => fetch(url, { ...base2, signal: AbortSignal.timeout(15e3) });
|
|
495
|
+
try {
|
|
496
|
+
return await run();
|
|
497
|
+
} catch {
|
|
498
|
+
return await run();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function parseX402FromBody(body) {
|
|
502
|
+
if (!body || typeof body !== "object") return null;
|
|
503
|
+
const o = body;
|
|
504
|
+
const accepts = o.accepts;
|
|
505
|
+
if (!Array.isArray(accepts) || accepts.length === 0) return null;
|
|
506
|
+
const first = accepts[0];
|
|
507
|
+
const raw = first.maxAmountRequired;
|
|
508
|
+
if (raw === void 0 || raw === null) return null;
|
|
509
|
+
let maxAmountAtomic;
|
|
510
|
+
try {
|
|
511
|
+
maxAmountAtomic = BigInt(String(raw));
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const priceUsd = Number(maxAmountAtomic) / 1e6;
|
|
516
|
+
const payTo = typeof first.payTo === "string" ? first.payTo : "";
|
|
517
|
+
let description = "";
|
|
518
|
+
if (typeof first.extra === "object" && first.extra !== null && "description" in first.extra) {
|
|
519
|
+
description = String(first.extra.description ?? "");
|
|
520
|
+
} else if (typeof first.description === "string") {
|
|
521
|
+
description = first.description;
|
|
522
|
+
}
|
|
523
|
+
const network = typeof first.network === "string" ? first.network : "";
|
|
524
|
+
return { priceUsd, maxAmountAtomic, payTo, description, chainLabel: network };
|
|
525
|
+
}
|
|
526
|
+
async function parseBodyData(response) {
|
|
527
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
528
|
+
const text = await response.text();
|
|
529
|
+
if (ct.includes("application/json")) {
|
|
530
|
+
try {
|
|
531
|
+
return JSON.parse(text);
|
|
532
|
+
} catch {
|
|
533
|
+
return text;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return text;
|
|
537
|
+
}
|
|
538
|
+
function isInsufficientFunds(err) {
|
|
539
|
+
const s = err instanceof Error ? err.message : String(err);
|
|
540
|
+
const lower = s.toLowerCase();
|
|
541
|
+
return lower.includes("insufficient") || lower.includes("balance") || lower.includes("exceeds maximum") || lower.includes("funds");
|
|
542
|
+
}
|
|
543
|
+
async function payTool(ctx, input) {
|
|
544
|
+
const { url, method, headers, body } = input;
|
|
545
|
+
const routeOnly = ctx.budget.canSpend(0, url);
|
|
546
|
+
if (!routeOnly.allowed) {
|
|
547
|
+
return jsonError(routeOnly.reason ?? "Request blocked by budget rules.", "Use set_budget to adjust allowlists, blocklists, or limits.");
|
|
548
|
+
}
|
|
549
|
+
const baseInit = buildRequestInit(method, headers, body);
|
|
550
|
+
let first;
|
|
551
|
+
try {
|
|
552
|
+
first = await fetchWithRetry(url, baseInit);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
555
|
+
return jsonError(
|
|
556
|
+
`Network error calling ${url}: ${msg}`,
|
|
557
|
+
"Check connectivity and URL. Retry the pay tool once the endpoint is reachable."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (first.status !== 402) {
|
|
561
|
+
if (!first.ok) {
|
|
562
|
+
const snippet = (await first.text()).slice(0, 500);
|
|
563
|
+
return jsonError(
|
|
564
|
+
`HTTP ${first.status} from ${url}: ${snippet}`,
|
|
565
|
+
"The endpoint returned an error without requesting payment. Fix the URL, method, headers, or body, or authenticate if required."
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
let data2;
|
|
569
|
+
try {
|
|
570
|
+
data2 = await parseBodyData(first);
|
|
571
|
+
} catch (e) {
|
|
572
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
573
|
+
return jsonError(`Failed to read response body: ${msg}`, "Retry the pay tool or inspect the endpoint response format.");
|
|
574
|
+
}
|
|
575
|
+
return jsonResult({
|
|
576
|
+
success: true,
|
|
577
|
+
data: data2,
|
|
578
|
+
payment: null,
|
|
579
|
+
budget_remaining: getBudgetRemaining(ctx)
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
let bodyJson;
|
|
583
|
+
try {
|
|
584
|
+
bodyJson = await first.json();
|
|
585
|
+
} catch {
|
|
586
|
+
return jsonError(
|
|
587
|
+
"Received HTTP 402 but could not parse JSON payment requirements.",
|
|
588
|
+
"Use estimate_cost to inspect the endpoint, or verify the server implements x402 with a JSON 402 body."
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const pricing = parseX402FromBody(bodyJson);
|
|
592
|
+
if (!pricing) {
|
|
593
|
+
return jsonError(
|
|
594
|
+
"Could not extract x402 payment requirements from 402 response.",
|
|
595
|
+
"Use estimate_cost on this URL to debug pricing, or contact the API provider."
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const spendCheck = ctx.budget.canSpend(pricing.priceUsd, url);
|
|
599
|
+
if (!spendCheck.allowed) {
|
|
600
|
+
return jsonError(
|
|
601
|
+
spendCheck.reason ?? "Payment blocked by budget.",
|
|
602
|
+
"Use set_budget to increase limits, or use spending_history to review past spend."
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const maxPerCallUsd = ctx.budget.getStatus().config.maxPerCall;
|
|
606
|
+
const paidFetch = ctx.x402Client.createPaidFetch(maxPerCallUsd);
|
|
607
|
+
let paid;
|
|
608
|
+
try {
|
|
609
|
+
paid = await paidFetch(url, baseInit);
|
|
610
|
+
} catch (e) {
|
|
611
|
+
if (isInsufficientFunds(e)) {
|
|
612
|
+
return jsonError(
|
|
613
|
+
e instanceof Error ? e.message : String(e),
|
|
614
|
+
"Use wallet_balance to confirm USDC and gas, then get_credit if ACE is configured, or add funds to the wallet."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return jsonError(
|
|
618
|
+
e instanceof Error ? e.message : String(e),
|
|
619
|
+
"Payment or signing failed. Verify VALEO_PRIVATE_KEY, chain, and endpoint compatibility with x402."
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (!paid.ok) {
|
|
623
|
+
let errText;
|
|
624
|
+
try {
|
|
625
|
+
errText = await paid.text();
|
|
626
|
+
} catch {
|
|
627
|
+
errText = "";
|
|
628
|
+
}
|
|
629
|
+
return jsonError(
|
|
630
|
+
`Paid request failed with HTTP ${paid.status}: ${errText.slice(0, 500)}`,
|
|
631
|
+
"The resource may require different parameters after payment. Check the API documentation."
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
let data;
|
|
635
|
+
try {
|
|
636
|
+
data = await parseBodyData(paid);
|
|
637
|
+
} catch (e) {
|
|
638
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
639
|
+
return jsonError(`Paid response could not be read: ${msg}`, "Retry the pay tool; the payment may still have settled on-chain.");
|
|
640
|
+
}
|
|
641
|
+
let txHash;
|
|
642
|
+
const payHeader = paid.headers.get("x-payment-response") ?? paid.headers.get("X-PAYMENT-RESPONSE") ?? void 0;
|
|
643
|
+
if (payHeader) {
|
|
644
|
+
try {
|
|
645
|
+
const decoded = decodeXPaymentResponse(payHeader);
|
|
646
|
+
if (decoded?.transaction) {
|
|
647
|
+
txHash = String(decoded.transaction);
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
653
|
+
const payment = {
|
|
654
|
+
amount: pricing.priceUsd.toFixed(6),
|
|
655
|
+
currency: "USDC",
|
|
656
|
+
chain: ctx.config.chain,
|
|
657
|
+
endpoint: url,
|
|
658
|
+
timestamp,
|
|
659
|
+
...txHash ? { txHash } : {}
|
|
660
|
+
};
|
|
661
|
+
ctx.receipts.add({
|
|
662
|
+
endpoint: url,
|
|
663
|
+
method,
|
|
664
|
+
amount: pricing.priceUsd,
|
|
665
|
+
currency: "USDC",
|
|
666
|
+
chain: ctx.config.chain,
|
|
667
|
+
status: "settled",
|
|
668
|
+
responseStatus: paid.status,
|
|
669
|
+
...txHash ? { txHash } : {}
|
|
670
|
+
});
|
|
671
|
+
ctx.budget.recordSpend(pricing.priceUsd);
|
|
672
|
+
return jsonResult({
|
|
673
|
+
success: true,
|
|
674
|
+
data,
|
|
675
|
+
payment,
|
|
676
|
+
budget_remaining: getBudgetRemaining(ctx)
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/tools/wallet-balance.ts
|
|
681
|
+
var EVM_CHAINS = /* @__PURE__ */ new Set(["base", "base-sepolia"]);
|
|
682
|
+
var SOL_CHAINS = /* @__PURE__ */ new Set(["solana", "solana-devnet"]);
|
|
683
|
+
function familyOf(chain) {
|
|
684
|
+
if (EVM_CHAINS.has(chain)) return "evm";
|
|
685
|
+
if (SOL_CHAINS.has(chain)) return "solana";
|
|
686
|
+
return "evm";
|
|
687
|
+
}
|
|
688
|
+
async function walletBalanceTool(ctx, input) {
|
|
689
|
+
const target = input.chain ?? ctx.config.chain;
|
|
690
|
+
if (familyOf(target) !== familyOf(ctx.config.chain)) {
|
|
691
|
+
return jsonError(
|
|
692
|
+
`Cannot check ${target} balance: configured VALEO_CHAIN is ${ctx.config.chain}. The same private key cannot be used for both EVM and Solana.`,
|
|
693
|
+
"Run a separate MCP server instance with VALEO_CHAIN set to the target chain family and a key for that chain."
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
if (target === "base" || target === "base-sepolia") {
|
|
698
|
+
const client2 = new X402BaseClient(ctx.config.privateKey, target);
|
|
699
|
+
const usdc2 = await client2.getUsdcBalance();
|
|
700
|
+
const eth = await client2.getEthBalance();
|
|
701
|
+
return jsonResult({
|
|
702
|
+
chain: target,
|
|
703
|
+
wallet: client2.getAddress(),
|
|
704
|
+
usdc: usdc2,
|
|
705
|
+
gas_token: `${eth} ETH`,
|
|
706
|
+
budget_remaining: getBudgetRemaining(ctx)
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const client = new X402SolanaClient(
|
|
710
|
+
ctx.config.privateKey,
|
|
711
|
+
target,
|
|
712
|
+
process.env.VALEO_SOLANA_RPC_URL
|
|
713
|
+
);
|
|
714
|
+
const usdc = await client.getUsdcBalance();
|
|
715
|
+
const sol = await client.getSolBalance();
|
|
716
|
+
return jsonResult({
|
|
717
|
+
chain: target,
|
|
718
|
+
wallet: client.getAddress(),
|
|
719
|
+
usdc,
|
|
720
|
+
gas_token: `${sol} SOL`,
|
|
721
|
+
budget_remaining: getBudgetRemaining(ctx)
|
|
722
|
+
});
|
|
723
|
+
} catch (e) {
|
|
724
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
725
|
+
return jsonError(
|
|
726
|
+
`Failed to read balances: ${msg}`,
|
|
727
|
+
"Verify VALEO_PRIVATE_KEY matches the chain, RPC is reachable, and try wallet_balance again."
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/tools/get-credit.ts
|
|
733
|
+
function pickString(obj, keys) {
|
|
734
|
+
for (const k of keys) {
|
|
735
|
+
const v = obj[k];
|
|
736
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
737
|
+
}
|
|
738
|
+
return void 0;
|
|
739
|
+
}
|
|
740
|
+
async function getCreditTool(ctx, input) {
|
|
741
|
+
if (!ctx.aceClient) {
|
|
742
|
+
return jsonError(
|
|
743
|
+
"ACE integration not configured. Set VALEO_ACE_API_KEY environment variable.",
|
|
744
|
+
"Get an API key at https://agentcreditengine.com and add VALEO_ACE_API_KEY to your MCP server env."
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
const wallet = ctx.x402Client.getAddress();
|
|
748
|
+
try {
|
|
749
|
+
const raw = await ctx.aceClient.requestCredit(input.amount, wallet, input.purpose);
|
|
750
|
+
if (!raw || typeof raw !== "object") {
|
|
751
|
+
return jsonResult({
|
|
752
|
+
approved: true,
|
|
753
|
+
note: "ACE returned a non-object response; raw payload attached.",
|
|
754
|
+
raw,
|
|
755
|
+
message: "Credit request completed; verify balances with wallet_balance."
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const o = raw;
|
|
759
|
+
const approved = typeof o.approved === "boolean" ? o.approved : typeof o.status === "string" ? o.status.toLowerCase() === "approved" : true;
|
|
760
|
+
if (!approved) {
|
|
761
|
+
return jsonResult({
|
|
762
|
+
approved: false,
|
|
763
|
+
reason: pickString(o, ["reason", "message", "error"]) ?? "Credit not approved",
|
|
764
|
+
current_exposure: pickString(o, ["current_exposure", "currentExposure"]),
|
|
765
|
+
max_exposure: pickString(o, ["max_exposure", "maxExposure"]),
|
|
766
|
+
raw_note: "See raw for full ACE response.",
|
|
767
|
+
raw: o
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
return jsonResult({
|
|
771
|
+
approved: true,
|
|
772
|
+
loan_id: pickString(o, ["loan_id", "loanId", "id"]),
|
|
773
|
+
amount: pickString(o, ["amount", "credit_amount", "creditAmount"]) ?? String(input.amount),
|
|
774
|
+
currency: pickString(o, ["currency"]) ?? "USDC",
|
|
775
|
+
fee: pickString(o, ["fee"]),
|
|
776
|
+
total_repayment: pickString(o, ["total_repayment", "totalRepayment"]),
|
|
777
|
+
auto_repay: typeof o.auto_repay === "boolean" ? o.auto_repay : typeof o.autoRepay === "boolean" ? o.autoRepay : true,
|
|
778
|
+
message: pickString(o, ["message"]) ?? `${pickString(o, ["amount"]) ?? input.amount} USDC credited. Auto-repayment may apply per ACE terms.`,
|
|
779
|
+
raw: o
|
|
780
|
+
});
|
|
781
|
+
} catch (e) {
|
|
782
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
783
|
+
return jsonError(
|
|
784
|
+
msg,
|
|
785
|
+
"Verify VALEO_ACE_API_KEY, wallet address, and ACE account status; retry or contact ACE support."
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/tools/repay-credit.ts
|
|
791
|
+
function pickString2(obj, keys) {
|
|
792
|
+
for (const k of keys) {
|
|
793
|
+
const v = obj[k];
|
|
794
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
795
|
+
}
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
async function repayCreditTool(ctx, input) {
|
|
799
|
+
if (!ctx.aceClient) {
|
|
800
|
+
return jsonError(
|
|
801
|
+
"ACE integration not configured. Set VALEO_ACE_API_KEY environment variable.",
|
|
802
|
+
"Get an API key at https://agentcreditengine.com and add VALEO_ACE_API_KEY to your MCP server env."
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const raw = await ctx.aceClient.repayLoan(input.loan_id, input.amount);
|
|
807
|
+
if (!raw || typeof raw !== "object") {
|
|
808
|
+
return jsonResult({
|
|
809
|
+
success: true,
|
|
810
|
+
note: "ACE returned a non-object response.",
|
|
811
|
+
raw
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
const o = raw;
|
|
815
|
+
return jsonResult({
|
|
816
|
+
success: true,
|
|
817
|
+
loan_id: input.loan_id,
|
|
818
|
+
status: pickString2(o, ["status", "state"]) ?? "submitted",
|
|
819
|
+
message: pickString2(o, ["message"]) ?? "Repayment request processed.",
|
|
820
|
+
raw: o
|
|
821
|
+
});
|
|
822
|
+
} catch (e) {
|
|
823
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
824
|
+
return jsonError(msg, "Verify loan_id, ACE balance, and VALEO_ACE_API_KEY; retry repay_credit.");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/tools/spending-history.ts
|
|
829
|
+
function receiptToRow(r) {
|
|
830
|
+
return {
|
|
831
|
+
timestamp: r.timestamp,
|
|
832
|
+
endpoint: r.endpoint,
|
|
833
|
+
amount: String(r.amount),
|
|
834
|
+
currency: r.currency,
|
|
835
|
+
chain: r.chain,
|
|
836
|
+
status: r.status,
|
|
837
|
+
...r.txHash ? { receipt_hash: r.txHash } : {}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function normalizeSentinelPayload(data) {
|
|
841
|
+
if (!data || typeof data !== "object") {
|
|
842
|
+
return { transactions: [], extra: { raw: data } };
|
|
843
|
+
}
|
|
844
|
+
const o = data;
|
|
845
|
+
const txs = o.transactions ?? o.data ?? o.items ?? o.results;
|
|
846
|
+
if (Array.isArray(txs)) {
|
|
847
|
+
return {
|
|
848
|
+
transactions: txs.map((t) => typeof t === "object" && t !== null ? t : { value: t }),
|
|
849
|
+
extra: { ...o, transactions: void 0, data: void 0, items: void 0, results: void 0 }
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return { transactions: [], extra: o };
|
|
853
|
+
}
|
|
854
|
+
async function spendingHistoryTool(ctx, input) {
|
|
855
|
+
const totalSession = ctx.receipts.getTotalSpent();
|
|
856
|
+
const totalToday = ctx.receipts.getTotalSpentToday();
|
|
857
|
+
if (ctx.sentinelClient) {
|
|
858
|
+
try {
|
|
859
|
+
const raw = await ctx.sentinelClient.getTransactions({
|
|
860
|
+
limit: input.limit,
|
|
861
|
+
since: input.since,
|
|
862
|
+
endpoint: input.endpoint
|
|
863
|
+
});
|
|
864
|
+
const { transactions, extra } = normalizeSentinelPayload(raw);
|
|
865
|
+
const mapped = transactions.map((t) => ({
|
|
866
|
+
timestamp: String(t.timestamp ?? t.created_at ?? t.time ?? ""),
|
|
867
|
+
endpoint: String(t.endpoint ?? t.url ?? t.resource ?? ""),
|
|
868
|
+
amount: String(t.amount ?? t.value ?? ""),
|
|
869
|
+
currency: String(t.currency ?? "USDC"),
|
|
870
|
+
chain: String(t.chain ?? t.network ?? ""),
|
|
871
|
+
status: String(t.status ?? "settled"),
|
|
872
|
+
...t.receipt_hash || t.receiptHash ? { receipt_hash: String(t.receipt_hash ?? t.receiptHash) } : {}
|
|
873
|
+
}));
|
|
874
|
+
return jsonResult({
|
|
875
|
+
source: "sentinel",
|
|
876
|
+
total_spent_session: totalSession.toFixed(2),
|
|
877
|
+
total_spent_today: totalToday.toFixed(2),
|
|
878
|
+
transaction_count: mapped.length,
|
|
879
|
+
transactions: mapped,
|
|
880
|
+
sentinel_meta: extra
|
|
881
|
+
});
|
|
882
|
+
} catch (e) {
|
|
883
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
884
|
+
return jsonError(
|
|
885
|
+
msg,
|
|
886
|
+
"Check VALEO_SENTINEL_API_KEY and network access. You can temporarily rely on session receipts without Sentinel."
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const local = ctx.receipts.getAll({
|
|
891
|
+
limit: input.limit,
|
|
892
|
+
since: input.since,
|
|
893
|
+
endpoint: input.endpoint
|
|
894
|
+
});
|
|
895
|
+
return jsonResult({
|
|
896
|
+
source: "session",
|
|
897
|
+
note: "Showing session-only data. Configure VALEO_SENTINEL_API_KEY for full historical audit trail.",
|
|
898
|
+
total_spent_session: totalSession.toFixed(2),
|
|
899
|
+
total_spent_today: totalToday.toFixed(2),
|
|
900
|
+
transaction_count: local.length,
|
|
901
|
+
transactions: local.map(receiptToRow)
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/tools/discover-apis.ts
|
|
906
|
+
async function discoverApisTool(ctx, input) {
|
|
907
|
+
try {
|
|
908
|
+
const data = await ctx.four02mdClient.search({
|
|
909
|
+
query: input.query,
|
|
910
|
+
category: input.category,
|
|
911
|
+
chain: input.chain,
|
|
912
|
+
maxPrice: input.max_price
|
|
913
|
+
});
|
|
914
|
+
let total = 0;
|
|
915
|
+
if (data && typeof data === "object" && "results" in data) {
|
|
916
|
+
const r = data.results;
|
|
917
|
+
if (Array.isArray(r)) total = r.length;
|
|
918
|
+
}
|
|
919
|
+
return jsonResult({
|
|
920
|
+
...typeof data === "object" && data !== null ? data : { results: data },
|
|
921
|
+
total,
|
|
922
|
+
tip: "Use the pay tool with any endpoint URL to make a paid call."
|
|
923
|
+
});
|
|
924
|
+
} catch (e) {
|
|
925
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
926
|
+
return jsonError(msg, "Verify 402.md registry availability and VALEO_402MD_BASE_URL; retry discover_apis.");
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/tools/set-budget.ts
|
|
931
|
+
function setBudgetTool(ctx, input) {
|
|
932
|
+
ctx.budget.updateConfig({
|
|
933
|
+
...input.max_per_call !== void 0 ? { maxPerCall: input.max_per_call } : {},
|
|
934
|
+
...input.daily_limit !== void 0 ? { dailyLimit: input.daily_limit } : {},
|
|
935
|
+
...input.session_limit !== void 0 ? { sessionLimit: input.session_limit } : {},
|
|
936
|
+
...input.blocked_endpoints !== void 0 ? { blockedEndpoints: input.blocked_endpoints } : {},
|
|
937
|
+
...input.allowed_endpoints !== void 0 ? { allowedEndpoints: input.allowed_endpoints } : {}
|
|
938
|
+
});
|
|
939
|
+
const st = ctx.budget.getStatus();
|
|
940
|
+
return jsonResult({
|
|
941
|
+
updated: true,
|
|
942
|
+
budget: {
|
|
943
|
+
max_per_call: st.config.maxPerCall,
|
|
944
|
+
daily_limit: st.config.dailyLimit,
|
|
945
|
+
session_limit: st.config.sessionLimit,
|
|
946
|
+
blocked_endpoints: st.config.blockedEndpoints,
|
|
947
|
+
allowed_endpoints: st.config.allowedEndpoints
|
|
948
|
+
},
|
|
949
|
+
current_spending: {
|
|
950
|
+
session: st.spending.session,
|
|
951
|
+
today: st.spending.today,
|
|
952
|
+
remaining_session: st.spending.remainingSession,
|
|
953
|
+
remaining_daily: st.spending.remainingDaily
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/tools/estimate-cost.ts
|
|
959
|
+
function parseX402FromBody2(body) {
|
|
960
|
+
if (!body || typeof body !== "object") return null;
|
|
961
|
+
const o = body;
|
|
962
|
+
const accepts = o.accepts;
|
|
963
|
+
if (!Array.isArray(accepts) || accepts.length === 0) return null;
|
|
964
|
+
const first = accepts[0];
|
|
965
|
+
const raw = first.maxAmountRequired;
|
|
966
|
+
if (raw === void 0 || raw === null) return null;
|
|
967
|
+
let maxAmountAtomic;
|
|
968
|
+
try {
|
|
969
|
+
maxAmountAtomic = BigInt(String(raw));
|
|
970
|
+
} catch {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
const priceUsd = Number(maxAmountAtomic) / 1e6;
|
|
974
|
+
const payTo = typeof first.payTo === "string" ? first.payTo : "";
|
|
975
|
+
let description = "";
|
|
976
|
+
if (typeof first.extra === "object" && first.extra !== null && "description" in first.extra) {
|
|
977
|
+
description = String(first.extra.description ?? "");
|
|
978
|
+
} else if (typeof first.description === "string") {
|
|
979
|
+
description = first.description;
|
|
980
|
+
}
|
|
981
|
+
const network = typeof first.network === "string" ? first.network : "";
|
|
982
|
+
const asset = typeof first.asset === "string" ? first.asset : "USDC";
|
|
983
|
+
return {
|
|
984
|
+
priceUsd,
|
|
985
|
+
payTo,
|
|
986
|
+
description,
|
|
987
|
+
chainLabel: network,
|
|
988
|
+
currency: asset.length > 20 ? "USDC" : asset
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
async function estimateCostTool(ctx, input) {
|
|
992
|
+
const { url } = input;
|
|
993
|
+
let res;
|
|
994
|
+
try {
|
|
995
|
+
res = await fetch(url, {
|
|
996
|
+
method: "GET",
|
|
997
|
+
signal: AbortSignal.timeout(15e3),
|
|
998
|
+
headers: { Accept: "application/json, text/plain, */*" }
|
|
999
|
+
});
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1002
|
+
return jsonError(
|
|
1003
|
+
`Probe failed for ${url}: ${msg}`,
|
|
1004
|
+
"Check the URL, network, and TLS. Retry estimate_cost or use pay after confirming connectivity."
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
if (res.status === 200) {
|
|
1008
|
+
const budgetCheck = ctx.budget.canSpend(0, url);
|
|
1009
|
+
return jsonResult({
|
|
1010
|
+
url,
|
|
1011
|
+
requires_payment: false,
|
|
1012
|
+
within_budget: budgetCheck.allowed,
|
|
1013
|
+
wallet_can_afford: true,
|
|
1014
|
+
wallet_balance: await readUsdcBalance(ctx).catch(() => "unknown"),
|
|
1015
|
+
note: "Endpoint returned 200 without requesting payment (free or already authorized)."
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
if (res.status === 402) {
|
|
1019
|
+
let bodyJson;
|
|
1020
|
+
try {
|
|
1021
|
+
bodyJson = await res.json();
|
|
1022
|
+
} catch {
|
|
1023
|
+
return jsonError(
|
|
1024
|
+
"HTTP 402 but body is not valid JSON.",
|
|
1025
|
+
"Inspect the endpoint manually or contact the provider for x402 pricing format."
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
const pricing = parseX402FromBody2(bodyJson);
|
|
1029
|
+
if (!pricing) {
|
|
1030
|
+
return jsonError(
|
|
1031
|
+
"Could not parse x402 pricing from 402 response.",
|
|
1032
|
+
"Use pay with a small budget to test, or inspect the raw 402 JSON from the server."
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
const spend = ctx.budget.canSpend(pricing.priceUsd, url);
|
|
1036
|
+
let walletBalanceStr = "0";
|
|
1037
|
+
let walletCanAfford = false;
|
|
1038
|
+
try {
|
|
1039
|
+
walletBalanceStr = await readUsdcBalance(ctx);
|
|
1040
|
+
const bal = parseFloat(walletBalanceStr);
|
|
1041
|
+
walletCanAfford = !Number.isNaN(bal) && bal + 1e-9 >= pricing.priceUsd;
|
|
1042
|
+
} catch {
|
|
1043
|
+
walletCanAfford = false;
|
|
1044
|
+
}
|
|
1045
|
+
return jsonResult({
|
|
1046
|
+
url,
|
|
1047
|
+
requires_payment: true,
|
|
1048
|
+
price: `$${pricing.priceUsd.toFixed(6)}`,
|
|
1049
|
+
currency: pricing.currency,
|
|
1050
|
+
chain: pricing.chainLabel || ctx.config.chain,
|
|
1051
|
+
pay_to: pricing.payTo,
|
|
1052
|
+
description: pricing.description,
|
|
1053
|
+
within_budget: spend.allowed,
|
|
1054
|
+
wallet_can_afford: walletCanAfford,
|
|
1055
|
+
wallet_balance: walletBalanceStr,
|
|
1056
|
+
...spend.allowed ? {} : { budget_reason: spend.reason }
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const snippet = (await res.text()).slice(0, 400);
|
|
1060
|
+
return jsonResult({
|
|
1061
|
+
url,
|
|
1062
|
+
requires_payment: false,
|
|
1063
|
+
http_status: res.status,
|
|
1064
|
+
body_snippet: snippet,
|
|
1065
|
+
within_budget: ctx.budget.canSpend(0, url).allowed,
|
|
1066
|
+
wallet_can_afford: true,
|
|
1067
|
+
wallet_balance: await readUsdcBalance(ctx).catch(() => "unknown"),
|
|
1068
|
+
note: "Non-402 response; endpoint may require auth, be misconfigured, or be unavailable."
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
async function readUsdcBalance(ctx) {
|
|
1072
|
+
return ctx.x402Client.getUsdcBalance();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/server.ts
|
|
1076
|
+
function readPackageVersion() {
|
|
1077
|
+
const here = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
|
|
1078
|
+
const pkgPath = join(here, "..", "package.json");
|
|
1079
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
1080
|
+
return JSON.parse(raw).version;
|
|
1081
|
+
}
|
|
1082
|
+
var pkgVersion = readPackageVersion();
|
|
1083
|
+
async function createAndRunMcpServer(ctx) {
|
|
1084
|
+
const server = new McpServer({
|
|
1085
|
+
name: "valeo",
|
|
1086
|
+
version: pkgVersion
|
|
1087
|
+
});
|
|
1088
|
+
server.tool(
|
|
1089
|
+
"pay",
|
|
1090
|
+
"Call any x402-enabled paid API endpoint. Automatically handles payment negotiation, signing, and settlement.",
|
|
1091
|
+
{
|
|
1092
|
+
url: z.string().describe("The full URL of the x402 API endpoint"),
|
|
1093
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).default("GET"),
|
|
1094
|
+
headers: z.record(z.string()).optional(),
|
|
1095
|
+
body: z.string().optional()
|
|
1096
|
+
},
|
|
1097
|
+
async (args) => payTool(ctx, args)
|
|
1098
|
+
);
|
|
1099
|
+
server.tool(
|
|
1100
|
+
"wallet_balance",
|
|
1101
|
+
"Check the current USDC balance of the agent's wallet on the configured chain (Base or Solana). Also shows ETH or SOL for gas.",
|
|
1102
|
+
{
|
|
1103
|
+
chain: z.enum(["base", "base-sepolia", "solana", "solana-devnet"]).optional()
|
|
1104
|
+
},
|
|
1105
|
+
async (args) => walletBalanceTool(ctx, args)
|
|
1106
|
+
);
|
|
1107
|
+
server.tool(
|
|
1108
|
+
"get_credit",
|
|
1109
|
+
"Borrow USDC from Valeo ACE when wallet balance is insufficient. Requires VALEO_ACE_API_KEY.",
|
|
1110
|
+
{
|
|
1111
|
+
amount: z.number().describe("Amount of USDC to borrow"),
|
|
1112
|
+
purpose: z.string().optional().describe("Brief description of what the credit is for")
|
|
1113
|
+
},
|
|
1114
|
+
async (args) => getCreditTool(ctx, args)
|
|
1115
|
+
);
|
|
1116
|
+
server.tool(
|
|
1117
|
+
"repay_credit",
|
|
1118
|
+
"Manually repay an outstanding ACE credit line.",
|
|
1119
|
+
{
|
|
1120
|
+
loan_id: z.string().describe("The loan ID to repay"),
|
|
1121
|
+
amount: z.number().optional().describe("Amount to repay. Omit for full balance.")
|
|
1122
|
+
},
|
|
1123
|
+
async (args) => repayCreditTool(ctx, args)
|
|
1124
|
+
);
|
|
1125
|
+
server.tool(
|
|
1126
|
+
"spending_history",
|
|
1127
|
+
"View recent spending history via Sentinel (if configured) or session receipts.",
|
|
1128
|
+
{
|
|
1129
|
+
limit: z.number().default(20).describe("Number of recent transactions"),
|
|
1130
|
+
since: z.string().optional().describe("ISO timestamp \u2014 only show transactions after this time"),
|
|
1131
|
+
endpoint: z.string().optional().describe("Filter by endpoint URL substring")
|
|
1132
|
+
},
|
|
1133
|
+
async (args) => spendingHistoryTool(ctx, args)
|
|
1134
|
+
);
|
|
1135
|
+
server.tool(
|
|
1136
|
+
"discover_apis",
|
|
1137
|
+
"Search the 402.md registry for x402-enabled APIs by keyword, category, or chain.",
|
|
1138
|
+
{
|
|
1139
|
+
query: z.string().optional(),
|
|
1140
|
+
category: z.string().optional(),
|
|
1141
|
+
max_price: z.number().optional(),
|
|
1142
|
+
chain: z.enum(["base", "solana", "any"]).default("any")
|
|
1143
|
+
},
|
|
1144
|
+
async (args) => discoverApisTool(ctx, args)
|
|
1145
|
+
);
|
|
1146
|
+
server.tool(
|
|
1147
|
+
"set_budget",
|
|
1148
|
+
"Configure per-call, daily, and session spending limits and optional endpoint allow/block lists.",
|
|
1149
|
+
{
|
|
1150
|
+
max_per_call: z.number().optional(),
|
|
1151
|
+
daily_limit: z.number().optional(),
|
|
1152
|
+
session_limit: z.number().optional(),
|
|
1153
|
+
blocked_endpoints: z.array(z.string()).optional(),
|
|
1154
|
+
allowed_endpoints: z.array(z.string()).optional()
|
|
1155
|
+
},
|
|
1156
|
+
async (args) => setBudgetTool(ctx, args)
|
|
1157
|
+
);
|
|
1158
|
+
server.tool(
|
|
1159
|
+
"estimate_cost",
|
|
1160
|
+
"Probe an x402 endpoint for pricing without paying (GET, no payment header).",
|
|
1161
|
+
{
|
|
1162
|
+
url: z.string().describe("The x402 endpoint URL to probe")
|
|
1163
|
+
},
|
|
1164
|
+
async (args) => estimateCostTool(ctx, args)
|
|
1165
|
+
);
|
|
1166
|
+
const transport = new StdioServerTransport();
|
|
1167
|
+
await server.connect(transport);
|
|
1168
|
+
console.error("\u2713 Valeo MCP server running");
|
|
1169
|
+
console.error(` Chain: ${ctx.config.chain}`);
|
|
1170
|
+
console.error(` Wallet: ${ctx.x402Client.getAddress()}`);
|
|
1171
|
+
console.error(` ACE: ${ctx.aceClient ? "enabled" : "not configured"}`);
|
|
1172
|
+
console.error(` Sentinel: ${ctx.sentinelClient ? "enabled" : "not configured"}`);
|
|
1173
|
+
console.error(` Budget: $${ctx.config.defaultSessionLimit}/session, $${ctx.config.defaultDailyLimit}/day`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/index.ts
|
|
1177
|
+
async function main() {
|
|
1178
|
+
const config = loadConfig();
|
|
1179
|
+
const budget = new BudgetManager(config);
|
|
1180
|
+
const receipts = new ReceiptStore();
|
|
1181
|
+
const x402Client = config.chain.startsWith("solana") ? new X402SolanaClient(
|
|
1182
|
+
config.privateKey,
|
|
1183
|
+
config.chain,
|
|
1184
|
+
process.env.VALEO_SOLANA_RPC_URL
|
|
1185
|
+
) : new X402BaseClient(config.privateKey, config.chain);
|
|
1186
|
+
const aceClient = config.aceApiKey ? new ACEClient(config.aceBaseUrl, config.aceApiKey) : null;
|
|
1187
|
+
const sentinelClient = config.sentinelApiKey ? new SentinelClient(config.sentinelBaseUrl, config.sentinelApiKey) : null;
|
|
1188
|
+
const four02mdClient = new Four02mdClient(config.four02mdBaseUrl);
|
|
1189
|
+
const ctx = {
|
|
1190
|
+
config,
|
|
1191
|
+
budget,
|
|
1192
|
+
receipts,
|
|
1193
|
+
x402Client,
|
|
1194
|
+
aceClient,
|
|
1195
|
+
sentinelClient,
|
|
1196
|
+
four02mdClient
|
|
1197
|
+
};
|
|
1198
|
+
await createAndRunMcpServer(ctx);
|
|
1199
|
+
}
|
|
1200
|
+
process.on("uncaughtException", (err) => {
|
|
1201
|
+
console.error("Uncaught exception:", err);
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
});
|
|
1204
|
+
process.on("unhandledRejection", (reason) => {
|
|
1205
|
+
console.error("Unhandled rejection:", reason);
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
});
|
|
1208
|
+
main().catch((err) => {
|
|
1209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1210
|
+
console.error("Fatal startup error:", msg);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
});
|
|
1213
|
+
export {
|
|
1214
|
+
loadConfig
|
|
1215
|
+
};
|
|
1216
|
+
//# sourceMappingURL=index.mjs.map
|