rook-wallet-mcp 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.
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ROOK MCP Server
4
+ *
5
+ * AI Agent'ların ROOK Wallet'ı kullanması için MCP (Model Context Protocol) server.
6
+ * Bu server LOKAL olarak kullanıcının bilgisayarında çalışır.
7
+ *
8
+ * Kurulum:
9
+ * 1. npm install -g @rook/mcp-server
10
+ * 2. .env dosyası oluştur (ROOK_PRIVATE_KEY, ROOK_DAILY_LIMIT, vs.)
11
+ * 3. Claude Desktop config'e ekle
12
+ *
13
+ * %100 Non-Custodial - Tüm işlemler kullanıcının bilgisayarında gerçekleşir.
14
+ */
15
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,500 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ROOK MCP Server
4
+ *
5
+ * AI Agent'ların ROOK Wallet'ı kullanması için MCP (Model Context Protocol) server.
6
+ * Bu server LOKAL olarak kullanıcının bilgisayarında çalışır.
7
+ *
8
+ * Kurulum:
9
+ * 1. npm install -g @rook/mcp-server
10
+ * 2. .env dosyası oluştur (ROOK_PRIVATE_KEY, ROOK_DAILY_LIMIT, vs.)
11
+ * 3. Claude Desktop config'e ekle
12
+ *
13
+ * %100 Non-Custodial - Tüm işlemler kullanıcının bilgisayarında gerçekleşir.
14
+ */
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
18
+ import { ethers } from "ethers";
19
+ function loadConfig() {
20
+ const privateKey = process.env.ROOK_PRIVATE_KEY;
21
+ if (!privateKey) {
22
+ throw new Error("ROOK_PRIVATE_KEY environment variable is required");
23
+ }
24
+ return {
25
+ privateKey,
26
+ dailyLimitMON: parseFloat(process.env.ROOK_DAILY_LIMIT || "10"),
27
+ perTxLimitMON: parseFloat(process.env.ROOK_PER_TX_LIMIT || "1"),
28
+ contractWhitelist: process.env.ROOK_WHITELIST?.split(",").filter(Boolean) || [],
29
+ rpcUrl: process.env.ROOK_RPC_URL || "https://testnet-rpc.monad.xyz",
30
+ chainId: parseInt(process.env.ROOK_CHAIN_ID || "10143"),
31
+ explorerUrl: process.env.ROOK_EXPLORER_URL || "https://monad-testnet.socialscan.io",
32
+ };
33
+ }
34
+ class SpendingTracker {
35
+ history = [];
36
+ getTodayDate() {
37
+ return new Date().toISOString().split("T")[0];
38
+ }
39
+ getSpentToday() {
40
+ const today = this.getTodayDate();
41
+ return this.history
42
+ .filter((r) => r.date === today)
43
+ .reduce((sum, r) => sum + r.amount, 0);
44
+ }
45
+ record(amount, txHash, to) {
46
+ this.history.push({
47
+ date: this.getTodayDate(),
48
+ amount,
49
+ txHash,
50
+ to,
51
+ timestamp: Date.now(),
52
+ });
53
+ }
54
+ getHistory() {
55
+ return [...this.history];
56
+ }
57
+ }
58
+ // ============================================================
59
+ // ROOK WALLET SERVICE
60
+ // ============================================================
61
+ class RookWalletService {
62
+ config;
63
+ wallet;
64
+ provider;
65
+ tracker;
66
+ constructor(config) {
67
+ this.config = config;
68
+ this.tracker = new SpendingTracker();
69
+ this.provider = new ethers.JsonRpcProvider(config.rpcUrl, {
70
+ chainId: config.chainId,
71
+ name: "Monad Testnet",
72
+ });
73
+ this.wallet = new ethers.Wallet(config.privateKey, this.provider);
74
+ }
75
+ get address() {
76
+ return this.wallet.address;
77
+ }
78
+ async getBalance() {
79
+ const balance = await this.provider.getBalance(this.wallet.address);
80
+ return ethers.formatEther(balance);
81
+ }
82
+ getRemainingDailyLimit() {
83
+ const spent = this.tracker.getSpentToday();
84
+ return Math.max(0, this.config.dailyLimitMON - spent);
85
+ }
86
+ getSpentToday() {
87
+ return this.tracker.getSpentToday();
88
+ }
89
+ validateTransaction(to, amountMON) {
90
+ // Check per-tx limit
91
+ if (amountMON > this.config.perTxLimitMON) {
92
+ return {
93
+ valid: false,
94
+ reason: `Amount ${amountMON} MON exceeds per-transaction limit of ${this.config.perTxLimitMON} MON`,
95
+ };
96
+ }
97
+ // Check daily limit
98
+ const remaining = this.getRemainingDailyLimit();
99
+ if (amountMON > remaining) {
100
+ return {
101
+ valid: false,
102
+ reason: `Would exceed daily limit. Remaining today: ${remaining.toFixed(4)} MON`,
103
+ };
104
+ }
105
+ // Check whitelist
106
+ if (this.config.contractWhitelist.length > 0) {
107
+ const isWhitelisted = this.config.contractWhitelist.some((addr) => addr.toLowerCase() === to.toLowerCase());
108
+ if (!isWhitelisted) {
109
+ return {
110
+ valid: false,
111
+ reason: "Recipient address is not in the allowed addresses list",
112
+ };
113
+ }
114
+ }
115
+ return { valid: true };
116
+ }
117
+ async send(to, amountMON) {
118
+ const amount = parseFloat(amountMON);
119
+ // Validate
120
+ const validation = this.validateTransaction(to, amount);
121
+ if (!validation.valid) {
122
+ return { success: false, error: validation.reason };
123
+ }
124
+ // Validate address
125
+ if (!ethers.isAddress(to)) {
126
+ return { success: false, error: "Invalid recipient address" };
127
+ }
128
+ try {
129
+ const tx = await this.wallet.sendTransaction({
130
+ to,
131
+ value: ethers.parseEther(amountMON),
132
+ });
133
+ await tx.wait();
134
+ // Record spending
135
+ this.tracker.record(amount, tx.hash, to);
136
+ return { success: true, txHash: tx.hash };
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : "Transaction failed";
140
+ return { success: false, error: message };
141
+ }
142
+ }
143
+ getStatus() {
144
+ return {
145
+ address: this.wallet.address,
146
+ dailyLimit: this.config.dailyLimitMON,
147
+ perTxLimit: this.config.perTxLimitMON,
148
+ spentToday: this.getSpentToday(),
149
+ remainingToday: this.getRemainingDailyLimit(),
150
+ whitelistEnabled: this.config.contractWhitelist.length > 0,
151
+ whitelistCount: this.config.contractWhitelist.length,
152
+ network: "Monad Testnet",
153
+ chainId: this.config.chainId,
154
+ explorerUrl: this.config.explorerUrl,
155
+ };
156
+ }
157
+ getTransactionHistory() {
158
+ return this.tracker.getHistory();
159
+ }
160
+ getExplorerUrl(txHash) {
161
+ return `${this.config.explorerUrl}/tx/${txHash}`;
162
+ }
163
+ }
164
+ // ============================================================
165
+ // MCP SERVER
166
+ // ============================================================
167
+ async function main() {
168
+ // Load configuration
169
+ const config = loadConfig();
170
+ const walletService = new RookWalletService(config);
171
+ console.error(`🏰 ROOK MCP Server starting...`);
172
+ console.error(` Address: ${walletService.address}`);
173
+ console.error(` Daily Limit: ${config.dailyLimitMON} MON`);
174
+ console.error(` Per-TX Limit: ${config.perTxLimitMON} MON`);
175
+ console.error(` Network: Monad Testnet (Chain ID: ${config.chainId})`);
176
+ // Create MCP server
177
+ const server = new Server({
178
+ name: "rook-wallet",
179
+ version: "0.1.0",
180
+ }, {
181
+ capabilities: {
182
+ tools: {},
183
+ resources: {},
184
+ prompts: {},
185
+ },
186
+ });
187
+ // ============================================================
188
+ // TOOLS
189
+ // ============================================================
190
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
191
+ return {
192
+ tools: [
193
+ {
194
+ name: "send_mon",
195
+ description: `Send MON cryptocurrency to an address. This wallet has spending limits:
196
+ - Daily limit: ${config.dailyLimitMON} MON
197
+ - Per-transaction limit: ${config.perTxLimitMON} MON
198
+ ${config.contractWhitelist.length > 0 ? `- Can only send to whitelisted addresses` : "- Can send to any address"}
199
+
200
+ Use this when you need to make a payment or transfer MON.`,
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ to: {
205
+ type: "string",
206
+ description: "The recipient wallet address (must start with 0x)",
207
+ },
208
+ amount: {
209
+ type: "string",
210
+ description: "Amount of MON to send (e.g., '0.5' or '1.0')",
211
+ },
212
+ },
213
+ required: ["to", "amount"],
214
+ },
215
+ },
216
+ {
217
+ name: "get_balance",
218
+ description: "Get the current MON balance of this wallet.",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: {},
222
+ },
223
+ },
224
+ {
225
+ name: "get_wallet_status",
226
+ description: "Get detailed status of the wallet including spending limits, today's spending, and remaining allowance.",
227
+ inputSchema: {
228
+ type: "object",
229
+ properties: {},
230
+ },
231
+ },
232
+ {
233
+ name: "check_spending_limit",
234
+ description: "Check if a specific amount can be sent without exceeding limits.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ amount: {
239
+ type: "string",
240
+ description: "Amount of MON to check",
241
+ },
242
+ to: {
243
+ type: "string",
244
+ description: "Optional: recipient address to check against whitelist",
245
+ },
246
+ },
247
+ required: ["amount"],
248
+ },
249
+ },
250
+ {
251
+ name: "get_transaction_history",
252
+ description: "Get the history of all transactions sent from this wallet during the current session. Shows amounts, recipients, timestamps, and explorer links.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {},
256
+ },
257
+ },
258
+ ],
259
+ };
260
+ });
261
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
262
+ const { name, arguments: args } = request.params;
263
+ switch (name) {
264
+ case "send_mon": {
265
+ const to = String(args?.to || "");
266
+ const amount = String(args?.amount || "0");
267
+ const result = await walletService.send(to, amount);
268
+ if (result.success) {
269
+ const explorerUrl = walletService.getExplorerUrl(result.txHash);
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: `✅ Transaction successful!\n\nSent: ${amount} MON\nTo: ${to}\nTx Hash: ${result.txHash}\n\nView on explorer: ${explorerUrl}\n\nRemaining daily limit: ${walletService.getRemainingDailyLimit().toFixed(4)} MON`,
275
+ },
276
+ ],
277
+ };
278
+ }
279
+ else {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: `❌ Transaction failed: ${result.error}`,
285
+ },
286
+ ],
287
+ isError: true,
288
+ };
289
+ }
290
+ }
291
+ case "get_balance": {
292
+ const balance = await walletService.getBalance();
293
+ const status = walletService.getStatus();
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: `💰 Wallet Balance\n\nAddress: ${status.address}\nBalance: ${balance} MON\n\nDaily Limit: ${status.dailyLimit} MON\nSpent Today: ${status.spentToday.toFixed(4)} MON\nRemaining: ${status.remainingToday.toFixed(4)} MON`,
299
+ },
300
+ ],
301
+ };
302
+ }
303
+ case "get_wallet_status": {
304
+ const balance = await walletService.getBalance();
305
+ const status = walletService.getStatus();
306
+ return {
307
+ content: [
308
+ {
309
+ type: "text",
310
+ text: `🏰 ROOK Wallet Status\n\n` +
311
+ `Address: ${status.address}\n` +
312
+ `Balance: ${balance} MON\n` +
313
+ `Network: ${status.network} (Chain ID: ${status.chainId})\n\n` +
314
+ `📊 Spending Limits:\n` +
315
+ `- Daily Limit: ${status.dailyLimit} MON\n` +
316
+ `- Per-TX Limit: ${status.perTxLimit} MON\n` +
317
+ `- Spent Today: ${status.spentToday.toFixed(4)} MON\n` +
318
+ `- Remaining Today: ${status.remainingToday.toFixed(4)} MON\n\n` +
319
+ `🔒 Whitelist: ${status.whitelistEnabled ? `Enabled (${status.whitelistCount} addresses)` : "Disabled (can send to any address)"}\n\n` +
320
+ `🔗 Explorer: ${status.explorerUrl}`,
321
+ },
322
+ ],
323
+ };
324
+ }
325
+ case "check_spending_limit": {
326
+ const amount = parseFloat(String(args?.amount || "0"));
327
+ const to = args?.to ? String(args.to) : "";
328
+ const validation = walletService.validateTransaction(to || "0x0000000000000000000000000000000000000000", amount);
329
+ if (validation.valid) {
330
+ return {
331
+ content: [
332
+ {
333
+ type: "text",
334
+ text: `✅ Amount ${amount} MON is within limits.\n\nRemaining daily limit after this: ${(walletService.getRemainingDailyLimit() - amount).toFixed(4)} MON`,
335
+ },
336
+ ],
337
+ };
338
+ }
339
+ else {
340
+ return {
341
+ content: [
342
+ {
343
+ type: "text",
344
+ text: `❌ Cannot send ${amount} MON: ${validation.reason}`,
345
+ },
346
+ ],
347
+ };
348
+ }
349
+ }
350
+ case "get_transaction_history": {
351
+ const history = walletService.getTransactionHistory();
352
+ if (history.length === 0) {
353
+ return {
354
+ content: [
355
+ {
356
+ type: "text",
357
+ text: "No transactions have been sent in this session yet.",
358
+ },
359
+ ],
360
+ };
361
+ }
362
+ const lines = history.map((record, i) => {
363
+ const explorerUrl = walletService.getExplorerUrl(record.txHash);
364
+ const time = new Date(record.timestamp).toLocaleTimeString();
365
+ return `${i + 1}. ${record.amount} MON -> ${record.to}\n Time: ${time}\n TX: ${record.txHash}\n Explorer: ${explorerUrl}`;
366
+ });
367
+ const totalSpent = history.reduce((sum, r) => sum + r.amount, 0);
368
+ return {
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: `Transaction History (${history.length} transactions)\n\n${lines.join("\n\n")}\n\nTotal spent this session: ${totalSpent.toFixed(4)} MON\nRemaining daily limit: ${walletService.getRemainingDailyLimit().toFixed(4)} MON`,
373
+ },
374
+ ],
375
+ };
376
+ }
377
+ default:
378
+ throw new Error(`Unknown tool: ${name}`);
379
+ }
380
+ });
381
+ // ============================================================
382
+ // RESOURCES
383
+ // ============================================================
384
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
385
+ return {
386
+ resources: [
387
+ {
388
+ uri: "rook://wallet/info",
389
+ name: "Wallet Information",
390
+ description: "Current wallet address, balance, and spending limits",
391
+ mimeType: "application/json",
392
+ },
393
+ ],
394
+ };
395
+ });
396
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
397
+ const { uri } = request.params;
398
+ if (uri === "rook://wallet/info") {
399
+ const balance = await walletService.getBalance();
400
+ const status = walletService.getStatus();
401
+ return {
402
+ contents: [
403
+ {
404
+ uri,
405
+ mimeType: "application/json",
406
+ text: JSON.stringify({
407
+ ...status,
408
+ balance,
409
+ }, null, 2),
410
+ },
411
+ ],
412
+ };
413
+ }
414
+ throw new Error(`Unknown resource: ${uri}`);
415
+ });
416
+ // ============================================================
417
+ // PROMPTS
418
+ // ============================================================
419
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
420
+ return {
421
+ prompts: [
422
+ {
423
+ name: "send-payment",
424
+ description: "Guide through sending a MON payment with safety checks. Checks balance and limits before sending.",
425
+ arguments: [
426
+ {
427
+ name: "to",
428
+ description: "Recipient wallet address (0x...)",
429
+ required: true,
430
+ },
431
+ {
432
+ name: "amount",
433
+ description: "Amount of MON to send",
434
+ required: true,
435
+ },
436
+ ],
437
+ },
438
+ {
439
+ name: "wallet-overview",
440
+ description: "Get a comprehensive overview of wallet status, spending limits, and transaction history.",
441
+ },
442
+ ],
443
+ };
444
+ });
445
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
446
+ const { name, arguments: args } = request.params;
447
+ switch (name) {
448
+ case "send-payment": {
449
+ const to = args?.to || "<recipient_address>";
450
+ const amount = args?.amount || "<amount>";
451
+ return {
452
+ description: `Send ${amount} MON to ${to} with safety checks`,
453
+ messages: [
454
+ {
455
+ role: "user",
456
+ content: {
457
+ type: "text",
458
+ text: `I want to send ${amount} MON to ${to}. Please follow these steps:
459
+
460
+ 1. First, use get_wallet_status to check the current wallet balance and spending limits.
461
+ 2. Then, use check_spending_limit with amount "${amount}" and to "${to}" to verify the transaction is within limits.
462
+ 3. If the check passes, use send_mon to send ${amount} MON to ${to}.
463
+ 4. After sending, use get_balance to confirm the updated balance.
464
+ 5. Provide a summary of the transaction.`,
465
+ },
466
+ },
467
+ ],
468
+ };
469
+ }
470
+ case "wallet-overview": {
471
+ return {
472
+ description: "Comprehensive wallet overview",
473
+ messages: [
474
+ {
475
+ role: "user",
476
+ content: {
477
+ type: "text",
478
+ text: `Please give me a comprehensive overview of my ROOK wallet:
479
+
480
+ 1. Use get_wallet_status to get the current status, balance, and spending limits.
481
+ 2. Use get_transaction_history to see all transactions from this session.
482
+ 3. Summarize everything clearly: balance, daily spending progress, remaining limits, and recent activity.`,
483
+ },
484
+ },
485
+ ],
486
+ };
487
+ }
488
+ default:
489
+ throw new Error(`Unknown prompt: ${name}`);
490
+ }
491
+ });
492
+ // Start server
493
+ const transport = new StdioServerTransport();
494
+ await server.connect(transport);
495
+ console.error("🏰 ROOK MCP Server running");
496
+ }
497
+ main().catch((error) => {
498
+ console.error("Fatal error:", error);
499
+ process.exit(1);
500
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "rook-wallet-mcp",
3
+ "version": "0.1.0",
4
+ "description": "ROOK Wallet MCP Server - AI Agent Wallet with Spending Limits on Monad",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "rook-wallet-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "dev": "tsx src/index.ts"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "ethers": "^6.13.4",
21
+ "zod": "^3.23.8"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "tsx": "^4.0.0",
26
+ "typescript": "^5.6.0"
27
+ },
28
+ "keywords": ["mcp", "wallet", "ai-agent", "monad", "crypto"],
29
+ "license": "MIT"
30
+ }