@aspect-warden/mcp-server 0.3.1 → 0.4.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 +152 -0
- package/dist/index.js +105 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @aspect-warden/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server that gives AI agents policy-enforced wallet capabilities on Sepolia. Works with Claude Desktop, OpenClaw, and any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @aspect-warden/mcp-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @aspect-warden/mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup with Claude Desktop
|
|
18
|
+
|
|
19
|
+
Add to your `claude_desktop_config.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"warden": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["@aspect-warden/mcp-server"],
|
|
27
|
+
"env": {
|
|
28
|
+
"RPC_URL": "https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY",
|
|
29
|
+
"SEPOLIA_USDT_ADDRESS": "0x7169D38820dfd117C3FA1f22a697dBA58d90BA06"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Restart Claude Desktop. The agent now has access to all Warden wallet tools.
|
|
37
|
+
|
|
38
|
+
## Setup with OpenClaw
|
|
39
|
+
|
|
40
|
+
Add the same config to your OpenClaw MCP settings, or install the warden-wallet skill:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx skills add tetherto/wdk-agent-skills
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Environment Variables
|
|
47
|
+
|
|
48
|
+
| Variable | Required | Default | Description |
|
|
49
|
+
|----------|----------|---------|-------------|
|
|
50
|
+
| `RPC_URL` | No | `https://rpc.sepolia.org` | Sepolia RPC endpoint |
|
|
51
|
+
| `POLICY_DELEGATE_ADDRESS` | No | — | Deployed PolicyDelegate contract for EIP-7702 |
|
|
52
|
+
| `SEPOLIA_USDT_ADDRESS` | No | `0x7169...BA06` | USDT token address on Sepolia |
|
|
53
|
+
| `ERC8004_IDENTITY_REGISTRY` | No | — | ERC-8004 identity registry address |
|
|
54
|
+
| `WARDEN_STORAGE_KEY` | No | machine-scoped | Passphrase for encrypting persisted wallet state |
|
|
55
|
+
|
|
56
|
+
## MCP Tools
|
|
57
|
+
|
|
58
|
+
Once connected, the agent can call these tools:
|
|
59
|
+
|
|
60
|
+
### Wallet Management
|
|
61
|
+
|
|
62
|
+
| Tool | Description |
|
|
63
|
+
|------|-------------|
|
|
64
|
+
| `warden_create_wallet` | Create a new agent wallet with policy enforcement |
|
|
65
|
+
| `warden_get_balance` | Check ETH and token balances |
|
|
66
|
+
| `warden_transfer` | Send ERC-20 tokens (validated against policy) |
|
|
67
|
+
| `warden_get_audit_log` | Fetch transaction decision history |
|
|
68
|
+
|
|
69
|
+
### Policy Management
|
|
70
|
+
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| `warden_setup_policy` | Configure spending policy from natural language |
|
|
74
|
+
| `warden_get_policy_status` | View current spending status and limits |
|
|
75
|
+
| `warden_update_policy` | Modify policy at runtime |
|
|
76
|
+
|
|
77
|
+
### On-Chain Enforcement (EIP-7702)
|
|
78
|
+
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `warden_delegate_to_policy` | Delegate EOA to PolicyDelegate contract |
|
|
82
|
+
| `warden_create_session_key` | Create scoped sub-agent permissions |
|
|
83
|
+
| `warden_revoke_session_key` | Revoke sub-agent access |
|
|
84
|
+
|
|
85
|
+
### Safety Controls
|
|
86
|
+
|
|
87
|
+
| Tool | Description |
|
|
88
|
+
|------|-------------|
|
|
89
|
+
| `warden_freeze` | Emergency halt all operations |
|
|
90
|
+
| `warden_unfreeze` | Resume after freeze |
|
|
91
|
+
| `warden_register_identity` | Register agent on ERC-8004 identity registry |
|
|
92
|
+
|
|
93
|
+
### Permissions (ERC-7715)
|
|
94
|
+
|
|
95
|
+
| Tool | Description |
|
|
96
|
+
|------|-------------|
|
|
97
|
+
| `warden_grant_permissions` | Grant permissions to an agent |
|
|
98
|
+
| `warden_revoke_permissions` | Revoke agent permissions |
|
|
99
|
+
| `warden_get_permissions` | View agent permission grants |
|
|
100
|
+
|
|
101
|
+
## Example Conversation
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
User: Create a conservative wallet. Max $50/day, USDT only.
|
|
105
|
+
|
|
106
|
+
Agent: [calls warden_create_wallet with maxPerTx=10, dailyLimit=50]
|
|
107
|
+
Created wallet 0xABC...123 with conservative policy.
|
|
108
|
+
- Max per transaction: 10 USDT
|
|
109
|
+
- Daily limit: 50 USDT
|
|
110
|
+
- Anomaly detection: enabled
|
|
111
|
+
|
|
112
|
+
User: Send 5 USDT to 0xDEF...456
|
|
113
|
+
|
|
114
|
+
Agent: [calls warden_transfer with amount=5, recipient=0xDEF...456]
|
|
115
|
+
Transfer approved and sent.
|
|
116
|
+
- Risk score: 12/100
|
|
117
|
+
- Daily remaining: 45 USDT
|
|
118
|
+
- Tx hash: 0x789...
|
|
119
|
+
|
|
120
|
+
User: Send 200 USDT to 0xDEF...456
|
|
121
|
+
|
|
122
|
+
Agent: [calls warden_transfer — PolicyEngine blocks it]
|
|
123
|
+
Transfer blocked: exceeds daily limit of 50 USDT.
|
|
124
|
+
Current daily spend: 5 USDT.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Wallet Persistence
|
|
128
|
+
|
|
129
|
+
Wallet private keys and policy configuration are automatically saved to `~/.warden/wallet-state.enc` using AES-256-CBC encryption. When the MCP server restarts (e.g., Claude Desktop relaunch), it restores the previous wallet — no need to call `warden_create_wallet` again.
|
|
130
|
+
|
|
131
|
+
- **Storage**: `~/.warden/wallet-state.enc` (encrypted, file permissions `0600`)
|
|
132
|
+
- **Encryption**: AES-256-CBC with key derived from `WARDEN_STORAGE_KEY` env var (or machine-scoped default)
|
|
133
|
+
- **What's persisted**: private key, agent ID, policy configuration
|
|
134
|
+
- **What's NOT persisted**: spending counters, audit logs, session keys (reset on restart)
|
|
135
|
+
|
|
136
|
+
To use a different wallet, simply call `warden_create_wallet` again — it overwrites the saved state.
|
|
137
|
+
|
|
138
|
+
## Architecture
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
AI Agent (Claude / OpenClaw)
|
|
142
|
+
↓ MCP Protocol (stdio)
|
|
143
|
+
@aspect-warden/mcp-server
|
|
144
|
+
↓ evaluates every tx
|
|
145
|
+
@aspect-warden/policy-engine (19 rules + anomaly detection)
|
|
146
|
+
↓ on-chain enforcement
|
|
147
|
+
PolicyDelegate.sol (EIP-7702 / Sepolia)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,10 @@ import { z } from 'zod';
|
|
|
5
5
|
import { createPublicClient, createWalletClient, http, parseAbi, encodeFunctionData, formatEther, formatUnits, parseUnits, } from 'viem';
|
|
6
6
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
7
7
|
import { sepolia } from 'viem/chains';
|
|
8
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
8
12
|
// ============================================================
|
|
9
13
|
// Configuration from environment
|
|
10
14
|
// ============================================================
|
|
@@ -34,6 +38,60 @@ const ERC8004_ABI = parseAbi([
|
|
|
34
38
|
]);
|
|
35
39
|
// PolicyEngine and AuditLogger imported from @aspect-warden/policy-engine
|
|
36
40
|
// ============================================================
|
|
41
|
+
// Encrypted Wallet Persistence
|
|
42
|
+
// ============================================================
|
|
43
|
+
const STORAGE_DIR = join(homedir(), '.warden');
|
|
44
|
+
const STATE_FILE = join(STORAGE_DIR, 'wallet-state.enc');
|
|
45
|
+
// Derive a 32-byte AES key from a passphrase
|
|
46
|
+
function deriveKey(passphrase) {
|
|
47
|
+
return createHash('sha256').update(passphrase).digest();
|
|
48
|
+
}
|
|
49
|
+
function encrypt(data, passphrase) {
|
|
50
|
+
const key = deriveKey(passphrase);
|
|
51
|
+
const iv = randomBytes(16);
|
|
52
|
+
const cipher = createCipheriv('aes-256-cbc', key, iv);
|
|
53
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
54
|
+
encrypted += cipher.final('hex');
|
|
55
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
56
|
+
}
|
|
57
|
+
function decrypt(payload, passphrase) {
|
|
58
|
+
const key = deriveKey(passphrase);
|
|
59
|
+
const [ivHex, encrypted] = payload.split(':');
|
|
60
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
61
|
+
const decipher = createDecipheriv('aes-256-cbc', key, iv);
|
|
62
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
63
|
+
decrypted += decipher.final('utf8');
|
|
64
|
+
return decrypted;
|
|
65
|
+
}
|
|
66
|
+
const STORAGE_KEY = process.env.WARDEN_STORAGE_KEY || `warden-${homedir()}`;
|
|
67
|
+
function saveState(state) {
|
|
68
|
+
try {
|
|
69
|
+
if (!existsSync(STORAGE_DIR)) {
|
|
70
|
+
mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 });
|
|
71
|
+
}
|
|
72
|
+
const json = JSON.stringify(state);
|
|
73
|
+
const encrypted = encrypt(json, STORAGE_KEY);
|
|
74
|
+
writeFileSync(STATE_FILE, encrypted, { mode: 0o600 });
|
|
75
|
+
console.error('[Warden MCP] Wallet state saved to ~/.warden/wallet-state.enc');
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('[Warden MCP] Failed to save wallet state:', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function loadState() {
|
|
82
|
+
try {
|
|
83
|
+
if (!existsSync(STATE_FILE))
|
|
84
|
+
return null;
|
|
85
|
+
const encrypted = readFileSync(STATE_FILE, 'utf8');
|
|
86
|
+
const json = decrypt(encrypted, STORAGE_KEY);
|
|
87
|
+
return JSON.parse(json);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error('[Warden MCP] Failed to load wallet state (may be from different key):', err);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ============================================================
|
|
37
95
|
// MCP Server State
|
|
38
96
|
// ============================================================
|
|
39
97
|
let policyEngine = null;
|
|
@@ -114,6 +172,22 @@ server.tool('warden_create_wallet', 'Create a new policy-enforced wallet for the
|
|
|
114
172
|
policyEngine = new PolicyEngine(policy);
|
|
115
173
|
auditLogger = new AuditLogger({ maxEntries: 10000 });
|
|
116
174
|
frozen = false;
|
|
175
|
+
// Persist wallet state for recovery across restarts
|
|
176
|
+
saveState({
|
|
177
|
+
privateKey: storedPrivateKey,
|
|
178
|
+
agentId,
|
|
179
|
+
policy: {
|
|
180
|
+
maxPerTx: policy.maxPerTx.toString(),
|
|
181
|
+
dailyLimit: policy.dailyLimit.toString(),
|
|
182
|
+
requireApprovalAbove: policy.requireApprovalAbove.toString(),
|
|
183
|
+
cooldownMs: policy.cooldownMs,
|
|
184
|
+
allowedTokens: policy.allowedTokens,
|
|
185
|
+
blockedTokens: policy.blockedTokens,
|
|
186
|
+
allowedRecipients: policy.allowedRecipients,
|
|
187
|
+
blockedRecipients: policy.blockedRecipients,
|
|
188
|
+
allowedChains: policy.allowedChains,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
117
191
|
return {
|
|
118
192
|
content: [{
|
|
119
193
|
type: 'text',
|
|
@@ -1034,6 +1108,37 @@ server.tool('warden_get_permissions', 'Query current permissions granted to an A
|
|
|
1034
1108
|
// Main -- stdio transport
|
|
1035
1109
|
// ============================================================
|
|
1036
1110
|
async function main() {
|
|
1111
|
+
// Restore wallet state from previous session if available
|
|
1112
|
+
const saved = loadState();
|
|
1113
|
+
if (saved) {
|
|
1114
|
+
try {
|
|
1115
|
+
initializeClients(saved.privateKey);
|
|
1116
|
+
const restoredPolicy = {
|
|
1117
|
+
agentId: saved.agentId,
|
|
1118
|
+
maxPerTx: BigInt(saved.policy.maxPerTx),
|
|
1119
|
+
dailyLimit: BigInt(saved.policy.dailyLimit),
|
|
1120
|
+
requireApprovalAbove: BigInt(saved.policy.requireApprovalAbove),
|
|
1121
|
+
cooldownMs: saved.policy.cooldownMs,
|
|
1122
|
+
allowedTokens: saved.policy.allowedTokens,
|
|
1123
|
+
blockedTokens: saved.policy.blockedTokens,
|
|
1124
|
+
allowedRecipients: saved.policy.allowedRecipients,
|
|
1125
|
+
blockedRecipients: saved.policy.blockedRecipients,
|
|
1126
|
+
allowedChains: saved.policy.allowedChains,
|
|
1127
|
+
anomalyDetection: {
|
|
1128
|
+
maxTxPerHour: 20,
|
|
1129
|
+
maxRecipientsPerHour: 5,
|
|
1130
|
+
largeTransactionPct: 50,
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
policyEngine = new PolicyEngine(restoredPolicy);
|
|
1134
|
+
auditLogger = new AuditLogger({ maxEntries: 10000 });
|
|
1135
|
+
frozen = false;
|
|
1136
|
+
console.error(`[Warden MCP] Restored wallet ${walletAddress} (agent: ${saved.agentId})`);
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
console.error('[Warden MCP] Failed to restore wallet state:', err);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1037
1142
|
const transport = new StdioServerTransport();
|
|
1038
1143
|
await server.connect(transport);
|
|
1039
1144
|
console.error('[Warden MCP] Server running on stdio');
|