@aspect-warden/mcp-server 0.3.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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,1045 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { createPublicClient, createWalletClient, http, parseAbi, encodeFunctionData, formatEther, formatUnits, parseUnits, } from 'viem';
6
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
7
+ import { sepolia } from 'viem/chains';
8
+ // ============================================================
9
+ // Configuration from environment
10
+ // ============================================================
11
+ const PROVIDER_URL = process.env.RPC_URL || 'https://rpc.sepolia.org';
12
+ const POLICY_DELEGATE_ADDRESS = process.env.POLICY_DELEGATE_ADDRESS;
13
+ const SEPOLIA_USDT = (process.env.SEPOLIA_USDT_ADDRESS || '0x7169D38820dfd117C3FA1f22a697dBA58d90BA06');
14
+ const ERC8004_IDENTITY_REGISTRY = process.env.ERC8004_IDENTITY_REGISTRY;
15
+ // ============================================================
16
+ // Import from @aspect-warden/policy-engine
17
+ // ============================================================
18
+ import { PolicyEngine, AuditLogger } from '@aspect-warden/policy-engine';
19
+ // ============================================================
20
+ // Contract ABIs
21
+ // ============================================================
22
+ const ERC20_ABI = parseAbi([
23
+ 'function balanceOf(address) view returns (uint256)',
24
+ 'function transfer(address to, uint256 amount) returns (bool)',
25
+ 'function decimals() view returns (uint8)',
26
+ ]);
27
+ const POLICY_DELEGATE_ABI = parseAbi([
28
+ 'function createSessionKey(address eoa, address key, uint256 maxPerTx, uint256 dailyLimit, uint48 validAfter, uint48 validUntil, uint256 cooldownSeconds) external',
29
+ 'function revokeSessionKey(address eoa, address key) external',
30
+ ]);
31
+ const ERC8004_ABI = parseAbi([
32
+ 'function registerAgent(string name, string[] capabilities) external returns (uint256)',
33
+ 'event AgentRegistered(uint256 indexed agentId, address indexed owner, string name)',
34
+ ]);
35
+ // PolicyEngine and AuditLogger imported from @aspect-warden/policy-engine
36
+ // ============================================================
37
+ // MCP Server State
38
+ // ============================================================
39
+ let policyEngine = null;
40
+ let auditLogger = null;
41
+ let walletAddress = null;
42
+ let frozen = false;
43
+ // Wallet key management (private key never exposed via MCP output)
44
+ let storedPrivateKey = null;
45
+ let publicClient = null;
46
+ let walletClient = null;
47
+ const sessionKeys = new Map();
48
+ const permissionGrants = new Map();
49
+ // ============================================================
50
+ // BigInt-safe JSON serializer
51
+ // ============================================================
52
+ function toJSON(value, indent) {
53
+ return JSON.stringify(value, (_key, v) => typeof v === 'bigint' ? v.toString() : v, indent);
54
+ }
55
+ // ============================================================
56
+ // Client initialization helper
57
+ // ============================================================
58
+ function initializeClients(privateKey) {
59
+ const account = privateKeyToAccount(privateKey);
60
+ publicClient = createPublicClient({
61
+ chain: sepolia,
62
+ transport: http(PROVIDER_URL),
63
+ });
64
+ walletClient = createWalletClient({
65
+ account,
66
+ chain: sepolia,
67
+ transport: http(PROVIDER_URL),
68
+ });
69
+ storedPrivateKey = privateKey;
70
+ walletAddress = account.address;
71
+ }
72
+ function requireWallet() {
73
+ if (!publicClient || !walletClient || !walletAddress || !storedPrivateKey) {
74
+ throw new Error('No wallet created. Call warden_create_wallet first.');
75
+ }
76
+ return { publicClient, walletClient, address: walletAddress };
77
+ }
78
+ // ============================================================
79
+ // MCP Server Setup
80
+ // ============================================================
81
+ const server = new McpServer({
82
+ name: 'warden-wallet',
83
+ version: '0.3.0',
84
+ });
85
+ // ============================================================
86
+ // Tool 1: Create Wallet
87
+ // ============================================================
88
+ server.tool('warden_create_wallet', 'Create a new policy-enforced wallet for the AI agent. Generates a random Ethereum keypair, initializes viem clients for Sepolia, and sets up policy enforcement with spending limits, anomaly detection, and audit logging.', {
89
+ agentId: z.string().describe('Unique identifier for this agent'),
90
+ maxPerTx: z.number().describe('Max spend per transaction in USDT (e.g. 100)'),
91
+ dailyLimit: z.number().describe('Daily spending cap in USDT (e.g. 500)'),
92
+ approvalThreshold: z.number().describe('Amount above which human approval is needed (e.g. 200)'),
93
+ cooldownSeconds: z.number().default(30).describe('Min seconds between transactions'),
94
+ }, async ({ agentId, maxPerTx, dailyLimit, approvalThreshold, cooldownSeconds }) => {
95
+ const privateKey = generatePrivateKey();
96
+ initializeClients(privateKey);
97
+ const policy = {
98
+ agentId,
99
+ maxPerTx: BigInt(maxPerTx) * 1000000n,
100
+ dailyLimit: BigInt(dailyLimit) * 1000000n,
101
+ requireApprovalAbove: BigInt(approvalThreshold) * 1000000n,
102
+ allowedTokens: [],
103
+ blockedTokens: [],
104
+ allowedRecipients: [],
105
+ blockedRecipients: [],
106
+ allowedChains: ['ethereum'],
107
+ cooldownMs: cooldownSeconds * 1000,
108
+ anomalyDetection: {
109
+ maxTxPerHour: 20,
110
+ maxRecipientsPerHour: 5,
111
+ largeTransactionPct: 50,
112
+ },
113
+ };
114
+ policyEngine = new PolicyEngine(policy);
115
+ auditLogger = new AuditLogger({ maxEntries: 10000 });
116
+ frozen = false;
117
+ return {
118
+ content: [{
119
+ type: 'text',
120
+ text: toJSON({
121
+ success: true,
122
+ address: walletAddress,
123
+ agentId,
124
+ chain: 'sepolia',
125
+ rpcUrl: PROVIDER_URL,
126
+ policy: {
127
+ maxPerTx: `${maxPerTx} USDT`,
128
+ dailyLimit: `${dailyLimit} USDT`,
129
+ approvalThreshold: `${approvalThreshold} USDT`,
130
+ cooldownSeconds,
131
+ },
132
+ note: 'Wallet created with policy enforcement. Fund with test ETH/USDT on Sepolia before sending transactions.',
133
+ }, 2),
134
+ }],
135
+ };
136
+ });
137
+ // ============================================================
138
+ // Tool 2: Get Balance
139
+ // ============================================================
140
+ server.tool('warden_get_balance', 'Check the wallet balance on Sepolia. Returns native ETH balance, or ERC-20 token balance if a token address is provided.', {
141
+ tokenAddress: z.string().optional().describe('ERC-20 token address. Omit for native ETH.'),
142
+ }, async ({ tokenAddress }) => {
143
+ let wallet;
144
+ try {
145
+ wallet = requireWallet();
146
+ }
147
+ catch (err) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ return {
150
+ content: [{ type: 'text', text: toJSON({ error: message }) }],
151
+ isError: true,
152
+ };
153
+ }
154
+ try {
155
+ if (tokenAddress) {
156
+ let decimals;
157
+ try {
158
+ decimals = await wallet.publicClient.readContract({
159
+ address: tokenAddress,
160
+ abi: ERC20_ABI,
161
+ functionName: 'decimals',
162
+ });
163
+ }
164
+ catch {
165
+ // Default to 6 decimals (USDT standard) if decimals() call fails
166
+ decimals = 6;
167
+ }
168
+ const tokenBalance = await wallet.publicClient.readContract({
169
+ address: tokenAddress,
170
+ abi: ERC20_ABI,
171
+ functionName: 'balanceOf',
172
+ args: [wallet.address],
173
+ });
174
+ const formatted = formatUnits(tokenBalance, decimals);
175
+ return {
176
+ content: [{
177
+ type: 'text',
178
+ text: toJSON({
179
+ address: wallet.address,
180
+ tokenAddress,
181
+ balance: formatted,
182
+ decimals,
183
+ rawBalance: tokenBalance.toString(),
184
+ }),
185
+ }],
186
+ };
187
+ }
188
+ const ethBalance = await wallet.publicClient.getBalance({ address: wallet.address });
189
+ const formatted = formatEther(ethBalance);
190
+ return {
191
+ content: [{
192
+ type: 'text',
193
+ text: toJSON({
194
+ address: wallet.address,
195
+ balance: `${formatted} ETH`,
196
+ rawBalance: ethBalance.toString(),
197
+ }),
198
+ }],
199
+ };
200
+ }
201
+ catch (err) {
202
+ const message = err instanceof Error ? err.message : String(err);
203
+ return {
204
+ content: [{ type: 'text', text: toJSON({ error: `Failed to fetch balance: ${message}` }) }],
205
+ isError: true,
206
+ };
207
+ }
208
+ });
209
+ // ============================================================
210
+ // Tool 3: Transfer
211
+ // ============================================================
212
+ server.tool('warden_transfer', 'Send ERC-20 tokens with policy enforcement. Evaluates the transfer against all 10 policy rules including spending limits, cooldowns, and anomaly detection. Submits the transaction on-chain if approved.', {
213
+ recipient: z.string().describe('Recipient wallet address (0x...)'),
214
+ amount: z.number().describe('Amount in USDT (e.g. 50 for 50 USDT)'),
215
+ tokenAddress: z.string().describe('ERC-20 token contract address'),
216
+ }, async ({ recipient, amount, tokenAddress }) => {
217
+ if (!policyEngine || !auditLogger) {
218
+ return {
219
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
220
+ isError: true,
221
+ };
222
+ }
223
+ let wallet;
224
+ try {
225
+ wallet = requireWallet();
226
+ }
227
+ catch (err) {
228
+ const message = err instanceof Error ? err.message : String(err);
229
+ return {
230
+ content: [{ type: 'text', text: toJSON({ error: message }) }],
231
+ isError: true,
232
+ };
233
+ }
234
+ if (frozen) {
235
+ return {
236
+ content: [{
237
+ type: 'text',
238
+ text: toJSON({
239
+ success: false,
240
+ blocked: true,
241
+ reason: 'Wallet is frozen. All operations are suspended.',
242
+ ruleTriggered: 'frozen',
243
+ }),
244
+ }],
245
+ };
246
+ }
247
+ const valueMicro = BigInt(Math.round(amount * 1e6));
248
+ const decision = policyEngine.evaluate(recipient, valueMicro, tokenAddress, 'ethereum');
249
+ auditLogger.log(decision);
250
+ if (!decision.approved) {
251
+ return {
252
+ content: [{
253
+ type: 'text',
254
+ text: toJSON({
255
+ success: false,
256
+ blocked: true,
257
+ reason: decision.reason,
258
+ ruleTriggered: decision.ruleTriggered,
259
+ riskScore: decision.riskScore,
260
+ }),
261
+ }],
262
+ };
263
+ }
264
+ try {
265
+ // ERC-20 transfer: encode transfer(recipient, amount) calldata
266
+ const transferAmount = parseUnits(amount.toString(), 6);
267
+ const data = encodeFunctionData({
268
+ abi: ERC20_ABI,
269
+ functionName: 'transfer',
270
+ args: [recipient, transferAmount],
271
+ });
272
+ const account = privateKeyToAccount(storedPrivateKey);
273
+ const hash = await wallet.walletClient.sendTransaction({
274
+ account,
275
+ to: tokenAddress,
276
+ data,
277
+ chain: sepolia,
278
+ });
279
+ const receipt = await wallet.publicClient.waitForTransactionReceipt({ hash });
280
+ if (receipt.status === 'reverted') {
281
+ return {
282
+ content: [{
283
+ type: 'text',
284
+ text: toJSON({
285
+ success: false,
286
+ error: 'Transaction reverted on-chain',
287
+ txHash: hash,
288
+ blockNumber: Number(receipt.blockNumber),
289
+ }),
290
+ }],
291
+ isError: true,
292
+ };
293
+ }
294
+ policyEngine.recordTransaction(valueMicro, recipient);
295
+ auditLogger.log(decision, {
296
+ hash,
297
+ blockNumber: Number(receipt.blockNumber),
298
+ gasUsed: receipt.gasUsed,
299
+ });
300
+ return {
301
+ content: [{
302
+ type: 'text',
303
+ text: toJSON({
304
+ success: true,
305
+ txHash: hash,
306
+ blockNumber: Number(receipt.blockNumber),
307
+ gasUsed: receipt.gasUsed.toString(),
308
+ amount: `${amount} USDT`,
309
+ recipient,
310
+ riskScore: decision.riskScore,
311
+ }),
312
+ }],
313
+ };
314
+ }
315
+ catch (err) {
316
+ const message = err instanceof Error ? err.message : String(err);
317
+ return {
318
+ content: [{ type: 'text', text: toJSON({ error: `Transfer failed: ${message}` }) }],
319
+ isError: true,
320
+ };
321
+ }
322
+ });
323
+ // ============================================================
324
+ // Tool 4: Get Policy Status
325
+ // ============================================================
326
+ server.tool('warden_get_policy_status', 'View current spending limits, remaining budget, window reset time, and audit statistics including approved/blocked counts and top block reasons.', {}, async () => {
327
+ if (!policyEngine || !auditLogger) {
328
+ return {
329
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
330
+ isError: true,
331
+ };
332
+ }
333
+ const status = policyEngine.getSpendingStatus();
334
+ const stats = auditLogger.getStats();
335
+ return {
336
+ content: [{
337
+ type: 'text',
338
+ text: toJSON({
339
+ frozen,
340
+ spending: {
341
+ spent: `${Number(status.spent) / 1e6} USDT`,
342
+ remaining: `${Number(status.remaining) / 1e6} USDT`,
343
+ windowResets: new Date(status.windowResets).toISOString(),
344
+ },
345
+ stats: {
346
+ totalTransactions: stats.total,
347
+ approved: stats.approved,
348
+ blocked: stats.blocked,
349
+ topBlockReasons: stats.topBlockReasons,
350
+ },
351
+ activeSessionKeys: sessionKeys.size,
352
+ }, 2),
353
+ }],
354
+ };
355
+ });
356
+ // ============================================================
357
+ // Tool 5: Get Audit Log
358
+ // ============================================================
359
+ server.tool('warden_get_audit_log', 'View recent transaction history with approve/block decisions, risk scores, and triggered rules. Supports filtering by approval status.', {
360
+ limit: z.number().default(10).describe('Number of entries to return'),
361
+ approvedOnly: z.boolean().optional().describe('Filter: true=approved only, false=blocked only, omit=all'),
362
+ }, async ({ limit, approvedOnly }) => {
363
+ if (!auditLogger) {
364
+ return {
365
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
366
+ isError: true,
367
+ };
368
+ }
369
+ const entries = auditLogger.getEntries({ approved: approvedOnly, limit });
370
+ return {
371
+ content: [{
372
+ type: 'text',
373
+ text: toJSON(entries.map(e => ({
374
+ approved: e.approved,
375
+ reason: e.reason,
376
+ rule: e.ruleTriggered,
377
+ riskScore: e.riskScore,
378
+ amount: `${Number(e.transactionDetails.value) / 1e6} USDT`,
379
+ to: e.transactionDetails.to,
380
+ time: new Date(e.timestamp).toISOString(),
381
+ txHash: e.txHash,
382
+ })), 2),
383
+ }],
384
+ };
385
+ });
386
+ // ============================================================
387
+ // Tool 6: Update Policy
388
+ // ============================================================
389
+ server.tool('warden_update_policy', 'Modify policy rules at runtime. Can update per-transaction limits, daily limits, and cooldown periods without recreating the wallet.', {
390
+ maxPerTx: z.number().optional().describe('New max per transaction in USDT'),
391
+ dailyLimit: z.number().optional().describe('New daily limit in USDT'),
392
+ cooldownSeconds: z.number().optional().describe('New cooldown in seconds'),
393
+ }, async ({ maxPerTx, dailyLimit, cooldownSeconds }) => {
394
+ if (!policyEngine) {
395
+ return {
396
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
397
+ isError: true,
398
+ };
399
+ }
400
+ const updates = {};
401
+ const updatedFields = [];
402
+ if (maxPerTx !== undefined) {
403
+ updates.maxPerTx = BigInt(maxPerTx) * 1000000n;
404
+ updatedFields.push('maxPerTx');
405
+ }
406
+ if (dailyLimit !== undefined) {
407
+ updates.dailyLimit = BigInt(dailyLimit) * 1000000n;
408
+ updatedFields.push('dailyLimit');
409
+ }
410
+ if (cooldownSeconds !== undefined) {
411
+ updates.cooldownMs = cooldownSeconds * 1000;
412
+ updatedFields.push('cooldownMs');
413
+ }
414
+ policyEngine.updatePolicy(updates);
415
+ return {
416
+ content: [{
417
+ type: 'text',
418
+ text: toJSON({ success: true, updatedFields }),
419
+ }],
420
+ };
421
+ });
422
+ // ============================================================
423
+ // Tool 7: Freeze
424
+ // ============================================================
425
+ server.tool('warden_freeze', 'EMERGENCY: Freeze all wallet operations immediately. No transfers will be allowed until unfrozen.', {}, async () => {
426
+ frozen = true;
427
+ return {
428
+ content: [{
429
+ type: 'text',
430
+ text: toJSON({
431
+ success: true,
432
+ frozen: true,
433
+ message: 'All wallet operations are now frozen.',
434
+ }),
435
+ }],
436
+ };
437
+ });
438
+ // ============================================================
439
+ // Tool 8: Unfreeze
440
+ // ============================================================
441
+ server.tool('warden_unfreeze', 'Resume wallet operations after an emergency freeze.', {}, async () => {
442
+ frozen = false;
443
+ return {
444
+ content: [{
445
+ type: 'text',
446
+ text: toJSON({
447
+ success: true,
448
+ frozen: false,
449
+ message: 'Wallet operations have been resumed.',
450
+ }),
451
+ }],
452
+ };
453
+ });
454
+ // ============================================================
455
+ // Tool 9: Create Session Key (on-chain via PolicyDelegate)
456
+ // ============================================================
457
+ server.tool('warden_create_session_key', 'Create a scoped session key for a sub-agent with its own spending limits and validity period. Requires POLICY_DELEGATE_ADDRESS env var for on-chain EIP-7702 session key creation.', {
458
+ agentAddress: z.string().describe('Sub-agent wallet address to grant session key to'),
459
+ maxPerTx: z.number().describe('Max spend per transaction in USDT for this session key'),
460
+ dailyLimit: z.number().describe('Daily spending cap in USDT for this session key'),
461
+ validForSeconds: z.number().describe('How long this session key is valid (seconds)'),
462
+ cooldownSeconds: z.number().describe('Min seconds between transactions for this session key'),
463
+ }, async ({ agentAddress, maxPerTx, dailyLimit, validForSeconds, cooldownSeconds }) => {
464
+ if (!POLICY_DELEGATE_ADDRESS) {
465
+ return {
466
+ content: [{
467
+ type: 'text',
468
+ text: toJSON({ error: 'On-chain session key creation requires POLICY_DELEGATE_ADDRESS env var to be set.' }),
469
+ }],
470
+ isError: true,
471
+ };
472
+ }
473
+ let wallet;
474
+ try {
475
+ wallet = requireWallet();
476
+ }
477
+ catch (err) {
478
+ const message = err instanceof Error ? err.message : String(err);
479
+ return {
480
+ content: [{ type: 'text', text: toJSON({ error: message }) }],
481
+ isError: true,
482
+ };
483
+ }
484
+ try {
485
+ const now = Math.floor(Date.now() / 1000);
486
+ const maxPerTxWei = BigInt(maxPerTx) * 1000000n;
487
+ const dailyLimitWei = BigInt(dailyLimit) * 1000000n;
488
+ const data = encodeFunctionData({
489
+ abi: POLICY_DELEGATE_ABI,
490
+ functionName: 'createSessionKey',
491
+ args: [
492
+ wallet.address,
493
+ agentAddress,
494
+ maxPerTxWei,
495
+ dailyLimitWei,
496
+ now,
497
+ now + validForSeconds,
498
+ BigInt(cooldownSeconds),
499
+ ],
500
+ });
501
+ const account = privateKeyToAccount(storedPrivateKey);
502
+ const hash = await wallet.walletClient.sendTransaction({
503
+ account,
504
+ to: wallet.address,
505
+ data,
506
+ chain: sepolia,
507
+ });
508
+ const receipt = await wallet.publicClient.waitForTransactionReceipt({ hash });
509
+ if (receipt.status === 'reverted') {
510
+ return {
511
+ content: [{
512
+ type: 'text',
513
+ text: toJSON({
514
+ success: false,
515
+ error: 'Session key creation transaction reverted',
516
+ txHash: hash,
517
+ }),
518
+ }],
519
+ isError: true,
520
+ };
521
+ }
522
+ const sessionData = {
523
+ agentAddress,
524
+ maxPerTx: maxPerTxWei,
525
+ dailyLimit: dailyLimitWei,
526
+ validUntil: (now + validForSeconds) * 1000,
527
+ cooldownSeconds,
528
+ createdAt: Date.now(),
529
+ revoked: false,
530
+ txHash: hash,
531
+ };
532
+ sessionKeys.set(agentAddress.toLowerCase(), sessionData);
533
+ return {
534
+ content: [{
535
+ type: 'text',
536
+ text: toJSON({
537
+ success: true,
538
+ sessionKeyAddress: agentAddress,
539
+ grantedTo: agentAddress,
540
+ txHash: hash,
541
+ blockNumber: Number(receipt.blockNumber),
542
+ permissions: {
543
+ maxPerTx: `${maxPerTx} USDT`,
544
+ dailyLimit: `${dailyLimit} USDT`,
545
+ cooldownSeconds,
546
+ },
547
+ validUntil: new Date(sessionData.validUntil).toISOString(),
548
+ }, 2),
549
+ }],
550
+ };
551
+ }
552
+ catch (err) {
553
+ const message = err instanceof Error ? err.message : String(err);
554
+ return {
555
+ content: [{ type: 'text', text: toJSON({ error: `Session key creation failed: ${message}` }) }],
556
+ isError: true,
557
+ };
558
+ }
559
+ });
560
+ // ============================================================
561
+ // Tool 10: Revoke Session Key
562
+ // ============================================================
563
+ server.tool('warden_revoke_session_key', 'Revoke a previously created session key, immediately disabling its access. If POLICY_DELEGATE_ADDRESS is set, also revokes on-chain.', {
564
+ sessionKeyAddress: z.string().describe('Address of the session key to revoke'),
565
+ }, async ({ sessionKeyAddress }) => {
566
+ const key = sessionKeyAddress.toLowerCase();
567
+ const session = sessionKeys.get(key);
568
+ if (!session) {
569
+ return {
570
+ content: [{
571
+ type: 'text',
572
+ text: toJSON({
573
+ success: false,
574
+ error: `Session key ${sessionKeyAddress} not found.`,
575
+ }),
576
+ }],
577
+ isError: true,
578
+ };
579
+ }
580
+ if (POLICY_DELEGATE_ADDRESS && publicClient && walletClient && storedPrivateKey) {
581
+ try {
582
+ const wallet = requireWallet();
583
+ const account = privateKeyToAccount(storedPrivateKey);
584
+ const data = encodeFunctionData({
585
+ abi: POLICY_DELEGATE_ABI,
586
+ functionName: 'revokeSessionKey',
587
+ args: [wallet.address, sessionKeyAddress],
588
+ });
589
+ const hash = await wallet.walletClient.sendTransaction({
590
+ account,
591
+ to: wallet.address,
592
+ data,
593
+ chain: sepolia,
594
+ });
595
+ const receipt = await wallet.publicClient.waitForTransactionReceipt({ hash });
596
+ if (receipt.status === 'reverted') {
597
+ return {
598
+ content: [{
599
+ type: 'text',
600
+ text: toJSON({
601
+ success: false,
602
+ error: 'Revoke session key transaction reverted',
603
+ txHash: hash,
604
+ }),
605
+ }],
606
+ isError: true,
607
+ };
608
+ }
609
+ session.revoked = true;
610
+ return {
611
+ content: [{
612
+ type: 'text',
613
+ text: toJSON({
614
+ success: true,
615
+ sessionKeyAddress,
616
+ revoked: true,
617
+ txHash: hash,
618
+ blockNumber: Number(receipt.blockNumber),
619
+ message: 'Session key has been revoked on-chain.',
620
+ }),
621
+ }],
622
+ };
623
+ }
624
+ catch (err) {
625
+ const message = err instanceof Error ? err.message : String(err);
626
+ return {
627
+ content: [{ type: 'text', text: toJSON({ error: `Revoke session key failed: ${message}` }) }],
628
+ isError: true,
629
+ };
630
+ }
631
+ }
632
+ session.revoked = true;
633
+ return {
634
+ content: [{
635
+ type: 'text',
636
+ text: toJSON({
637
+ success: true,
638
+ sessionKeyAddress,
639
+ revoked: true,
640
+ message: 'Session key has been revoked locally. Set POLICY_DELEGATE_ADDRESS to also revoke on-chain.',
641
+ }),
642
+ }],
643
+ };
644
+ });
645
+ // ============================================================
646
+ // Tool 11: Register Identity (ERC-8004)
647
+ // ============================================================
648
+ server.tool('warden_register_identity', 'Register the agent on the ERC-8004 Agent Identity Registry. Requires ERC8004_IDENTITY_REGISTRY env var for the on-chain registry contract address.', {
649
+ agentName: z.string().describe('Human-readable name for this agent'),
650
+ capabilities: z.array(z.string()).describe('List of agent capabilities (e.g. ["transfer", "swap", "bridge"])'),
651
+ }, async ({ agentName, capabilities }) => {
652
+ if (!ERC8004_IDENTITY_REGISTRY) {
653
+ return {
654
+ content: [{
655
+ type: 'text',
656
+ text: toJSON({ error: 'ERC-8004 registry address not configured. Set ERC8004_IDENTITY_REGISTRY env var.' }),
657
+ }],
658
+ isError: true,
659
+ };
660
+ }
661
+ let wallet;
662
+ try {
663
+ wallet = requireWallet();
664
+ }
665
+ catch (err) {
666
+ const message = err instanceof Error ? err.message : String(err);
667
+ return {
668
+ content: [{ type: 'text', text: toJSON({ error: message }) }],
669
+ isError: true,
670
+ };
671
+ }
672
+ try {
673
+ const account = privateKeyToAccount(storedPrivateKey);
674
+ const data = encodeFunctionData({
675
+ abi: ERC8004_ABI,
676
+ functionName: 'registerAgent',
677
+ args: [agentName, capabilities],
678
+ });
679
+ const hash = await wallet.walletClient.sendTransaction({
680
+ account,
681
+ to: ERC8004_IDENTITY_REGISTRY,
682
+ data,
683
+ chain: sepolia,
684
+ });
685
+ const receipt = await wallet.publicClient.waitForTransactionReceipt({ hash });
686
+ if (receipt.status === 'reverted') {
687
+ return {
688
+ content: [{
689
+ type: 'text',
690
+ text: toJSON({
691
+ success: false,
692
+ error: 'Identity registration transaction reverted',
693
+ txHash: hash,
694
+ }),
695
+ }],
696
+ isError: true,
697
+ };
698
+ }
699
+ return {
700
+ content: [{
701
+ type: 'text',
702
+ text: toJSON({
703
+ success: true,
704
+ registry: 'ERC-8004 Agent Identity Registry',
705
+ registryAddress: ERC8004_IDENTITY_REGISTRY,
706
+ agentName,
707
+ capabilities,
708
+ owner: wallet.address,
709
+ txHash: hash,
710
+ blockNumber: Number(receipt.blockNumber),
711
+ }, 2),
712
+ }],
713
+ };
714
+ }
715
+ catch (err) {
716
+ const message = err instanceof Error ? err.message : String(err);
717
+ return {
718
+ content: [{ type: 'text', text: toJSON({ error: `Identity registration failed: ${message}` }) }],
719
+ isError: true,
720
+ };
721
+ }
722
+ });
723
+ // ============================================================
724
+ // Tool 12: Grant Permissions (ERC-7715)
725
+ // ============================================================
726
+ server.tool('warden_grant_permissions', 'Grant scoped permissions to an AI agent following ERC-7715 standard. Updates policy engine limits and optionally creates an on-chain session key.', {
727
+ agentId: z.string().describe('Agent identifier to grant permissions to'),
728
+ permissions: z.array(z.object({
729
+ type: z.enum(['token-transfer', 'contract-call', 'native-transfer']).describe('Permission type'),
730
+ token: z.string().optional().describe('Token address for token-transfer type'),
731
+ maxPerTx: z.number().optional().describe('Max per transaction in USDT'),
732
+ dailyLimit: z.number().optional().describe('Daily spending limit in USDT'),
733
+ allowedRecipients: z.array(z.string()).optional().describe('Allowed recipient addresses'),
734
+ allowedContracts: z.array(z.string()).optional().describe('Allowed contract addresses for contract-call type'),
735
+ })).describe('Scoped permissions to grant'),
736
+ expiry: z.number().describe('Unix timestamp (seconds) when permissions expire'),
737
+ cooldownSeconds: z.number().default(30).describe('Min seconds between transactions'),
738
+ }, async ({ agentId, permissions, expiry, cooldownSeconds }) => {
739
+ if (!policyEngine || !auditLogger) {
740
+ return {
741
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
742
+ isError: true,
743
+ };
744
+ }
745
+ if (permissions.length === 0) {
746
+ return {
747
+ content: [{ type: 'text', text: toJSON({ error: 'At least one permission must be specified.' }) }],
748
+ isError: true,
749
+ };
750
+ }
751
+ const now = Math.floor(Date.now() / 1000);
752
+ if (expiry <= now) {
753
+ return {
754
+ content: [{ type: 'text', text: toJSON({ error: `Expiry ${expiry} is in the past. Current time: ${now}` }) }],
755
+ isError: true,
756
+ };
757
+ }
758
+ // Derive aggregate limits from permissions: take the highest maxPerTx and dailyLimit across all scopes
759
+ let aggregateMaxPerTx = 0;
760
+ let aggregateDailyLimit = 0;
761
+ const allowedTokens = [];
762
+ const allowedRecipients = [];
763
+ for (const perm of permissions) {
764
+ if (perm.maxPerTx !== undefined && perm.maxPerTx > aggregateMaxPerTx) {
765
+ aggregateMaxPerTx = perm.maxPerTx;
766
+ }
767
+ if (perm.dailyLimit !== undefined && perm.dailyLimit > aggregateDailyLimit) {
768
+ aggregateDailyLimit = perm.dailyLimit;
769
+ }
770
+ if (perm.type === 'token-transfer' && perm.token) {
771
+ allowedTokens.push(perm.token.toLowerCase());
772
+ }
773
+ if (perm.allowedRecipients) {
774
+ for (const r of perm.allowedRecipients) {
775
+ if (!allowedRecipients.includes(r.toLowerCase())) {
776
+ allowedRecipients.push(r.toLowerCase());
777
+ }
778
+ }
779
+ }
780
+ }
781
+ const policyUpdates = {
782
+ cooldownMs: cooldownSeconds * 1000,
783
+ sessionKey: {
784
+ address: agentId,
785
+ validUntil: expiry * 1000,
786
+ },
787
+ };
788
+ if (aggregateMaxPerTx > 0) {
789
+ policyUpdates.maxPerTx = BigInt(aggregateMaxPerTx) * 1000000n;
790
+ }
791
+ if (aggregateDailyLimit > 0) {
792
+ policyUpdates.dailyLimit = BigInt(aggregateDailyLimit) * 1000000n;
793
+ }
794
+ if (allowedTokens.length > 0) {
795
+ policyUpdates.allowedTokens = allowedTokens;
796
+ }
797
+ if (allowedRecipients.length > 0) {
798
+ policyUpdates.allowedRecipients = allowedRecipients;
799
+ }
800
+ // On-chain session key if delegate is configured
801
+ let txHash;
802
+ if (POLICY_DELEGATE_ADDRESS && publicClient && walletClient && storedPrivateKey) {
803
+ try {
804
+ const wallet = requireWallet();
805
+ const account = privateKeyToAccount(storedPrivateKey);
806
+ const maxPerTxWei = BigInt(aggregateMaxPerTx) * 1000000n;
807
+ const dailyLimitWei = BigInt(aggregateDailyLimit) * 1000000n;
808
+ const data = encodeFunctionData({
809
+ abi: POLICY_DELEGATE_ABI,
810
+ functionName: 'createSessionKey',
811
+ args: [
812
+ wallet.address,
813
+ agentId,
814
+ maxPerTxWei,
815
+ dailyLimitWei,
816
+ now,
817
+ expiry,
818
+ BigInt(cooldownSeconds),
819
+ ],
820
+ });
821
+ const hash = await wallet.walletClient.sendTransaction({
822
+ account,
823
+ to: wallet.address,
824
+ data,
825
+ chain: sepolia,
826
+ });
827
+ const receipt = await wallet.publicClient.waitForTransactionReceipt({ hash });
828
+ if (receipt.status === 'reverted') {
829
+ return {
830
+ content: [{
831
+ type: 'text',
832
+ text: toJSON({
833
+ success: false,
834
+ error: 'On-chain session key creation reverted',
835
+ txHash: hash,
836
+ }),
837
+ }],
838
+ isError: true,
839
+ };
840
+ }
841
+ txHash = hash;
842
+ }
843
+ catch (err) {
844
+ const message = err instanceof Error ? err.message : String(err);
845
+ return {
846
+ content: [{ type: 'text', text: toJSON({ error: `On-chain permission grant failed: ${message}` }) }],
847
+ isError: true,
848
+ };
849
+ }
850
+ }
851
+ // Commit local state only after on-chain succeeds (or if no on-chain needed)
852
+ policyEngine.updatePolicy(policyUpdates);
853
+ frozen = false;
854
+ const grant = {
855
+ agentId,
856
+ permissions,
857
+ expiry,
858
+ cooldownSeconds,
859
+ grantedAt: Date.now(),
860
+ txHash,
861
+ };
862
+ permissionGrants.set(agentId, grant);
863
+ return {
864
+ content: [{
865
+ type: 'text',
866
+ text: toJSON({
867
+ success: true,
868
+ standard: 'ERC-7715',
869
+ agentId,
870
+ permissions: permissions.map(p => ({
871
+ type: p.type,
872
+ token: p.token,
873
+ maxPerTx: p.maxPerTx !== undefined ? `${p.maxPerTx} USDT` : undefined,
874
+ dailyLimit: p.dailyLimit !== undefined ? `${p.dailyLimit} USDT` : undefined,
875
+ allowedRecipients: p.allowedRecipients,
876
+ allowedContracts: p.allowedContracts,
877
+ })),
878
+ expiry: new Date(expiry * 1000).toISOString(),
879
+ cooldownSeconds,
880
+ txHash: txHash ?? null,
881
+ }, 2),
882
+ }],
883
+ };
884
+ });
885
+ // ============================================================
886
+ // Tool 13: Revoke Permissions (ERC-7715)
887
+ // ============================================================
888
+ server.tool('warden_revoke_permissions', 'Revoke all permissions from an AI agent following ERC-7715 standard. Zeros out policy limits, freezes the agent, and optionally revokes the on-chain session key.', {
889
+ agentId: z.string().describe('Agent identifier to revoke permissions from'),
890
+ }, async ({ agentId }) => {
891
+ if (!policyEngine) {
892
+ return {
893
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
894
+ isError: true,
895
+ };
896
+ }
897
+ const grant = permissionGrants.get(agentId);
898
+ if (!grant) {
899
+ return {
900
+ content: [{ type: 'text', text: toJSON({ error: `No ERC-7715 permissions found for agent: ${agentId}` }) }],
901
+ isError: true,
902
+ };
903
+ }
904
+ // On-chain revocation if delegate is configured
905
+ let txHash;
906
+ if (POLICY_DELEGATE_ADDRESS && publicClient && walletClient && storedPrivateKey) {
907
+ try {
908
+ const wallet = requireWallet();
909
+ const account = privateKeyToAccount(storedPrivateKey);
910
+ const data = encodeFunctionData({
911
+ abi: POLICY_DELEGATE_ABI,
912
+ functionName: 'revokeSessionKey',
913
+ args: [wallet.address, agentId],
914
+ });
915
+ const hash = await wallet.walletClient.sendTransaction({
916
+ account,
917
+ to: wallet.address,
918
+ data,
919
+ chain: sepolia,
920
+ });
921
+ const receipt = await wallet.publicClient.waitForTransactionReceipt({ hash });
922
+ if (receipt.status === 'reverted') {
923
+ return {
924
+ content: [{
925
+ type: 'text',
926
+ text: toJSON({
927
+ success: false,
928
+ error: 'On-chain session key revocation reverted',
929
+ txHash: hash,
930
+ }),
931
+ }],
932
+ isError: true,
933
+ };
934
+ }
935
+ txHash = hash;
936
+ }
937
+ catch (err) {
938
+ const message = err instanceof Error ? err.message : String(err);
939
+ return {
940
+ content: [{ type: 'text', text: toJSON({ error: `On-chain permission revocation failed: ${message}` }) }],
941
+ isError: true,
942
+ };
943
+ }
944
+ }
945
+ // Zero out policy limits and freeze
946
+ policyEngine.updatePolicy({
947
+ maxPerTx: 0n,
948
+ dailyLimit: 0n,
949
+ allowedTokens: [],
950
+ allowedRecipients: [],
951
+ sessionKey: undefined,
952
+ });
953
+ frozen = true;
954
+ permissionGrants.delete(agentId);
955
+ return {
956
+ content: [{
957
+ type: 'text',
958
+ text: toJSON({
959
+ success: true,
960
+ standard: 'ERC-7715',
961
+ agentId,
962
+ revoked: true,
963
+ frozen: true,
964
+ txHash: txHash ?? null,
965
+ message: 'All permissions revoked. Wallet is frozen.',
966
+ }, 2),
967
+ }],
968
+ };
969
+ });
970
+ // ============================================================
971
+ // Tool 14: Get Permissions (ERC-7715)
972
+ // ============================================================
973
+ server.tool('warden_get_permissions', 'Query current permissions granted to an AI agent following ERC-7715 standard. Returns policy limits, expiry, spending status, and compliance info.', {
974
+ agentId: z.string().describe('Agent identifier to query permissions for'),
975
+ }, async ({ agentId }) => {
976
+ if (!policyEngine) {
977
+ return {
978
+ content: [{ type: 'text', text: toJSON({ error: 'No wallet created. Call warden_create_wallet first.' }) }],
979
+ isError: true,
980
+ };
981
+ }
982
+ const grant = permissionGrants.get(agentId);
983
+ if (!grant) {
984
+ return {
985
+ content: [{ type: 'text', text: toJSON({ error: `No ERC-7715 permissions found for agent: ${agentId}` }) }],
986
+ isError: true,
987
+ };
988
+ }
989
+ const policy = policyEngine.getPolicy();
990
+ const spending = policyEngine.getSpendingStatus();
991
+ const now = Math.floor(Date.now() / 1000);
992
+ const isExpired = grant.expiry <= now;
993
+ return {
994
+ content: [{
995
+ type: 'text',
996
+ text: toJSON({
997
+ standard: 'ERC-7715',
998
+ agentId,
999
+ policy: {
1000
+ maxPerTx: `${Number(policy.maxPerTx) / 1e6} USDT`,
1001
+ dailyLimit: `${Number(policy.dailyLimit) / 1e6} USDT`,
1002
+ cooldownMs: policy.cooldownMs,
1003
+ allowedTokens: policy.allowedTokens,
1004
+ allowedRecipients: policy.allowedRecipients,
1005
+ },
1006
+ permissions: grant.permissions.map(p => ({
1007
+ type: p.type,
1008
+ token: p.token,
1009
+ maxPerTx: p.maxPerTx !== undefined ? `${p.maxPerTx} USDT` : undefined,
1010
+ dailyLimit: p.dailyLimit !== undefined ? `${p.dailyLimit} USDT` : undefined,
1011
+ allowedRecipients: p.allowedRecipients,
1012
+ allowedContracts: p.allowedContracts,
1013
+ })),
1014
+ expiry: {
1015
+ timestamp: grant.expiry,
1016
+ iso: new Date(grant.expiry * 1000).toISOString(),
1017
+ expired: isExpired,
1018
+ remainingSeconds: isExpired ? 0 : grant.expiry - now,
1019
+ },
1020
+ spending: {
1021
+ spent: `${Number(spending.spent) / 1e6} USDT`,
1022
+ remaining: `${Number(spending.remaining) / 1e6} USDT`,
1023
+ windowResets: new Date(spending.windowResets).toISOString(),
1024
+ },
1025
+ frozen,
1026
+ grantedAt: new Date(grant.grantedAt).toISOString(),
1027
+ txHash: grant.txHash ?? null,
1028
+ erc7715Compliant: true,
1029
+ }, 2),
1030
+ }],
1031
+ };
1032
+ });
1033
+ // ============================================================
1034
+ // Main -- stdio transport
1035
+ // ============================================================
1036
+ async function main() {
1037
+ const transport = new StdioServerTransport();
1038
+ await server.connect(transport);
1039
+ console.error('[Warden MCP] Server running on stdio');
1040
+ console.error(`[Warden MCP] RPC: ${PROVIDER_URL}`);
1041
+ console.error(`[Warden MCP] PolicyDelegate: ${POLICY_DELEGATE_ADDRESS ?? 'not configured'}`);
1042
+ console.error(`[Warden MCP] ERC-8004 Registry: ${ERC8004_IDENTITY_REGISTRY ?? 'not configured'}`);
1043
+ console.error(`[Warden MCP] Default USDT: ${SEPOLIA_USDT}`);
1044
+ }
1045
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@aspect-warden/mcp-server",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "warden-mcp": "./dist/index.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "files": ["dist"],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@aspect-warden/policy-engine": "^0.3.0",
16
+ "@modelcontextprotocol/sdk": "^1.0.0",
17
+ "zod": "^3.22.0",
18
+ "viem": "^2.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.4.0",
22
+ "@types/node": "^20.0.0"
23
+ }
24
+ }