agentlili 0.1.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/README.md +533 -0
- package/dist/chunk-AAYS2L5P.js +946 -0
- package/dist/chunk-AAYS2L5P.js.map +1 -0
- package/dist/cli.js +803 -0
- package/dist/cli.js.map +1 -0
- package/dist/mcp-server.js +1468 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +111 -0
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import {
|
|
4
|
+
WalletManager,
|
|
5
|
+
prisma
|
|
6
|
+
} from "./chunk-AAYS2L5P.js";
|
|
7
|
+
|
|
8
|
+
// src/mcp/server.ts
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import {
|
|
12
|
+
CallToolRequestSchema,
|
|
13
|
+
ListToolsRequestSchema,
|
|
14
|
+
ListPromptsRequestSchema,
|
|
15
|
+
GetPromptRequestSchema
|
|
16
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
|
|
18
|
+
// src/lib/audit-log.ts
|
|
19
|
+
import crypto from "crypto";
|
|
20
|
+
function hmacKey() {
|
|
21
|
+
return process.env.WALLET_ENCRYPTION_KEY ?? "audit-fallback-key";
|
|
22
|
+
}
|
|
23
|
+
function computeHmac(prevHash, data) {
|
|
24
|
+
return crypto.createHmac("sha256", hmacKey()).update(prevHash + data).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
async function readAuditLog(opts = {}) {
|
|
27
|
+
const where = {};
|
|
28
|
+
if (opts.action) where.action = opts.action;
|
|
29
|
+
if (opts.walletId) where.walletId = opts.walletId;
|
|
30
|
+
if (opts.agentId) where.agentId = opts.agentId;
|
|
31
|
+
if (opts.after || opts.before) {
|
|
32
|
+
const timestampFilter = {};
|
|
33
|
+
if (opts.after) timestampFilter.gt = new Date(opts.after);
|
|
34
|
+
if (opts.before) timestampFilter.lt = new Date(opts.before);
|
|
35
|
+
where.timestamp = timestampFilter;
|
|
36
|
+
}
|
|
37
|
+
const rows = await prisma.auditLog.findMany({
|
|
38
|
+
where,
|
|
39
|
+
orderBy: { seq: "desc" },
|
|
40
|
+
skip: opts.offset ?? 0,
|
|
41
|
+
take: opts.limit ?? 100
|
|
42
|
+
});
|
|
43
|
+
return rows.map((row) => ({
|
|
44
|
+
timestamp: row.timestamp.toISOString(),
|
|
45
|
+
seq: row.seq,
|
|
46
|
+
action: row.action,
|
|
47
|
+
walletId: row.walletId ?? void 0,
|
|
48
|
+
agentId: row.agentId ?? void 0,
|
|
49
|
+
ip: row.ip ?? void 0,
|
|
50
|
+
outcome: row.outcome,
|
|
51
|
+
details: row.details ?? void 0,
|
|
52
|
+
integrity: row.integrity
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
async function verifyAuditIntegrity() {
|
|
56
|
+
const rows = await prisma.auditLog.findMany({
|
|
57
|
+
orderBy: { seq: "asc" }
|
|
58
|
+
});
|
|
59
|
+
if (rows.length === 0) return { valid: true, total: 0 };
|
|
60
|
+
let prevHash = "genesis";
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const entry = {
|
|
63
|
+
timestamp: row.timestamp.toISOString(),
|
|
64
|
+
seq: row.seq,
|
|
65
|
+
action: row.action,
|
|
66
|
+
walletId: row.walletId ?? void 0,
|
|
67
|
+
agentId: row.agentId ?? void 0,
|
|
68
|
+
ip: row.ip ?? void 0,
|
|
69
|
+
outcome: row.outcome,
|
|
70
|
+
details: row.details ?? void 0
|
|
71
|
+
};
|
|
72
|
+
const rawForHmac = {
|
|
73
|
+
timestamp: entry.timestamp,
|
|
74
|
+
action: entry.action,
|
|
75
|
+
outcome: entry.outcome,
|
|
76
|
+
seq: entry.seq
|
|
77
|
+
};
|
|
78
|
+
if (entry.walletId !== void 0) rawForHmac.walletId = entry.walletId;
|
|
79
|
+
if (entry.agentId !== void 0) rawForHmac.agentId = entry.agentId;
|
|
80
|
+
if (entry.ip !== void 0) rawForHmac.ip = entry.ip;
|
|
81
|
+
if (entry.details !== void 0) rawForHmac.details = entry.details;
|
|
82
|
+
const entryData = JSON.stringify(rawForHmac);
|
|
83
|
+
const expected = computeHmac(prevHash, entryData);
|
|
84
|
+
if (expected !== row.integrity) {
|
|
85
|
+
return { valid: false, total: rows.length, brokenAt: row.seq };
|
|
86
|
+
}
|
|
87
|
+
prevHash = row.integrity;
|
|
88
|
+
}
|
|
89
|
+
return { valid: true, total: rows.length };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/lib/spending-limits.ts
|
|
93
|
+
var DEFAULT_LIMITS = {
|
|
94
|
+
maxPerTx: 1,
|
|
95
|
+
maxPerDay: 5,
|
|
96
|
+
enabled: true
|
|
97
|
+
};
|
|
98
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
99
|
+
async function getSpendingLimits(walletId) {
|
|
100
|
+
const row = await prisma.spendingLimits.findUnique({
|
|
101
|
+
where: { walletId }
|
|
102
|
+
});
|
|
103
|
+
if (!row) {
|
|
104
|
+
return { ...DEFAULT_LIMITS };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
maxPerTx: row.maxPerTx,
|
|
108
|
+
maxPerDay: row.maxPerDay,
|
|
109
|
+
enabled: row.enabled
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function setSpendingLimits(walletId, config) {
|
|
113
|
+
const current = await getSpendingLimits(walletId);
|
|
114
|
+
const updated = {
|
|
115
|
+
maxPerTx: typeof config.maxPerTx === "number" ? Math.max(0, config.maxPerTx) : current.maxPerTx,
|
|
116
|
+
maxPerDay: typeof config.maxPerDay === "number" ? Math.max(0, config.maxPerDay) : current.maxPerDay,
|
|
117
|
+
enabled: typeof config.enabled === "boolean" ? config.enabled : current.enabled
|
|
118
|
+
};
|
|
119
|
+
await prisma.spendingLimits.upsert({
|
|
120
|
+
where: { walletId },
|
|
121
|
+
create: {
|
|
122
|
+
walletId,
|
|
123
|
+
maxPerTx: updated.maxPerTx,
|
|
124
|
+
maxPerDay: updated.maxPerDay,
|
|
125
|
+
enabled: updated.enabled
|
|
126
|
+
},
|
|
127
|
+
update: {
|
|
128
|
+
maxPerTx: updated.maxPerTx,
|
|
129
|
+
maxPerDay: updated.maxPerDay,
|
|
130
|
+
enabled: updated.enabled
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return updated;
|
|
134
|
+
}
|
|
135
|
+
async function getDailySpending(walletId) {
|
|
136
|
+
const cutoff = new Date(Date.now() - DAY_MS);
|
|
137
|
+
const result = await prisma.spendingRecord.aggregate({
|
|
138
|
+
where: {
|
|
139
|
+
walletId,
|
|
140
|
+
timestamp: { gte: cutoff }
|
|
141
|
+
},
|
|
142
|
+
_sum: { amount: true }
|
|
143
|
+
});
|
|
144
|
+
const total = result._sum.amount ?? 0;
|
|
145
|
+
return Math.round(total * 1e9) / 1e9;
|
|
146
|
+
}
|
|
147
|
+
async function getSpendingSummary(walletId) {
|
|
148
|
+
const limits = await getSpendingLimits(walletId);
|
|
149
|
+
const dailySpent = await getDailySpending(walletId);
|
|
150
|
+
const dailyRemaining = limits.enabled && limits.maxPerDay > 0 ? Math.max(0, limits.maxPerDay - dailySpent) : null;
|
|
151
|
+
return { limits, dailySpent, dailyRemaining };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/lib/agent-lifecycle.ts
|
|
155
|
+
var VALID_LIFECYCLE_STATES = [
|
|
156
|
+
"active",
|
|
157
|
+
"paused",
|
|
158
|
+
"terminated"
|
|
159
|
+
];
|
|
160
|
+
var MAX_TRANSITIONS = 10;
|
|
161
|
+
var VALID_TRANSITIONS = {
|
|
162
|
+
active: ["paused", "terminated"],
|
|
163
|
+
paused: ["active", "terminated"],
|
|
164
|
+
terminated: []
|
|
165
|
+
};
|
|
166
|
+
async function getAgentLifecycle(walletId) {
|
|
167
|
+
const row = await prisma.agentLifecycle.findUnique({
|
|
168
|
+
where: { walletId }
|
|
169
|
+
});
|
|
170
|
+
if (!row) {
|
|
171
|
+
return {
|
|
172
|
+
state: "active",
|
|
173
|
+
updatedAt: "",
|
|
174
|
+
transitions: []
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const state = VALID_LIFECYCLE_STATES.includes(row.state) ? row.state : "active";
|
|
178
|
+
return {
|
|
179
|
+
state,
|
|
180
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
181
|
+
reason: row.reason ?? void 0,
|
|
182
|
+
transitions: row.transitions ?? []
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function validateTransition(from, to) {
|
|
186
|
+
if (from === to) {
|
|
187
|
+
return `Agent is already in "${to}" state`;
|
|
188
|
+
}
|
|
189
|
+
const allowed = VALID_TRANSITIONS[from];
|
|
190
|
+
if (!allowed.includes(to)) {
|
|
191
|
+
return `Cannot transition from "${from}" to "${to}". Terminated agents cannot be restarted.`;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
async function setAgentLifecycle(walletId, newState, reason) {
|
|
196
|
+
if (!VALID_LIFECYCLE_STATES.includes(newState)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Invalid lifecycle state: ${newState}. Must be one of: ${VALID_LIFECYCLE_STATES.join(", ")}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const current = await getAgentLifecycle(walletId);
|
|
202
|
+
const error = validateTransition(current.state, newState);
|
|
203
|
+
if (error) {
|
|
204
|
+
throw new Error(error);
|
|
205
|
+
}
|
|
206
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
207
|
+
const transition = {
|
|
208
|
+
from: current.state,
|
|
209
|
+
to: newState,
|
|
210
|
+
timestamp: now,
|
|
211
|
+
reason
|
|
212
|
+
};
|
|
213
|
+
const transitions = [...current.transitions, transition].slice(
|
|
214
|
+
-MAX_TRANSITIONS
|
|
215
|
+
);
|
|
216
|
+
await prisma.agentLifecycle.upsert({
|
|
217
|
+
where: { walletId },
|
|
218
|
+
update: {
|
|
219
|
+
state: newState,
|
|
220
|
+
reason: reason ?? null,
|
|
221
|
+
transitions
|
|
222
|
+
},
|
|
223
|
+
create: {
|
|
224
|
+
walletId,
|
|
225
|
+
state: newState,
|
|
226
|
+
reason: reason ?? null,
|
|
227
|
+
transitions
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
state: newState,
|
|
232
|
+
updatedAt: now,
|
|
233
|
+
reason,
|
|
234
|
+
transitions
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function isAgentExecutable(walletId) {
|
|
238
|
+
const lifecycle = await getAgentLifecycle(walletId);
|
|
239
|
+
if (lifecycle.state === "active") {
|
|
240
|
+
return { allowed: true, state: "active" };
|
|
241
|
+
}
|
|
242
|
+
const reason = lifecycle.state === "paused" ? "Agent is paused. Resume it to continue execution." : "Agent is terminated. Create a new agent to continue.";
|
|
243
|
+
return { allowed: false, state: lifecycle.state, reason };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/lib/tx-log.ts
|
|
247
|
+
async function readTxLog(walletId, limit = 50) {
|
|
248
|
+
const rows = await prisma.txLog.findMany({
|
|
249
|
+
where: { walletId },
|
|
250
|
+
orderBy: { timestamp: "desc" },
|
|
251
|
+
take: limit
|
|
252
|
+
});
|
|
253
|
+
return rows.map((row) => ({
|
|
254
|
+
timestamp: row.timestamp.toISOString(),
|
|
255
|
+
walletId: row.walletId,
|
|
256
|
+
toolName: row.toolName,
|
|
257
|
+
args: row.args,
|
|
258
|
+
result: row.result,
|
|
259
|
+
success: row.success
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/lib/agent-strategy.ts
|
|
264
|
+
var STRATEGY_PRESETS = {
|
|
265
|
+
conservative: {
|
|
266
|
+
id: "conservative",
|
|
267
|
+
name: "Conservative",
|
|
268
|
+
description: "Low risk \u2014 small positions, only well-known protocols, no leverage. Prioritizes capital preservation.",
|
|
269
|
+
maxPositionSize: 0.1,
|
|
270
|
+
slippageBps: 100,
|
|
271
|
+
allowLeverage: false,
|
|
272
|
+
allowUnverifiedTokens: false,
|
|
273
|
+
maxOpenPositions: 2,
|
|
274
|
+
protocolGuidance: "Only use well-established protocols: Jupiter (swaps), Marinade/Jito (liquid staking). Avoid Drift, Adrena, and leveraged products.",
|
|
275
|
+
promptGuidance: [
|
|
276
|
+
"You are operating in CONSERVATIVE mode. Capital preservation is your top priority.",
|
|
277
|
+
"- Maximum position size: 10% of wallet balance per trade",
|
|
278
|
+
"- Use tight slippage (1% / 100 bps) \u2014 reject trades with higher slippage",
|
|
279
|
+
"- Only interact with well-known, audited protocols (Jupiter, Orca, Marinade, Jito)",
|
|
280
|
+
"- Do NOT use leverage, perpetuals, or margin trading",
|
|
281
|
+
"- Do NOT trade unverified or low-liquidity tokens",
|
|
282
|
+
"- Maximum 2 open positions at a time",
|
|
283
|
+
"- Always check balance before and after trades",
|
|
284
|
+
"- Prefer liquid staking (mSOL, jitoSOL) over risky DeFi yields",
|
|
285
|
+
"- If unsure about a trade, err on the side of NOT executing it"
|
|
286
|
+
].join("\n")
|
|
287
|
+
},
|
|
288
|
+
balanced: {
|
|
289
|
+
id: "balanced",
|
|
290
|
+
name: "Balanced",
|
|
291
|
+
description: "Moderate risk \u2014 standard positions, most protocols allowed, moderate slippage tolerance. Good all-around strategy.",
|
|
292
|
+
maxPositionSize: 0.25,
|
|
293
|
+
slippageBps: 300,
|
|
294
|
+
allowLeverage: false,
|
|
295
|
+
allowUnverifiedTokens: false,
|
|
296
|
+
maxOpenPositions: 5,
|
|
297
|
+
protocolGuidance: "Use established protocols freely: Jupiter, Orca, Raydium, Meteora for swaps/LP, Marinade/Jito for staking, Lulo for lending. Avoid leverage protocols.",
|
|
298
|
+
promptGuidance: [
|
|
299
|
+
"You are operating in BALANCED mode. Seek reasonable returns while managing risk.",
|
|
300
|
+
"- Maximum position size: 25% of wallet balance per trade",
|
|
301
|
+
"- Standard slippage tolerance (3% / 300 bps)",
|
|
302
|
+
"- Use established protocols: Jupiter, Orca, Raydium, Meteora, Marinade, Jito, Lulo",
|
|
303
|
+
"- Do NOT use leverage or perpetual trading",
|
|
304
|
+
"- Only trade tokens with reasonable liquidity",
|
|
305
|
+
"- Maximum 5 open positions at a time",
|
|
306
|
+
"- Diversify across different protocols and strategies when possible",
|
|
307
|
+
"- Provide liquidity on concentrated AMMs only if the range is reasonable",
|
|
308
|
+
"- Monitor position health and rebalance if needed"
|
|
309
|
+
].join("\n")
|
|
310
|
+
},
|
|
311
|
+
aggressive: {
|
|
312
|
+
id: "aggressive",
|
|
313
|
+
name: "Aggressive",
|
|
314
|
+
description: "High risk \u2014 large positions, all protocols including leverage, wider slippage. Maximizes upside potential.",
|
|
315
|
+
maxPositionSize: 0.5,
|
|
316
|
+
slippageBps: 500,
|
|
317
|
+
allowLeverage: true,
|
|
318
|
+
allowUnverifiedTokens: true,
|
|
319
|
+
maxOpenPositions: 10,
|
|
320
|
+
protocolGuidance: "All protocols available: Jupiter, Orca, Raydium, Meteora, Drift, Adrena, Lulo, and experimental protocols. Leverage and perps are allowed.",
|
|
321
|
+
promptGuidance: [
|
|
322
|
+
"You are operating in AGGRESSIVE mode. Maximize returns \u2014 higher risk is acceptable.",
|
|
323
|
+
"- Maximum position size: 50% of wallet balance per trade",
|
|
324
|
+
"- Wide slippage tolerance (5% / 500 bps) for fast execution",
|
|
325
|
+
"- All protocols available including Drift and Adrena for perpetuals",
|
|
326
|
+
"- Leverage and margin trading are ALLOWED \u2014 use up to 3x leverage max",
|
|
327
|
+
"- New and trending tokens are allowed if requested",
|
|
328
|
+
"- Maximum 10 open positions at a time",
|
|
329
|
+
"- Actively seek yield farming and liquidity provision opportunities",
|
|
330
|
+
"- Execute multi-step DeFi strategies: swap \u2192 LP \u2192 stake for compound yields",
|
|
331
|
+
"- Act decisively \u2014 speed of execution matters"
|
|
332
|
+
].join("\n")
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var VALID_STRATEGY_IDS = [
|
|
336
|
+
"conservative",
|
|
337
|
+
"balanced",
|
|
338
|
+
"aggressive"
|
|
339
|
+
];
|
|
340
|
+
var DEFAULT_STRATEGY_ID = "balanced";
|
|
341
|
+
async function getWalletStrategy(walletId) {
|
|
342
|
+
const row = await prisma.agentStrategyConfig.findUnique({
|
|
343
|
+
where: { walletId }
|
|
344
|
+
});
|
|
345
|
+
if (!row) {
|
|
346
|
+
return { strategyId: DEFAULT_STRATEGY_ID, updatedAt: "" };
|
|
347
|
+
}
|
|
348
|
+
const id = VALID_STRATEGY_IDS.includes(row.strategyId) ? row.strategyId : DEFAULT_STRATEGY_ID;
|
|
349
|
+
return {
|
|
350
|
+
strategyId: id,
|
|
351
|
+
updatedAt: row.updatedAt.toISOString()
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/lib/agent-monitor.ts
|
|
356
|
+
var EXECUTING_THRESHOLD_MS = 3e4;
|
|
357
|
+
function deriveStatus(lastAction) {
|
|
358
|
+
if (!lastAction) return "idle";
|
|
359
|
+
const elapsed = Date.now() - new Date(lastAction.timestamp).getTime();
|
|
360
|
+
if (elapsed < EXECUTING_THRESHOLD_MS) {
|
|
361
|
+
return lastAction.success ? "executing" : "error";
|
|
362
|
+
}
|
|
363
|
+
if (!lastAction.success) return "error";
|
|
364
|
+
return "idle";
|
|
365
|
+
}
|
|
366
|
+
function extractLastAction(entries) {
|
|
367
|
+
if (entries.length === 0) return null;
|
|
368
|
+
const latest = entries[0];
|
|
369
|
+
return {
|
|
370
|
+
toolName: latest.toolName,
|
|
371
|
+
success: latest.success,
|
|
372
|
+
timestamp: latest.timestamp
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function calcSuccessRate(entries) {
|
|
376
|
+
if (entries.length === 0) return 1;
|
|
377
|
+
const successes = entries.filter((e) => e.success).length;
|
|
378
|
+
return successes / entries.length;
|
|
379
|
+
}
|
|
380
|
+
async function buildAgentEntry(wallet, balance, txEntries) {
|
|
381
|
+
const lastAction = extractLastAction(txEntries);
|
|
382
|
+
const lifecycle = await getAgentLifecycle(wallet.id);
|
|
383
|
+
const status = lifecycle.state === "paused" ? "paused" : lifecycle.state === "terminated" ? "terminated" : deriveStatus(lastAction);
|
|
384
|
+
const strategy = await getWalletStrategy(wallet.id);
|
|
385
|
+
const spending = await getSpendingSummary(wallet.id);
|
|
386
|
+
return {
|
|
387
|
+
id: wallet.id,
|
|
388
|
+
publicKey: wallet.publicKey,
|
|
389
|
+
label: wallet.label || `Agent ${wallet.id}`,
|
|
390
|
+
createdAt: wallet.createdAt,
|
|
391
|
+
balance,
|
|
392
|
+
status,
|
|
393
|
+
lifecycleState: lifecycle.state,
|
|
394
|
+
strategy: strategy.strategyId,
|
|
395
|
+
lastAction,
|
|
396
|
+
spending: {
|
|
397
|
+
dailySpent: spending.dailySpent,
|
|
398
|
+
dailyLimit: spending.limits.maxPerDay,
|
|
399
|
+
dailyRemaining: spending.dailyRemaining,
|
|
400
|
+
enabled: spending.limits.enabled
|
|
401
|
+
},
|
|
402
|
+
totalActions: txEntries.length,
|
|
403
|
+
successRate: calcSuccessRate(txEntries)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function buildAgentMonitor(manager) {
|
|
407
|
+
const allWallets = await manager.listWallets();
|
|
408
|
+
const wallets = allWallets.filter((w) => w.isAgent !== false);
|
|
409
|
+
const entries = await Promise.all(
|
|
410
|
+
wallets.map(async (wallet) => {
|
|
411
|
+
let balance = 0;
|
|
412
|
+
try {
|
|
413
|
+
balance = await manager.getBalance(wallet.publicKey);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
const txEntries = await readTxLog(wallet.id, 50);
|
|
417
|
+
return buildAgentEntry(wallet, balance, txEntries);
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
const statusOrder = {
|
|
421
|
+
executing: 0,
|
|
422
|
+
error: 1,
|
|
423
|
+
idle: 2,
|
|
424
|
+
paused: 3,
|
|
425
|
+
terminated: 4
|
|
426
|
+
};
|
|
427
|
+
entries.sort((a, b) => {
|
|
428
|
+
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
429
|
+
if (statusDiff !== 0) return statusDiff;
|
|
430
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
agents: entries,
|
|
434
|
+
totalAgents: entries.length,
|
|
435
|
+
activeAgents: entries.filter((e) => e.status === "executing").length,
|
|
436
|
+
errorAgents: entries.filter((e) => e.status === "error").length,
|
|
437
|
+
pausedAgents: entries.filter((e) => e.status === "paused").length,
|
|
438
|
+
terminatedAgents: entries.filter((e) => e.status === "terminated").length,
|
|
439
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/lib/portfolio.ts
|
|
444
|
+
var SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
445
|
+
var JUPITER_PRICE_URL = "https://api.jup.ag/price/v2";
|
|
446
|
+
var PRICE_TIMEOUT_MS = 8e3;
|
|
447
|
+
var KNOWN_TOKENS = {
|
|
448
|
+
[SOL_MINT]: "SOL",
|
|
449
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
|
|
450
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: "USDT",
|
|
451
|
+
mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So: "mSOL",
|
|
452
|
+
"7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj": "stSOL",
|
|
453
|
+
DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263: "BONK",
|
|
454
|
+
JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN: "JUP"
|
|
455
|
+
};
|
|
456
|
+
function resolveSymbol(mint) {
|
|
457
|
+
return KNOWN_TOKENS[mint] ?? `${mint.slice(0, 4)}\u2026${mint.slice(-4)}`;
|
|
458
|
+
}
|
|
459
|
+
var ALLOC_COLORS = [
|
|
460
|
+
"#22c55e",
|
|
461
|
+
"#3b82f6",
|
|
462
|
+
"#f59e0b",
|
|
463
|
+
"#ef4444",
|
|
464
|
+
"#8b5cf6",
|
|
465
|
+
"#ec4899",
|
|
466
|
+
"#06b6d4",
|
|
467
|
+
"#f97316",
|
|
468
|
+
"#14b8a6",
|
|
469
|
+
"#6366f1"
|
|
470
|
+
];
|
|
471
|
+
async function fetchPrices(mints) {
|
|
472
|
+
const prices = /* @__PURE__ */ new Map();
|
|
473
|
+
if (mints.length === 0) return prices;
|
|
474
|
+
try {
|
|
475
|
+
const ids = mints.join(",");
|
|
476
|
+
const controller = new AbortController();
|
|
477
|
+
const timer = setTimeout(() => controller.abort(), PRICE_TIMEOUT_MS);
|
|
478
|
+
const res = await fetch(`${JUPITER_PRICE_URL}?ids=${ids}`, {
|
|
479
|
+
signal: controller.signal
|
|
480
|
+
});
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
if (!res.ok) return prices;
|
|
483
|
+
const json = await res.json();
|
|
484
|
+
for (const [mint, info] of Object.entries(json.data ?? {})) {
|
|
485
|
+
if (info.price) {
|
|
486
|
+
const p = parseFloat(info.price);
|
|
487
|
+
if (Number.isFinite(p) && p > 0) prices.set(mint, p);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
}
|
|
492
|
+
return prices;
|
|
493
|
+
}
|
|
494
|
+
async function buildPortfolio(manager) {
|
|
495
|
+
const wallets = await manager.listWallets();
|
|
496
|
+
const walletData = await Promise.all(
|
|
497
|
+
wallets.map(async (w) => {
|
|
498
|
+
const [solBalance, tokens] = await Promise.all([
|
|
499
|
+
manager.getBalance(w.publicKey).catch(() => 0),
|
|
500
|
+
manager.getTokenBalances(w.publicKey).catch(() => [])
|
|
501
|
+
]);
|
|
502
|
+
return { wallet: w, solBalance, tokens };
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
const allMints = /* @__PURE__ */ new Set([SOL_MINT]);
|
|
506
|
+
for (const { tokens } of walletData) {
|
|
507
|
+
for (const t of tokens) allMints.add(t.mint);
|
|
508
|
+
}
|
|
509
|
+
const prices = await fetchPrices([...allMints]);
|
|
510
|
+
const solPrice = prices.get(SOL_MINT) ?? null;
|
|
511
|
+
const walletHoldings = walletData.map(
|
|
512
|
+
({ wallet, solBalance, tokens }) => {
|
|
513
|
+
const solValueUsd = solPrice !== null ? solBalance * solPrice : null;
|
|
514
|
+
const tokenHoldings = tokens.map((t) => {
|
|
515
|
+
const priceUsd = prices.get(t.mint) ?? null;
|
|
516
|
+
const valueUsd = priceUsd !== null ? t.balance * priceUsd : null;
|
|
517
|
+
return {
|
|
518
|
+
mint: t.mint,
|
|
519
|
+
symbol: resolveSymbol(t.mint),
|
|
520
|
+
balance: t.balance,
|
|
521
|
+
decimals: t.decimals,
|
|
522
|
+
priceUsd,
|
|
523
|
+
valueUsd
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
const tokenValueSum = tokenHoldings.reduce(
|
|
527
|
+
(s, t) => s + (t.valueUsd ?? 0),
|
|
528
|
+
0
|
|
529
|
+
);
|
|
530
|
+
const totalValueUsd2 = solValueUsd !== null ? solValueUsd + tokenValueSum : null;
|
|
531
|
+
return {
|
|
532
|
+
walletId: wallet.id,
|
|
533
|
+
publicKey: wallet.publicKey,
|
|
534
|
+
label: wallet.label || `Agent ${wallet.id.slice(0, 6)}`,
|
|
535
|
+
solBalance,
|
|
536
|
+
solValueUsd,
|
|
537
|
+
tokens: tokenHoldings,
|
|
538
|
+
totalValueUsd: totalValueUsd2
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
const allocMap = /* @__PURE__ */ new Map();
|
|
543
|
+
const totalSolValue = walletHoldings.reduce(
|
|
544
|
+
(s, w) => s + (w.solValueUsd ?? 0),
|
|
545
|
+
0
|
|
546
|
+
);
|
|
547
|
+
if (totalSolValue > 0) {
|
|
548
|
+
allocMap.set(SOL_MINT, { label: "SOL", valueUsd: totalSolValue });
|
|
549
|
+
}
|
|
550
|
+
for (const wh of walletHoldings) {
|
|
551
|
+
for (const t of wh.tokens) {
|
|
552
|
+
if (t.valueUsd && t.valueUsd > 0) {
|
|
553
|
+
const ex = allocMap.get(t.mint);
|
|
554
|
+
if (ex) ex.valueUsd += t.valueUsd;
|
|
555
|
+
else allocMap.set(t.mint, { label: t.symbol, valueUsd: t.valueUsd });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const totalValueUsd = walletHoldings.reduce(
|
|
560
|
+
(s, w) => s + (w.totalValueUsd ?? 0),
|
|
561
|
+
0
|
|
562
|
+
);
|
|
563
|
+
const sortedAlloc = [...allocMap.entries()].sort(
|
|
564
|
+
(a, b) => b[1].valueUsd - a[1].valueUsd
|
|
565
|
+
);
|
|
566
|
+
const allocation = sortedAlloc.map(
|
|
567
|
+
([mint, { label, valueUsd }], i) => ({
|
|
568
|
+
label,
|
|
569
|
+
mint,
|
|
570
|
+
valueUsd,
|
|
571
|
+
percentage: totalValueUsd > 0 ? valueUsd / totalValueUsd * 100 : 0,
|
|
572
|
+
color: ALLOC_COLORS[i % ALLOC_COLORS.length]
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
return {
|
|
576
|
+
totalValueUsd: solPrice !== null ? totalValueUsd : null,
|
|
577
|
+
solPrice,
|
|
578
|
+
walletCount: wallets.length,
|
|
579
|
+
wallets: walletHoldings,
|
|
580
|
+
allocation,
|
|
581
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/lib/fraud-monitor.ts
|
|
586
|
+
import crypto3 from "crypto";
|
|
587
|
+
|
|
588
|
+
// src/lib/notifications.ts
|
|
589
|
+
import crypto2 from "crypto";
|
|
590
|
+
|
|
591
|
+
// src/lib/event-bus.ts
|
|
592
|
+
var encoder = new TextEncoder();
|
|
593
|
+
var nextEventId = 1;
|
|
594
|
+
var EventBus = class {
|
|
595
|
+
constructor() {
|
|
596
|
+
this.clients = /* @__PURE__ */ new Map();
|
|
597
|
+
this.listeners = [];
|
|
598
|
+
}
|
|
599
|
+
/** Register a non-SSE listener. Returns a cleanup function. */
|
|
600
|
+
addListener(fn) {
|
|
601
|
+
this.listeners.push(fn);
|
|
602
|
+
return () => {
|
|
603
|
+
this.listeners = this.listeners.filter((l) => l !== fn);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** Register a new SSE client. Returns a cleanup function. */
|
|
607
|
+
addClient(client) {
|
|
608
|
+
this.clients.set(client.id, client);
|
|
609
|
+
return () => {
|
|
610
|
+
this.clients.delete(client.id);
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/** Get count of connected clients. */
|
|
614
|
+
get clientCount() {
|
|
615
|
+
return this.clients.size;
|
|
616
|
+
}
|
|
617
|
+
/** Emit a typed event to all matching clients. Returns the full event. */
|
|
618
|
+
emit(partial) {
|
|
619
|
+
const event = {
|
|
620
|
+
...partial,
|
|
621
|
+
id: String(nextEventId++),
|
|
622
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
623
|
+
};
|
|
624
|
+
const encoded = encoder.encode(`data: ${JSON.stringify(event)}
|
|
625
|
+
|
|
626
|
+
`);
|
|
627
|
+
for (const [clientId, client] of this.clients) {
|
|
628
|
+
if (client.walletId && event.walletId && client.walletId !== event.walletId) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
client.controller.enqueue(encoded);
|
|
633
|
+
} catch {
|
|
634
|
+
this.clients.delete(clientId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const listener of this.listeners) {
|
|
638
|
+
try {
|
|
639
|
+
listener(event);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return event;
|
|
644
|
+
}
|
|
645
|
+
/** Send a keep-alive comment to all connected clients. */
|
|
646
|
+
ping() {
|
|
647
|
+
const encoded = encoder.encode(`: ping
|
|
648
|
+
|
|
649
|
+
`);
|
|
650
|
+
for (const [clientId, client] of this.clients) {
|
|
651
|
+
try {
|
|
652
|
+
client.controller.enqueue(encoded);
|
|
653
|
+
} catch {
|
|
654
|
+
this.clients.delete(clientId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/** Remove all clients and listeners — only for tests. */
|
|
659
|
+
_reset() {
|
|
660
|
+
this.clients.clear();
|
|
661
|
+
this.listeners = [];
|
|
662
|
+
nextEventId = 1;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var GLOBAL_KEY = "__agent_event_bus__";
|
|
666
|
+
function getEventBus() {
|
|
667
|
+
const g = globalThis;
|
|
668
|
+
if (!g[GLOBAL_KEY]) {
|
|
669
|
+
g[GLOBAL_KEY] = new EventBus();
|
|
670
|
+
}
|
|
671
|
+
return g[GLOBAL_KEY];
|
|
672
|
+
}
|
|
673
|
+
var eventBus = getEventBus();
|
|
674
|
+
|
|
675
|
+
// src/lib/fraud-monitor.ts
|
|
676
|
+
async function getAlerts(walletId, opts) {
|
|
677
|
+
const where = { walletId };
|
|
678
|
+
if (opts?.unacknowledgedOnly) {
|
|
679
|
+
where.acknowledged = false;
|
|
680
|
+
}
|
|
681
|
+
const rows = await prisma.fraudAlert.findMany({
|
|
682
|
+
where,
|
|
683
|
+
orderBy: { timestamp: "desc" }
|
|
684
|
+
});
|
|
685
|
+
return rows.map((row) => ({
|
|
686
|
+
id: row.id,
|
|
687
|
+
walletId: row.walletId,
|
|
688
|
+
rule: row.rule,
|
|
689
|
+
severity: row.severity,
|
|
690
|
+
timestamp: row.timestamp.toISOString(),
|
|
691
|
+
details: row.details,
|
|
692
|
+
acknowledged: row.acknowledged
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
async function getFraudStats(walletId) {
|
|
696
|
+
const alerts = await getAlerts(walletId);
|
|
697
|
+
const bySeverity = {};
|
|
698
|
+
const byRule = {};
|
|
699
|
+
let unacknowledged = 0;
|
|
700
|
+
for (const a of alerts) {
|
|
701
|
+
bySeverity[a.severity] = (bySeverity[a.severity] ?? 0) + 1;
|
|
702
|
+
byRule[a.rule] = (byRule[a.rule] ?? 0) + 1;
|
|
703
|
+
if (!a.acknowledged) unacknowledged++;
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
totalAlerts: alerts.length,
|
|
707
|
+
unacknowledged,
|
|
708
|
+
bySeverity,
|
|
709
|
+
byRule
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/mcp/tools.ts
|
|
714
|
+
var _manager = null;
|
|
715
|
+
function getManager(ctx) {
|
|
716
|
+
if (!_manager) {
|
|
717
|
+
_manager = new WalletManager(ctx.encryptionKey, ctx.rpcUrl);
|
|
718
|
+
}
|
|
719
|
+
return _manager;
|
|
720
|
+
}
|
|
721
|
+
var TOOLS = [
|
|
722
|
+
// ── Wallet Operations ──
|
|
723
|
+
{
|
|
724
|
+
name: "stng_wallet_create",
|
|
725
|
+
description: "Create a new Solana wallet with AES-256-GCM encryption. On devnet, automatically airdrops 1 SOL. Returns wallet ID, public key, and balance.",
|
|
726
|
+
inputSchema: {
|
|
727
|
+
type: "object",
|
|
728
|
+
properties: {
|
|
729
|
+
label: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Human-readable label for the wallet (e.g., 'trading-bot-1')"
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
required: []
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
name: "stng_wallet_list",
|
|
739
|
+
description: "List all wallets managed by this instance. Returns wallet IDs, public keys, labels, and creation dates.",
|
|
740
|
+
inputSchema: {
|
|
741
|
+
type: "object",
|
|
742
|
+
properties: {},
|
|
743
|
+
required: []
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "stng_wallet_balance",
|
|
748
|
+
description: "Get the SOL balance of a wallet by its public key (base58). Returns balance in SOL.",
|
|
749
|
+
inputSchema: {
|
|
750
|
+
type: "object",
|
|
751
|
+
properties: {
|
|
752
|
+
public_key: {
|
|
753
|
+
type: "string",
|
|
754
|
+
description: "Solana public key (base58 encoded)"
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
required: ["public_key"]
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: "stng_wallet_tokens",
|
|
762
|
+
description: "Get all SPL token balances for a wallet. Returns mint addresses, balances, and decimals.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
type: "object",
|
|
765
|
+
properties: {
|
|
766
|
+
public_key: {
|
|
767
|
+
type: "string",
|
|
768
|
+
description: "Solana public key (base58 encoded)"
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
required: ["public_key"]
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "stng_wallet_airdrop",
|
|
776
|
+
description: "Request a devnet SOL airdrop to a wallet. Only works on devnet/localnet. Default: 1 SOL.",
|
|
777
|
+
inputSchema: {
|
|
778
|
+
type: "object",
|
|
779
|
+
properties: {
|
|
780
|
+
public_key: {
|
|
781
|
+
type: "string",
|
|
782
|
+
description: "Solana public key (base58 encoded)"
|
|
783
|
+
},
|
|
784
|
+
amount: {
|
|
785
|
+
type: "number",
|
|
786
|
+
description: "Amount of SOL to airdrop (default: 1)"
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
required: ["public_key"]
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
// ── FROST Threshold Signing ──
|
|
793
|
+
{
|
|
794
|
+
name: "stng_frost_create",
|
|
795
|
+
description: "Create a FROST 2-of-3 threshold signing wallet. Uses Shamir secret sharing (RFC 9591) so no single party holds the full key. Returns wallet ID and group public key.",
|
|
796
|
+
inputSchema: {
|
|
797
|
+
type: "object",
|
|
798
|
+
properties: {
|
|
799
|
+
label: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: "Label for the FROST wallet"
|
|
802
|
+
},
|
|
803
|
+
threshold: {
|
|
804
|
+
type: "number",
|
|
805
|
+
description: "Signing threshold (default: 2)"
|
|
806
|
+
},
|
|
807
|
+
total_shares: {
|
|
808
|
+
type: "number",
|
|
809
|
+
description: "Total key shares (default: 3)"
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
required: []
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
name: "stng_frost_list",
|
|
817
|
+
description: "List all FROST threshold wallets with their group public keys, thresholds, and share counts.",
|
|
818
|
+
inputSchema: {
|
|
819
|
+
type: "object",
|
|
820
|
+
properties: {},
|
|
821
|
+
required: []
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: "stng_frost_verify",
|
|
826
|
+
description: "Verify the integrity of a FROST wallet's key shares. Confirms shares can reconstruct a valid group signature.",
|
|
827
|
+
inputSchema: {
|
|
828
|
+
type: "object",
|
|
829
|
+
properties: {
|
|
830
|
+
wallet_id: {
|
|
831
|
+
type: "string",
|
|
832
|
+
description: "FROST wallet ID (8-char hex)"
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
required: ["wallet_id"]
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
// ── Agent Lifecycle ──
|
|
839
|
+
{
|
|
840
|
+
name: "stng_agent_status",
|
|
841
|
+
description: "Get the lifecycle state (active/paused/terminated) of an agent wallet, including transition history and execution eligibility.",
|
|
842
|
+
inputSchema: {
|
|
843
|
+
type: "object",
|
|
844
|
+
properties: {
|
|
845
|
+
wallet_id: {
|
|
846
|
+
type: "string",
|
|
847
|
+
description: "Agent wallet ID (8-char hex)"
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
required: ["wallet_id"]
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: "stng_agent_pause",
|
|
855
|
+
description: "Pause an active agent. Paused agents cannot execute transactions but retain their wallets and state.",
|
|
856
|
+
inputSchema: {
|
|
857
|
+
type: "object",
|
|
858
|
+
properties: {
|
|
859
|
+
wallet_id: {
|
|
860
|
+
type: "string",
|
|
861
|
+
description: "Agent wallet ID to pause"
|
|
862
|
+
},
|
|
863
|
+
reason: {
|
|
864
|
+
type: "string",
|
|
865
|
+
description: "Reason for pausing (logged in audit trail)"
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
required: ["wallet_id"]
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
name: "stng_agent_resume",
|
|
873
|
+
description: "Resume a paused agent, restoring its ability to execute transactions.",
|
|
874
|
+
inputSchema: {
|
|
875
|
+
type: "object",
|
|
876
|
+
properties: {
|
|
877
|
+
wallet_id: {
|
|
878
|
+
type: "string",
|
|
879
|
+
description: "Agent wallet ID to resume"
|
|
880
|
+
},
|
|
881
|
+
reason: {
|
|
882
|
+
type: "string",
|
|
883
|
+
description: "Reason for resuming (logged in audit trail)"
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
required: ["wallet_id"]
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
name: "stng_agent_monitor",
|
|
891
|
+
description: "Get fleet-wide agent monitoring dashboard. Shows all agents with balances, statuses, strategies, success rates, and spending summaries.",
|
|
892
|
+
inputSchema: {
|
|
893
|
+
type: "object",
|
|
894
|
+
properties: {},
|
|
895
|
+
required: []
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
// ── Spending & Security ──
|
|
899
|
+
{
|
|
900
|
+
name: "stng_spending_limits",
|
|
901
|
+
description: "Get or update spending limits for a wallet. Returns per-transaction limit, daily limit, and current usage.",
|
|
902
|
+
inputSchema: {
|
|
903
|
+
type: "object",
|
|
904
|
+
properties: {
|
|
905
|
+
wallet_id: {
|
|
906
|
+
type: "string",
|
|
907
|
+
description: "Wallet ID (8-char hex)"
|
|
908
|
+
},
|
|
909
|
+
action: {
|
|
910
|
+
type: "string",
|
|
911
|
+
enum: ["get", "set"],
|
|
912
|
+
description: "Whether to get current limits or set new ones (default: get)"
|
|
913
|
+
},
|
|
914
|
+
per_tx_limit: {
|
|
915
|
+
type: "number",
|
|
916
|
+
description: "Max SOL per transaction (only for action=set)"
|
|
917
|
+
},
|
|
918
|
+
daily_limit: {
|
|
919
|
+
type: "number",
|
|
920
|
+
description: "Max SOL per 24h (only for action=set)"
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
required: ["wallet_id"]
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: "stng_audit_log",
|
|
928
|
+
description: "Read the HMAC-chained audit log. Filter by action type, wallet ID, agent ID, or time range. Returns tamper-evident entries newest-first.",
|
|
929
|
+
inputSchema: {
|
|
930
|
+
type: "object",
|
|
931
|
+
properties: {
|
|
932
|
+
action: {
|
|
933
|
+
type: "string",
|
|
934
|
+
description: "Filter by action type (e.g., 'wallet_create', 'frost_sign', 'agent_tool_call')"
|
|
935
|
+
},
|
|
936
|
+
wallet_id: {
|
|
937
|
+
type: "string",
|
|
938
|
+
description: "Filter by wallet ID"
|
|
939
|
+
},
|
|
940
|
+
limit: {
|
|
941
|
+
type: "number",
|
|
942
|
+
description: "Max entries to return (default: 20)"
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
required: []
|
|
946
|
+
}
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
name: "stng_audit_verify",
|
|
950
|
+
description: "Verify the integrity of the entire HMAC-chained audit log. Detects any tampering or corruption in the chain.",
|
|
951
|
+
inputSchema: {
|
|
952
|
+
type: "object",
|
|
953
|
+
properties: {},
|
|
954
|
+
required: []
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
// ── Portfolio & Analytics ──
|
|
958
|
+
{
|
|
959
|
+
name: "stng_portfolio",
|
|
960
|
+
description: "Get a portfolio summary across all wallets. Includes SOL and token balances, USD values (via Jupiter Price API), and allocation breakdown.",
|
|
961
|
+
inputSchema: {
|
|
962
|
+
type: "object",
|
|
963
|
+
properties: {},
|
|
964
|
+
required: []
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
name: "stng_tx_history",
|
|
969
|
+
description: "Get recent transaction history for a wallet. Shows tool calls, results, and success/failure status.",
|
|
970
|
+
inputSchema: {
|
|
971
|
+
type: "object",
|
|
972
|
+
properties: {
|
|
973
|
+
wallet_id: {
|
|
974
|
+
type: "string",
|
|
975
|
+
description: "Wallet ID (8-char hex)"
|
|
976
|
+
},
|
|
977
|
+
limit: {
|
|
978
|
+
type: "number",
|
|
979
|
+
description: "Max entries (default: 20)"
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
required: ["wallet_id"]
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
name: "stng_fraud_alerts",
|
|
987
|
+
description: "Get fraud detection alerts for a wallet. Monitors for rapid drain, velocity spikes, new program interaction, authority changes, and repeated failures.",
|
|
988
|
+
inputSchema: {
|
|
989
|
+
type: "object",
|
|
990
|
+
properties: {
|
|
991
|
+
wallet_id: {
|
|
992
|
+
type: "string",
|
|
993
|
+
description: "Wallet ID (8-char hex)"
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
required: ["wallet_id"]
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
];
|
|
1000
|
+
async function handleTool(name, args, ctx) {
|
|
1001
|
+
const manager = getManager(ctx);
|
|
1002
|
+
switch (name) {
|
|
1003
|
+
// ── Wallet Operations ──
|
|
1004
|
+
case "stng_wallet_create": {
|
|
1005
|
+
const label = args.label;
|
|
1006
|
+
const wallet = await manager.createWalletWithAirdrop(label, 1, {
|
|
1007
|
+
isAgent: true
|
|
1008
|
+
});
|
|
1009
|
+
return {
|
|
1010
|
+
wallet_id: wallet.wallet.id,
|
|
1011
|
+
public_key: wallet.wallet.publicKey,
|
|
1012
|
+
label: wallet.wallet.label,
|
|
1013
|
+
created_at: wallet.wallet.createdAt,
|
|
1014
|
+
airdrop: wallet.airdrop ? { signature: wallet.airdrop.signature, amount: wallet.airdrop.amount } : null
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
case "stng_wallet_list": {
|
|
1018
|
+
const wallets = await manager.listWallets();
|
|
1019
|
+
return {
|
|
1020
|
+
count: wallets.length,
|
|
1021
|
+
wallets: wallets.map((w) => ({
|
|
1022
|
+
id: w.id,
|
|
1023
|
+
public_key: w.publicKey,
|
|
1024
|
+
label: w.label ?? null,
|
|
1025
|
+
created_at: w.createdAt,
|
|
1026
|
+
is_agent: w.isAgent ?? false
|
|
1027
|
+
}))
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
case "stng_wallet_balance": {
|
|
1031
|
+
const pk = args.public_key;
|
|
1032
|
+
const balance = await manager.getBalance(pk);
|
|
1033
|
+
return { public_key: pk, balance_sol: balance };
|
|
1034
|
+
}
|
|
1035
|
+
case "stng_wallet_tokens": {
|
|
1036
|
+
const pk = args.public_key;
|
|
1037
|
+
const tokens = await manager.getTokenBalances(pk);
|
|
1038
|
+
return { public_key: pk, tokens };
|
|
1039
|
+
}
|
|
1040
|
+
case "stng_wallet_airdrop": {
|
|
1041
|
+
const pk = args.public_key;
|
|
1042
|
+
const amount = args.amount ?? 1;
|
|
1043
|
+
const sig = await manager.requestAirdrop(pk, amount);
|
|
1044
|
+
return { public_key: pk, amount, signature: sig };
|
|
1045
|
+
}
|
|
1046
|
+
// ── FROST Threshold Signing ──
|
|
1047
|
+
case "stng_frost_create": {
|
|
1048
|
+
const label = args.label;
|
|
1049
|
+
const threshold = args.threshold ?? 2;
|
|
1050
|
+
const totalShares = args.total_shares ?? 3;
|
|
1051
|
+
const wallet = await manager.createFrostWallet({
|
|
1052
|
+
threshold,
|
|
1053
|
+
totalShares,
|
|
1054
|
+
label
|
|
1055
|
+
});
|
|
1056
|
+
return {
|
|
1057
|
+
wallet_id: wallet.id,
|
|
1058
|
+
group_public_key: wallet.groupPublicKey,
|
|
1059
|
+
threshold: wallet.threshold,
|
|
1060
|
+
total_shares: wallet.totalShares,
|
|
1061
|
+
label: wallet.label ?? null,
|
|
1062
|
+
created_at: wallet.createdAt
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
case "stng_frost_list": {
|
|
1066
|
+
const wallets = await manager.listFrostWallets();
|
|
1067
|
+
return {
|
|
1068
|
+
count: wallets.length,
|
|
1069
|
+
wallets: wallets.map((w) => ({
|
|
1070
|
+
id: w.id,
|
|
1071
|
+
group_public_key: w.groupPublicKey,
|
|
1072
|
+
threshold: w.threshold,
|
|
1073
|
+
total_shares: w.totalShares,
|
|
1074
|
+
label: w.label ?? null,
|
|
1075
|
+
created_at: w.createdAt
|
|
1076
|
+
}))
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
case "stng_frost_verify": {
|
|
1080
|
+
const walletId = args.wallet_id;
|
|
1081
|
+
const valid = await manager.verifyFrostShares(walletId);
|
|
1082
|
+
return {
|
|
1083
|
+
wallet_id: walletId,
|
|
1084
|
+
shares_valid: valid,
|
|
1085
|
+
message: valid ? "All key shares are intact and can reconstruct a valid group signature." : "Share verification failed \u2014 shares may be corrupted."
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
// ── Agent Lifecycle ──
|
|
1089
|
+
case "stng_agent_status": {
|
|
1090
|
+
const walletId = args.wallet_id;
|
|
1091
|
+
const lifecycle = await getAgentLifecycle(walletId);
|
|
1092
|
+
const executable = await isAgentExecutable(walletId);
|
|
1093
|
+
return {
|
|
1094
|
+
wallet_id: walletId,
|
|
1095
|
+
state: lifecycle.state,
|
|
1096
|
+
updated_at: lifecycle.updatedAt,
|
|
1097
|
+
can_execute: executable.allowed,
|
|
1098
|
+
reason: lifecycle.reason ?? null,
|
|
1099
|
+
transitions: lifecycle.transitions
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
case "stng_agent_pause": {
|
|
1103
|
+
const walletId = args.wallet_id;
|
|
1104
|
+
const reason = args.reason;
|
|
1105
|
+
const result = await setAgentLifecycle(walletId, "paused", reason);
|
|
1106
|
+
return {
|
|
1107
|
+
wallet_id: walletId,
|
|
1108
|
+
state: result.state,
|
|
1109
|
+
updated_at: result.updatedAt,
|
|
1110
|
+
reason: result.reason ?? null
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
case "stng_agent_resume": {
|
|
1114
|
+
const walletId = args.wallet_id;
|
|
1115
|
+
const reason = args.reason;
|
|
1116
|
+
const result = await setAgentLifecycle(walletId, "active", reason);
|
|
1117
|
+
return {
|
|
1118
|
+
wallet_id: walletId,
|
|
1119
|
+
state: result.state,
|
|
1120
|
+
updated_at: result.updatedAt,
|
|
1121
|
+
reason: result.reason ?? null
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
case "stng_agent_monitor": {
|
|
1125
|
+
const data = await buildAgentMonitor(manager);
|
|
1126
|
+
return {
|
|
1127
|
+
total_agents: data.totalAgents,
|
|
1128
|
+
active: data.activeAgents,
|
|
1129
|
+
paused: data.pausedAgents,
|
|
1130
|
+
terminated: data.terminatedAgents,
|
|
1131
|
+
error: data.errorAgents,
|
|
1132
|
+
agents: data.agents.map((a) => ({
|
|
1133
|
+
id: a.id,
|
|
1134
|
+
public_key: a.publicKey,
|
|
1135
|
+
label: a.label,
|
|
1136
|
+
balance_sol: a.balance,
|
|
1137
|
+
status: a.status,
|
|
1138
|
+
lifecycle_state: a.lifecycleState,
|
|
1139
|
+
strategy: a.strategy,
|
|
1140
|
+
total_actions: a.totalActions,
|
|
1141
|
+
success_rate: a.successRate,
|
|
1142
|
+
last_action: a.lastAction
|
|
1143
|
+
})),
|
|
1144
|
+
fetched_at: data.fetchedAt
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
// ── Spending & Security ──
|
|
1148
|
+
case "stng_spending_limits": {
|
|
1149
|
+
const walletId = args.wallet_id;
|
|
1150
|
+
const action = args.action ?? "get";
|
|
1151
|
+
if (action === "set") {
|
|
1152
|
+
const config = {};
|
|
1153
|
+
if (args.per_tx_limit !== void 0)
|
|
1154
|
+
config.maxPerTx = args.per_tx_limit;
|
|
1155
|
+
if (args.daily_limit !== void 0)
|
|
1156
|
+
config.maxPerDay = args.daily_limit;
|
|
1157
|
+
await setSpendingLimits(walletId, config);
|
|
1158
|
+
}
|
|
1159
|
+
const limits = await getSpendingLimits(walletId);
|
|
1160
|
+
const summary = await getSpendingSummary(walletId);
|
|
1161
|
+
return {
|
|
1162
|
+
wallet_id: walletId,
|
|
1163
|
+
limits,
|
|
1164
|
+
summary
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
case "stng_audit_log": {
|
|
1168
|
+
const opts = {
|
|
1169
|
+
limit: args.limit ?? 20
|
|
1170
|
+
};
|
|
1171
|
+
if (args.action) opts.action = args.action;
|
|
1172
|
+
if (args.wallet_id) opts.walletId = args.wallet_id;
|
|
1173
|
+
const entries = await readAuditLog(opts);
|
|
1174
|
+
return { count: entries.length, entries };
|
|
1175
|
+
}
|
|
1176
|
+
case "stng_audit_verify": {
|
|
1177
|
+
const result = await verifyAuditIntegrity();
|
|
1178
|
+
return {
|
|
1179
|
+
...result,
|
|
1180
|
+
message: result.valid ? `Audit log integrity verified \u2014 ${result.total} entries, chain intact.` : `Audit log integrity BROKEN at entry ${result.brokenAt} of ${result.total}.`
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
// ── Portfolio & Analytics ──
|
|
1184
|
+
case "stng_portfolio": {
|
|
1185
|
+
const portfolio = await buildPortfolio(manager);
|
|
1186
|
+
return portfolio;
|
|
1187
|
+
}
|
|
1188
|
+
case "stng_tx_history": {
|
|
1189
|
+
const walletId = args.wallet_id;
|
|
1190
|
+
const limit = args.limit ?? 20;
|
|
1191
|
+
const entries = await readTxLog(walletId, limit);
|
|
1192
|
+
return { wallet_id: walletId, count: entries.length, transactions: entries };
|
|
1193
|
+
}
|
|
1194
|
+
case "stng_fraud_alerts": {
|
|
1195
|
+
const walletId = args.wallet_id;
|
|
1196
|
+
const alerts = await getAlerts(walletId);
|
|
1197
|
+
const stats = await getFraudStats(walletId);
|
|
1198
|
+
return {
|
|
1199
|
+
wallet_id: walletId,
|
|
1200
|
+
alerts,
|
|
1201
|
+
stats
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
default:
|
|
1205
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/mcp/prompts.ts
|
|
1210
|
+
var PROMPTS = [
|
|
1211
|
+
{
|
|
1212
|
+
name: "portfolio_overview",
|
|
1213
|
+
description: "Get a comprehensive portfolio overview across all wallets with USD values, allocations, and balances.",
|
|
1214
|
+
arguments: []
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: "security_audit",
|
|
1218
|
+
description: "Run a full security audit: verify audit log integrity, check spending limits, review fraud alerts, and FROST wallet status.",
|
|
1219
|
+
arguments: [
|
|
1220
|
+
{
|
|
1221
|
+
name: "wallet_id",
|
|
1222
|
+
description: "Specific wallet to audit (optional \u2014 omit for fleet-wide)",
|
|
1223
|
+
required: false
|
|
1224
|
+
}
|
|
1225
|
+
]
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
name: "create_agent_wallet",
|
|
1229
|
+
description: "Guided workflow to create a new agent wallet with spending limits and optional FROST threshold protection.",
|
|
1230
|
+
arguments: [
|
|
1231
|
+
{
|
|
1232
|
+
name: "label",
|
|
1233
|
+
description: "Name for the new agent (e.g., 'trading-bot-alpha')",
|
|
1234
|
+
required: true
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
name: "use_frost",
|
|
1238
|
+
description: "Whether to create a FROST threshold wallet (yes/no, default: no)",
|
|
1239
|
+
required: false
|
|
1240
|
+
}
|
|
1241
|
+
]
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
name: "fleet_health",
|
|
1245
|
+
description: "Check the health of all agents in the fleet: statuses, balances, success rates, and spending.",
|
|
1246
|
+
arguments: []
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
name: "investigate_wallet",
|
|
1250
|
+
description: "Deep investigation of a specific wallet: balance, tokens, transaction history, spending, fraud alerts, and lifecycle state.",
|
|
1251
|
+
arguments: [
|
|
1252
|
+
{
|
|
1253
|
+
name: "wallet_id",
|
|
1254
|
+
description: "The wallet ID to investigate",
|
|
1255
|
+
required: true
|
|
1256
|
+
}
|
|
1257
|
+
]
|
|
1258
|
+
}
|
|
1259
|
+
];
|
|
1260
|
+
function getPromptContent(name, args) {
|
|
1261
|
+
switch (name) {
|
|
1262
|
+
case "portfolio_overview":
|
|
1263
|
+
return `Please give me a comprehensive portfolio overview.
|
|
1264
|
+
|
|
1265
|
+
Steps:
|
|
1266
|
+
1. Use stng_portfolio to get all wallet balances, token holdings, and USD values
|
|
1267
|
+
2. Use stng_wallet_list to get wallet labels and metadata
|
|
1268
|
+
3. Present a clear summary including:
|
|
1269
|
+
- Total portfolio value in USD
|
|
1270
|
+
- Per-wallet breakdown (label, SOL balance, token balances, USD value)
|
|
1271
|
+
- Asset allocation percentages
|
|
1272
|
+
- Any wallets with zero balance that may need funding`;
|
|
1273
|
+
case "security_audit": {
|
|
1274
|
+
const walletClause = args.wallet_id ? `Focus on wallet ${args.wallet_id}.` : "Audit all wallets fleet-wide.";
|
|
1275
|
+
return `Please run a comprehensive security audit. ${walletClause}
|
|
1276
|
+
|
|
1277
|
+
Steps:
|
|
1278
|
+
1. Use stng_audit_verify to check HMAC chain integrity of the audit log
|
|
1279
|
+
2. Use stng_audit_log to review recent security-relevant events (wallet_create, frost_sign, spending_limit_exceeded)
|
|
1280
|
+
3. Use stng_frost_list to check FROST wallet status and verify shares with stng_frost_verify
|
|
1281
|
+
4. ${args.wallet_id ? `Use stng_spending_limits for wallet ${args.wallet_id}` : "Use stng_agent_monitor to check all agents"}
|
|
1282
|
+
5. ${args.wallet_id ? `Use stng_fraud_alerts for wallet ${args.wallet_id}` : "Check fraud alerts for any flagged wallets"}
|
|
1283
|
+
|
|
1284
|
+
Report:
|
|
1285
|
+
- Audit log integrity status (intact/broken)
|
|
1286
|
+
- Total audit entries and recent activity
|
|
1287
|
+
- FROST wallet health
|
|
1288
|
+
- Spending limit compliance
|
|
1289
|
+
- Any fraud alerts or anomalies
|
|
1290
|
+
- Security recommendations`;
|
|
1291
|
+
}
|
|
1292
|
+
case "create_agent_wallet": {
|
|
1293
|
+
const label = args.label || "new-agent";
|
|
1294
|
+
const useFrost = args.use_frost?.toLowerCase() === "yes";
|
|
1295
|
+
return `Please create a new agent wallet named "${label}".
|
|
1296
|
+
|
|
1297
|
+
Steps:
|
|
1298
|
+
1. ${useFrost ? "Use stng_frost_create to create a FROST 2-of-3 threshold wallet" : "Use stng_wallet_create to create a standard wallet"} with label "${label}"
|
|
1299
|
+
2. Use stng_spending_limits with action=set to configure:
|
|
1300
|
+
- per_tx_limit: 0.5 SOL (conservative start)
|
|
1301
|
+
- daily_limit: 2 SOL
|
|
1302
|
+
3. Use stng_agent_status to confirm the agent is in "active" state
|
|
1303
|
+
4. Use stng_wallet_balance to confirm initial balance
|
|
1304
|
+
|
|
1305
|
+
Report the new agent's:
|
|
1306
|
+
- Wallet ID
|
|
1307
|
+
- Public key
|
|
1308
|
+
- ${useFrost ? "FROST group public key and threshold" : "Balance"}
|
|
1309
|
+
- Spending limits
|
|
1310
|
+
- Lifecycle state`;
|
|
1311
|
+
}
|
|
1312
|
+
case "fleet_health":
|
|
1313
|
+
return `Please check the health of all agents in the fleet.
|
|
1314
|
+
|
|
1315
|
+
Steps:
|
|
1316
|
+
1. Use stng_agent_monitor to get the full fleet dashboard
|
|
1317
|
+
2. For any agents in "error" or "paused" state, use stng_agent_status for details
|
|
1318
|
+
3. Use stng_portfolio to check fleet-wide balances
|
|
1319
|
+
|
|
1320
|
+
Report:
|
|
1321
|
+
- Total agents (active / paused / terminated / error)
|
|
1322
|
+
- Per-agent health summary (balance, success rate, last action, strategy)
|
|
1323
|
+
- Any agents that need attention (low balance, high failure rate, paused)
|
|
1324
|
+
- Fleet-wide statistics (total value, average success rate)`;
|
|
1325
|
+
case "investigate_wallet": {
|
|
1326
|
+
const walletId = args.wallet_id || "[WALLET_ID]";
|
|
1327
|
+
return `Please do a deep investigation of wallet ${walletId}.
|
|
1328
|
+
|
|
1329
|
+
Steps:
|
|
1330
|
+
1. Use stng_agent_status to check lifecycle state and transition history
|
|
1331
|
+
2. Use stng_spending_limits to review spending configuration and usage
|
|
1332
|
+
3. Use stng_tx_history to review recent transactions
|
|
1333
|
+
4. Use stng_fraud_alerts to check for any security alerts
|
|
1334
|
+
5. Use stng_audit_log filtered by wallet_id=${walletId} to review audit trail
|
|
1335
|
+
|
|
1336
|
+
Report:
|
|
1337
|
+
- Wallet state (active/paused/terminated) and why
|
|
1338
|
+
- Spending: limits vs actual usage, any violations
|
|
1339
|
+
- Recent transactions: success/failure patterns, tools used
|
|
1340
|
+
- Fraud alerts: any triggered rules, severity
|
|
1341
|
+
- Audit trail: key events timeline
|
|
1342
|
+
- Recommendations for this wallet`;
|
|
1343
|
+
}
|
|
1344
|
+
default:
|
|
1345
|
+
return `Unknown prompt: ${name}`;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/mcp/server.ts
|
|
1350
|
+
function createServer(ctx) {
|
|
1351
|
+
const server = new Server(
|
|
1352
|
+
{
|
|
1353
|
+
name: "stng-defi-wallets",
|
|
1354
|
+
version: "0.1.0"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
capabilities: {
|
|
1358
|
+
tools: {},
|
|
1359
|
+
prompts: {}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
);
|
|
1363
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1364
|
+
return { tools: [...TOOLS] };
|
|
1365
|
+
});
|
|
1366
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1367
|
+
return { prompts: PROMPTS };
|
|
1368
|
+
});
|
|
1369
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1370
|
+
const { name, arguments: args } = request.params;
|
|
1371
|
+
const prompt = PROMPTS.find((p) => p.name === name);
|
|
1372
|
+
if (!prompt) {
|
|
1373
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
1374
|
+
}
|
|
1375
|
+
const content = getPromptContent(name, args || {});
|
|
1376
|
+
return {
|
|
1377
|
+
description: prompt.description,
|
|
1378
|
+
messages: [
|
|
1379
|
+
{
|
|
1380
|
+
role: "user",
|
|
1381
|
+
content: {
|
|
1382
|
+
type: "text",
|
|
1383
|
+
text: content
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
]
|
|
1387
|
+
};
|
|
1388
|
+
});
|
|
1389
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1390
|
+
const { name, arguments: args } = request.params;
|
|
1391
|
+
try {
|
|
1392
|
+
const result = await handleTool(
|
|
1393
|
+
name,
|
|
1394
|
+
args ?? {},
|
|
1395
|
+
ctx
|
|
1396
|
+
);
|
|
1397
|
+
return {
|
|
1398
|
+
content: [
|
|
1399
|
+
{
|
|
1400
|
+
type: "text",
|
|
1401
|
+
text: JSON.stringify(result, null, 2)
|
|
1402
|
+
}
|
|
1403
|
+
]
|
|
1404
|
+
};
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1407
|
+
return {
|
|
1408
|
+
content: [
|
|
1409
|
+
{
|
|
1410
|
+
type: "text",
|
|
1411
|
+
text: JSON.stringify({ error: message })
|
|
1412
|
+
}
|
|
1413
|
+
],
|
|
1414
|
+
isError: true
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
return server;
|
|
1419
|
+
}
|
|
1420
|
+
async function runMcpServer() {
|
|
1421
|
+
const encryptionKey = process.env.WALLET_ENCRYPTION_KEY;
|
|
1422
|
+
if (!encryptionKey) {
|
|
1423
|
+
throw new Error("WALLET_ENCRYPTION_KEY environment variable is required");
|
|
1424
|
+
}
|
|
1425
|
+
const rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.devnet.solana.com";
|
|
1426
|
+
const ctx = { encryptionKey, rpcUrl };
|
|
1427
|
+
const server = createServer(ctx);
|
|
1428
|
+
const transport = new StdioServerTransport();
|
|
1429
|
+
await server.connect(transport);
|
|
1430
|
+
console.error(
|
|
1431
|
+
`stng-defi-wallets MCP server started (${TOOLS.length} tools, ${PROMPTS.length} prompts)`
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
var isDirectRun = process.argv[1]?.endsWith("server.ts") || process.argv[1]?.endsWith("server.js");
|
|
1435
|
+
if (isDirectRun) {
|
|
1436
|
+
if (!process.env.WALLET_ENCRYPTION_KEY) {
|
|
1437
|
+
console.error(
|
|
1438
|
+
"Error: WALLET_ENCRYPTION_KEY environment variable is required"
|
|
1439
|
+
);
|
|
1440
|
+
console.error("");
|
|
1441
|
+
console.error("Usage:");
|
|
1442
|
+
console.error(
|
|
1443
|
+
" WALLET_ENCRYPTION_KEY=<hex-key> npx tsx src/mcp/server.ts"
|
|
1444
|
+
);
|
|
1445
|
+
console.error("");
|
|
1446
|
+
console.error("Environment variables:");
|
|
1447
|
+
console.error(
|
|
1448
|
+
" WALLET_ENCRYPTION_KEY (required) 32-byte hex key for wallet encryption"
|
|
1449
|
+
);
|
|
1450
|
+
console.error(
|
|
1451
|
+
" SOLANA_RPC_URL (optional) Solana RPC, default: https://api.devnet.solana.com"
|
|
1452
|
+
);
|
|
1453
|
+
console.error("");
|
|
1454
|
+
console.error("Generate a key:");
|
|
1455
|
+
console.error(
|
|
1456
|
+
` node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
|
1457
|
+
);
|
|
1458
|
+
process.exit(1);
|
|
1459
|
+
}
|
|
1460
|
+
runMcpServer().catch((error) => {
|
|
1461
|
+
console.error("Fatal error:", error);
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
export {
|
|
1466
|
+
runMcpServer
|
|
1467
|
+
};
|
|
1468
|
+
//# sourceMappingURL=mcp-server.js.map
|