@azeth/mcp-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/account.d.ts +4 -0
- package/dist/tools/account.d.ts.map +1 -0
- package/dist/tools/account.js +640 -0
- package/dist/tools/account.js.map +1 -0
- package/dist/tools/agreements.d.ts +4 -0
- package/dist/tools/agreements.d.ts.map +1 -0
- package/dist/tools/agreements.js +865 -0
- package/dist/tools/agreements.js.map +1 -0
- package/dist/tools/guardian-approval.d.ts +4 -0
- package/dist/tools/guardian-approval.d.ts.map +1 -0
- package/dist/tools/guardian-approval.js +319 -0
- package/dist/tools/guardian-approval.js.map +1 -0
- package/dist/tools/guardian.d.ts +4 -0
- package/dist/tools/guardian.d.ts.map +1 -0
- package/dist/tools/guardian.js +267 -0
- package/dist/tools/guardian.js.map +1 -0
- package/dist/tools/messaging.d.ts +4 -0
- package/dist/tools/messaging.d.ts.map +1 -0
- package/dist/tools/messaging.js +353 -0
- package/dist/tools/messaging.js.map +1 -0
- package/dist/tools/payments.d.ts +14 -0
- package/dist/tools/payments.d.ts.map +1 -0
- package/dist/tools/payments.js +723 -0
- package/dist/tools/payments.js.map +1 -0
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +608 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/reputation.d.ts +4 -0
- package/dist/tools/reputation.d.ts.map +1 -0
- package/dist/tools/reputation.js +433 -0
- package/dist/tools/reputation.js.map +1 -0
- package/dist/tools/transfer.d.ts +4 -0
- package/dist/tools/transfer.d.ts.map +1 -0
- package/dist/tools/transfer.js +181 -0
- package/dist/tools/transfer.js.map +1 -0
- package/dist/utils/client.d.ts +25 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +100 -0
- package/dist/utils/client.js.map +1 -0
- package/dist/utils/error-selectors.d.ts +23 -0
- package/dist/utils/error-selectors.d.ts.map +1 -0
- package/dist/utils/error-selectors.js +159 -0
- package/dist/utils/error-selectors.js.map +1 -0
- package/dist/utils/rate-limit.d.ts +17 -0
- package/dist/utils/rate-limit.d.ts.map +1 -0
- package/dist/utils/rate-limit.js +75 -0
- package/dist/utils/rate-limit.js.map +1 -0
- package/dist/utils/resolve.d.ts +38 -0
- package/dist/utils/resolve.d.ts.map +1 -0
- package/dist/utils/resolve.js +308 -0
- package/dist/utils/resolve.js.map +1 -0
- package/dist/utils/response.d.ts +42 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +257 -0
- package/dist/utils/response.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseEther, parseUnits } from 'viem';
|
|
3
|
+
import { AZETH_CONTRACTS, ERC8004_REGISTRY, TOKENS, formatTokenAmount } from '@azeth/common';
|
|
4
|
+
import { TrustRegistryModuleAbi } from '@azeth/common/abis';
|
|
5
|
+
import { createClient, resolveChain, validateAddress } from '../utils/client.js';
|
|
6
|
+
import { resolveSmartAccount } from '../utils/resolve.js';
|
|
7
|
+
import { success, error, handleError, guardianRequiredError } from '../utils/response.js';
|
|
8
|
+
/** Minimal ABI for ERC-8004 Identity Registry tokenURI (external contract, not in generated ABIs) */
|
|
9
|
+
const ERC8004_TOKEN_URI_ABI = [
|
|
10
|
+
{
|
|
11
|
+
type: 'function',
|
|
12
|
+
name: 'tokenURI',
|
|
13
|
+
inputs: [{ name: 'tokenId', type: 'uint256', internalType: 'uint256' }],
|
|
14
|
+
outputs: [{ name: '', type: 'string', internalType: 'string' }],
|
|
15
|
+
stateMutability: 'view',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
/** Parse an ERC-8004 data: URI to extract agent metadata (name + entityType).
|
|
19
|
+
* URI format: data:application/json,{encoded JSON with name/entityType fields}
|
|
20
|
+
*/
|
|
21
|
+
function parseAgentMetadata(uri) {
|
|
22
|
+
try {
|
|
23
|
+
if (!uri.startsWith('data:application/json,'))
|
|
24
|
+
return { name: '(unknown)', entityType: 'agent' };
|
|
25
|
+
const jsonStr = decodeURIComponent(uri.slice('data:application/json,'.length));
|
|
26
|
+
const metadata = JSON.parse(jsonStr);
|
|
27
|
+
return {
|
|
28
|
+
name: metadata.name ?? '(unknown)',
|
|
29
|
+
entityType: metadata.entityType ?? 'agent',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { name: '(unknown)', entityType: 'agent' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Register account-related MCP tools: azeth_create_account, azeth_balance, azeth_history, azeth_deposit, azeth_accounts */
|
|
37
|
+
export function registerAccountTools(server) {
|
|
38
|
+
// ──────────────────────────────────────────────
|
|
39
|
+
// azeth_create_account
|
|
40
|
+
// ──────────────────────────────────────────────
|
|
41
|
+
server.registerTool('azeth_create_account', {
|
|
42
|
+
description: [
|
|
43
|
+
'Deploy a new Azeth smart account with guardian guardrails and register it on the ERC-8004 trust registry.',
|
|
44
|
+
'',
|
|
45
|
+
'Use this when: An AI agent or service needs its own on-chain identity with spending limits and trust registry presence.',
|
|
46
|
+
'One EOA can own multiple smart accounts.',
|
|
47
|
+
'',
|
|
48
|
+
'Single atomic transaction: deploys smart account proxy, installs all 4 modules (Guardian, TrustRegistry,',
|
|
49
|
+
'PaymentAgreement, Reputation), registers on ERC-8004, and permanently revokes factory access.',
|
|
50
|
+
'',
|
|
51
|
+
'Returns: The deployed smart account address, trust registry token ID, and transaction hash.',
|
|
52
|
+
'',
|
|
53
|
+
'Guardian: By default, the guardian is derived from AZETH_GUARDIAN_KEY env var. If not set, falls back to self-guardian (owner address).',
|
|
54
|
+
'For production, always use a separate guardian key. Set AZETH_GUARDIAN_KEY in your .env file.',
|
|
55
|
+
'',
|
|
56
|
+
'Note: This is a state-changing operation that deploys contracts on-chain. It requires ETH for gas.',
|
|
57
|
+
'The owner private key is read from the AZETH_PRIVATE_KEY environment variable.',
|
|
58
|
+
'',
|
|
59
|
+
'Example: { "name": "PriceFeedBot", "entityType": "service", "description": "Real-time crypto price data", "capabilities": ["price-feed", "market-data"] }',
|
|
60
|
+
].join('\n'),
|
|
61
|
+
inputSchema: z.object({
|
|
62
|
+
chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
|
|
63
|
+
name: z.string().min(1).max(256).describe('Display name for this participant in the trust registry.'),
|
|
64
|
+
entityType: z.enum(['agent', 'service', 'infrastructure']).describe('Participant type: "agent" (AI agent), "service" (API/oracle), or "infrastructure" (bridge/relay).'),
|
|
65
|
+
description: z.string().min(1).max(2048).describe('Human-readable description of what this participant does.'),
|
|
66
|
+
capabilities: z.preprocess((val) => typeof val === 'string' ? JSON.parse(val) : val, z.array(z.string().max(128)).min(1).max(50)).describe('List of capabilities this participant offers (e.g., ["swap", "price-feed", "translation"]).'),
|
|
67
|
+
endpoint: z.string().url().max(2048)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Optional HTTP endpoint (http:// or https://) where this participant can be reached.'),
|
|
70
|
+
maxTxAmountUSD: z.coerce.number().positive().optional()
|
|
71
|
+
.describe('Max USD per transaction (default: $100 testnet, $50 mainnet).'),
|
|
72
|
+
dailySpendLimitUSD: z.coerce.number().positive().optional()
|
|
73
|
+
.describe('Max USD per day (default: $1000 testnet, $500 mainnet).'),
|
|
74
|
+
guardian: z.string().optional().describe('Guardian address for co-signing operations that exceed spending limits. ' +
|
|
75
|
+
'If omitted, derived from AZETH_GUARDIAN_KEY env var. ' +
|
|
76
|
+
'If neither is set, defaults to the owner address (self-guardian, NOT recommended for production).'),
|
|
77
|
+
emergencyWithdrawTo: z.string().optional().describe('Address where funds are sent during emergency withdrawal. ' +
|
|
78
|
+
'Defaults to the owner EOA address (derived from AZETH_PRIVATE_KEY). ' +
|
|
79
|
+
'Must be a trusted address you control — this is your recovery destination.'),
|
|
80
|
+
}),
|
|
81
|
+
}, async (args) => {
|
|
82
|
+
// Runtime validation for endpoint protocol (replaces .refine() which breaks MCP JSON Schema)
|
|
83
|
+
if (args.endpoint && !args.endpoint.startsWith('http://') && !args.endpoint.startsWith('https://')) {
|
|
84
|
+
return error('INVALID_INPUT', 'Endpoint must use HTTP or HTTPS protocol.');
|
|
85
|
+
}
|
|
86
|
+
let client;
|
|
87
|
+
try {
|
|
88
|
+
client = await createClient(args.chain);
|
|
89
|
+
// Default guardrails: conservative limits
|
|
90
|
+
const isTestnet = resolveChain(args.chain) === 'baseSepolia';
|
|
91
|
+
const maxTxUSD = args.maxTxAmountUSD ?? (isTestnet ? 100 : 50);
|
|
92
|
+
const dailyUSD = args.dailySpendLimitUSD ?? (isTestnet ? 1000 : 500);
|
|
93
|
+
// Resolve guardian address: explicit param > AZETH_GUARDIAN_KEY env > self-guardian (owner)
|
|
94
|
+
let guardianAddress = client.address; // fallback: self-guardian
|
|
95
|
+
let guardianSource = 'self (owner EOA)';
|
|
96
|
+
if (args.guardian) {
|
|
97
|
+
// Explicit guardian address provided
|
|
98
|
+
if (!validateAddress(args.guardian)) {
|
|
99
|
+
return error('INVALID_INPUT', `Invalid guardian address: "${args.guardian}".`, 'Must be 0x-prefixed followed by 40 hex characters.');
|
|
100
|
+
}
|
|
101
|
+
guardianAddress = args.guardian;
|
|
102
|
+
guardianSource = 'explicit parameter';
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Try deriving from AZETH_GUARDIAN_KEY env var
|
|
106
|
+
const guardianKey = process.env['AZETH_GUARDIAN_KEY'];
|
|
107
|
+
if (guardianKey && /^0x[0-9a-fA-F]{64}$/.test(guardianKey.trim())) {
|
|
108
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
109
|
+
const guardianAccount = privateKeyToAccount(guardianKey.trim());
|
|
110
|
+
guardianAddress = guardianAccount.address;
|
|
111
|
+
guardianSource = 'AZETH_GUARDIAN_KEY env var';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Resolve emergencyWithdrawTo: explicit param > AZETH_EMERGENCY_ADDRESS env > owner EOA
|
|
115
|
+
let emergencyAddress = client.address;
|
|
116
|
+
if (args.emergencyWithdrawTo) {
|
|
117
|
+
if (!validateAddress(args.emergencyWithdrawTo)) {
|
|
118
|
+
return error('INVALID_INPUT', `Invalid emergencyWithdrawTo address: "${args.emergencyWithdrawTo}".`, 'Must be 0x-prefixed followed by 40 hex characters.');
|
|
119
|
+
}
|
|
120
|
+
emergencyAddress = args.emergencyWithdrawTo;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const envEmergency = process.env['AZETH_EMERGENCY_ADDRESS'];
|
|
124
|
+
if (envEmergency && validateAddress(envEmergency)) {
|
|
125
|
+
emergencyAddress = envEmergency;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Default token whitelist: ETH + USDC + WETH so that payment agreements
|
|
129
|
+
// and other executor-module operations work out of the box.
|
|
130
|
+
const chain = resolveChain(args.chain);
|
|
131
|
+
const defaultTokens = [
|
|
132
|
+
'0x0000000000000000000000000000000000000000',
|
|
133
|
+
TOKENS[chain].USDC,
|
|
134
|
+
TOKENS[chain].WETH,
|
|
135
|
+
];
|
|
136
|
+
const result = await client.createAccount({
|
|
137
|
+
owner: client.address,
|
|
138
|
+
guardrails: {
|
|
139
|
+
maxTxAmountUSD: BigInt(Math.round(maxTxUSD)) * 10n ** 18n,
|
|
140
|
+
dailySpendLimitUSD: BigInt(Math.round(dailyUSD)) * 10n ** 18n,
|
|
141
|
+
guardianMaxTxAmountUSD: BigInt(Math.round(maxTxUSD * 5)) * 10n ** 18n,
|
|
142
|
+
guardianDailySpendLimitUSD: BigInt(Math.round(dailyUSD * 5)) * 10n ** 18n,
|
|
143
|
+
guardian: guardianAddress,
|
|
144
|
+
emergencyWithdrawTo: emergencyAddress,
|
|
145
|
+
},
|
|
146
|
+
tokens: defaultTokens,
|
|
147
|
+
registry: {
|
|
148
|
+
name: args.name,
|
|
149
|
+
description: args.description,
|
|
150
|
+
entityType: args.entityType,
|
|
151
|
+
capabilities: args.capabilities,
|
|
152
|
+
endpoint: args.endpoint,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return success({
|
|
156
|
+
account: result.account,
|
|
157
|
+
tokenId: result.tokenId.toString(),
|
|
158
|
+
txHash: result.txHash,
|
|
159
|
+
guardian: guardianAddress,
|
|
160
|
+
guardianSource,
|
|
161
|
+
emergencyWithdrawTo: emergencyAddress,
|
|
162
|
+
...(guardianAddress === client.address ? {
|
|
163
|
+
warning: 'Guardian is set to the owner address (self-guardian). This means guardian co-signatures use the same key as the owner, providing no additional security. For production, set AZETH_GUARDIAN_KEY in your environment or provide a separate guardian address.',
|
|
164
|
+
} : {}),
|
|
165
|
+
}, { txHash: result.txHash });
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return handleError(err);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
try {
|
|
172
|
+
await client?.destroy();
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// ──────────────────────────────────────────────
|
|
180
|
+
// azeth_balance
|
|
181
|
+
// ──────────────────────────────────────────────
|
|
182
|
+
server.registerTool('azeth_balance', {
|
|
183
|
+
description: [
|
|
184
|
+
'Check all balances with USD values for your EOA and all Azeth smart accounts.',
|
|
185
|
+
'',
|
|
186
|
+
'Use this when: You need to know how much ETH, USDC, or WETH your accounts hold,',
|
|
187
|
+
'or you want a total portfolio value in USD before making a transfer or payment.',
|
|
188
|
+
'',
|
|
189
|
+
'Returns: Multi-account breakdown with per-token USD values and grand total.',
|
|
190
|
+
'EOA is shown first (index 0), followed by smart accounts in deployment order.',
|
|
191
|
+
'',
|
|
192
|
+
'Optionally filter to a single smart account by providing its address.',
|
|
193
|
+
'',
|
|
194
|
+
'Note: This is a read-only, single-RPC-call operation and safe to call repeatedly.',
|
|
195
|
+
'The owner is determined by the AZETH_PRIVATE_KEY environment variable.',
|
|
196
|
+
'',
|
|
197
|
+
'Example: {} or { "smartAccount": "#1" }',
|
|
198
|
+
].join('\n'),
|
|
199
|
+
inputSchema: z.object({
|
|
200
|
+
chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
|
|
201
|
+
smartAccount: z.string().optional().describe('Smart account address, name, or "#N" (account index). If omitted, shows all accounts.'),
|
|
202
|
+
}),
|
|
203
|
+
}, async (args) => {
|
|
204
|
+
let client;
|
|
205
|
+
try {
|
|
206
|
+
client = await createClient(args.chain);
|
|
207
|
+
// Resolve smartAccount: address, name, "#N", or undefined
|
|
208
|
+
let targetAddress;
|
|
209
|
+
if (args.smartAccount) {
|
|
210
|
+
try {
|
|
211
|
+
targetAddress = await resolveSmartAccount(args.smartAccount, client);
|
|
212
|
+
}
|
|
213
|
+
catch (resolveErr) {
|
|
214
|
+
return handleError(resolveErr);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const allBalances = await client.getAllBalances();
|
|
218
|
+
let accounts = allBalances.accounts;
|
|
219
|
+
if (targetAddress) {
|
|
220
|
+
const target = targetAddress.toLowerCase();
|
|
221
|
+
accounts = accounts.filter((ab) => ab.account.toLowerCase() === target);
|
|
222
|
+
}
|
|
223
|
+
// Enrich smart account labels with names from the trust registry.
|
|
224
|
+
// Skip index 0 (EOA). Non-fatal: falls back to default label on any failure.
|
|
225
|
+
const chain = resolveChain(args.chain);
|
|
226
|
+
const trustRegistryAddr = AZETH_CONTRACTS[chain].trustRegistryModule;
|
|
227
|
+
const identityRegistryAddr = ERC8004_REGISTRY[chain];
|
|
228
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
229
|
+
const ab = accounts[i];
|
|
230
|
+
// EOA label is always "EOA Wallet" — skip
|
|
231
|
+
if (i === 0 && !targetAddress)
|
|
232
|
+
continue;
|
|
233
|
+
try {
|
|
234
|
+
const tokenId = await client.publicClient.readContract({
|
|
235
|
+
address: trustRegistryAddr,
|
|
236
|
+
abi: TrustRegistryModuleAbi,
|
|
237
|
+
functionName: 'getTokenId',
|
|
238
|
+
args: [ab.account],
|
|
239
|
+
});
|
|
240
|
+
if (tokenId > 0n) {
|
|
241
|
+
const uri = await client.publicClient.readContract({
|
|
242
|
+
address: identityRegistryAddr,
|
|
243
|
+
abi: ERC8004_TOKEN_URI_ABI,
|
|
244
|
+
functionName: 'tokenURI',
|
|
245
|
+
args: [tokenId],
|
|
246
|
+
});
|
|
247
|
+
const name = parseAgentMetadata(uri).name;
|
|
248
|
+
if (name !== '(unknown)') {
|
|
249
|
+
// Extract the #N index from the existing label if present
|
|
250
|
+
const indexMatch = ab.label.match(/#(\d+)/);
|
|
251
|
+
ab.label = indexMatch ? `${name} (#${indexMatch[1]})` : name;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Non-fatal — keep existing label
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return success({
|
|
260
|
+
owner: client.address,
|
|
261
|
+
grandTotalUSD: allBalances.grandTotalUSDFormatted,
|
|
262
|
+
accounts: accounts.map((ab) => ({
|
|
263
|
+
account: ab.account,
|
|
264
|
+
label: ab.label,
|
|
265
|
+
totalUSD: ab.totalUSDFormatted,
|
|
266
|
+
balances: ab.balances.map((tb) => ({
|
|
267
|
+
token: tb.symbol,
|
|
268
|
+
balance: tb.balanceFormatted,
|
|
269
|
+
usdValue: tb.usdFormatted,
|
|
270
|
+
})),
|
|
271
|
+
})),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
return handleError(err);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
try {
|
|
279
|
+
await client?.destroy();
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// ──────────────────────────────────────────────
|
|
287
|
+
// azeth_history
|
|
288
|
+
// ──────────────────────────────────────────────
|
|
289
|
+
server.registerTool('azeth_history', {
|
|
290
|
+
description: [
|
|
291
|
+
'Get recent transaction history for your Azeth smart account.',
|
|
292
|
+
'',
|
|
293
|
+
'Use this when: You need to review past transactions, verify a payment was sent, or audit account activity.',
|
|
294
|
+
'',
|
|
295
|
+
'Returns: Array of transaction records with hash, from, to, value, block number, and timestamp.',
|
|
296
|
+
'',
|
|
297
|
+
'Note: Full indexed history requires the Azeth server to be running. Returns empty results if the server is unavailable.',
|
|
298
|
+
'The account is determined by the AZETH_PRIVATE_KEY environment variable.',
|
|
299
|
+
'',
|
|
300
|
+
'Example: { "limit": 5 } or { "smartAccount": "#2", "limit": 20 }',
|
|
301
|
+
].join('\n'),
|
|
302
|
+
inputSchema: z.object({
|
|
303
|
+
chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
|
|
304
|
+
limit: z.coerce.number().int().min(1).max(100).optional().describe('Maximum number of transactions to return. Defaults to 10.'),
|
|
305
|
+
smartAccount: z.string().optional().describe('Smart account address, name, or "#N" (account index). If omitted, uses your first smart account.'),
|
|
306
|
+
}),
|
|
307
|
+
}, async (args) => {
|
|
308
|
+
let client;
|
|
309
|
+
try {
|
|
310
|
+
client = await createClient(args.chain);
|
|
311
|
+
// Resolve smartAccount: address, name, "#N", or undefined
|
|
312
|
+
let forAccount;
|
|
313
|
+
if (args.smartAccount) {
|
|
314
|
+
try {
|
|
315
|
+
forAccount = await resolveSmartAccount(args.smartAccount, client);
|
|
316
|
+
}
|
|
317
|
+
catch (resolveErr) {
|
|
318
|
+
return handleError(resolveErr);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const history = await client.getHistory({ limit: args.limit ?? 10 }, forAccount);
|
|
322
|
+
// Resolve token symbols and decimals for formatting
|
|
323
|
+
const chain = resolveChain(args.chain);
|
|
324
|
+
const tokens = TOKENS[chain];
|
|
325
|
+
const ZERO_ADDR = '0x0000000000000000000000000000000000000000';
|
|
326
|
+
return success({
|
|
327
|
+
smartAccount: client.smartAccount ?? 'not-deployed',
|
|
328
|
+
transactions: history.map((tx) => {
|
|
329
|
+
// Determine token symbol and decimals for formatting
|
|
330
|
+
const tokenAddr = tx.token?.toLowerCase() ?? null;
|
|
331
|
+
let symbol = 'ETH';
|
|
332
|
+
let decimals = 18;
|
|
333
|
+
if (tokenAddr && tokenAddr !== ZERO_ADDR) {
|
|
334
|
+
if (tokenAddr === tokens.USDC.toLowerCase()) {
|
|
335
|
+
symbol = 'USDC';
|
|
336
|
+
decimals = 6;
|
|
337
|
+
}
|
|
338
|
+
else if (tokenAddr === tokens.WETH.toLowerCase()) {
|
|
339
|
+
symbol = 'WETH';
|
|
340
|
+
decimals = 18;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
symbol = 'TOKEN';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const valueFormatted = `${formatTokenAmount(tx.value, decimals, decimals === 6 ? 2 : 6)} ${symbol}`;
|
|
347
|
+
const timestampISO = tx.timestamp > 0
|
|
348
|
+
? new Date(tx.timestamp * 1000).toISOString()
|
|
349
|
+
: null;
|
|
350
|
+
return {
|
|
351
|
+
hash: tx.hash,
|
|
352
|
+
from: tx.from,
|
|
353
|
+
to: tx.to,
|
|
354
|
+
value: tx.value.toString(),
|
|
355
|
+
valueFormatted,
|
|
356
|
+
token: tx.token ?? ZERO_ADDR,
|
|
357
|
+
tokenSymbol: symbol,
|
|
358
|
+
blockNumber: tx.blockNumber.toString(),
|
|
359
|
+
timestamp: tx.timestamp,
|
|
360
|
+
timestampISO,
|
|
361
|
+
};
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
return handleError(err);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
try {
|
|
370
|
+
await client?.destroy();
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// ──────────────────────────────────────────────
|
|
378
|
+
// azeth_deposit
|
|
379
|
+
// ──────────────────────────────────────────────
|
|
380
|
+
server.registerTool('azeth_deposit', {
|
|
381
|
+
description: [
|
|
382
|
+
'Deposit ETH or ERC-20 tokens from your EOA wallet into your own Azeth smart account.',
|
|
383
|
+
'',
|
|
384
|
+
'Use this when: Your smart account needs funding for transfers, x402 payments, or other operations.',
|
|
385
|
+
'',
|
|
386
|
+
'SECURITY: This verifies ON-CHAIN that the target is a real Azeth smart account owned by you.',
|
|
387
|
+
'You cannot deposit to someone else\'s smart account.',
|
|
388
|
+
'',
|
|
389
|
+
'Returns: Transaction hash and deposit details.',
|
|
390
|
+
'',
|
|
391
|
+
'Note: If no target account is specified, deposits to your first smart account.',
|
|
392
|
+
'For ETH deposits, omit the token parameter. For ERC-20 tokens, provide the token contract address AND decimals.',
|
|
393
|
+
'The amount is in human-readable units (e.g., "0.01" for 0.01 ETH, "100" for 100 USDC).',
|
|
394
|
+
'',
|
|
395
|
+
'Example: { "amount": "0.01" } or { "amount": "50", "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "decimals": 6 }',
|
|
396
|
+
].join('\n'),
|
|
397
|
+
inputSchema: z.object({
|
|
398
|
+
chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
|
|
399
|
+
amount: z.string().describe('Amount to deposit in human-readable units (e.g., "0.01" for 0.01 ETH).'),
|
|
400
|
+
token: z.string().optional().describe('ERC-20 token contract address. Omit for native ETH deposit.'),
|
|
401
|
+
decimals: z.coerce.number().int().min(0).max(18).optional()
|
|
402
|
+
.describe('Token decimals for ERC-20 deposits. REQUIRED when token is specified. Use 6 for USDC, 18 for WETH.'),
|
|
403
|
+
smartAccount: z.string().optional()
|
|
404
|
+
.describe('Target smart account address, name, or "#N" (account index). If omitted, deposits to your first Azeth account.'),
|
|
405
|
+
}),
|
|
406
|
+
}, async (args) => {
|
|
407
|
+
if (args.token && !validateAddress(args.token)) {
|
|
408
|
+
return error('INVALID_INPUT', `Invalid token address: "${args.token}".`, 'Must be 0x-prefixed followed by 40 hex characters.');
|
|
409
|
+
}
|
|
410
|
+
if (args.token && args.decimals === undefined) {
|
|
411
|
+
return error('INVALID_INPUT', 'decimals is required when token address is provided.', 'Use 6 for USDC, 18 for WETH.');
|
|
412
|
+
}
|
|
413
|
+
let client;
|
|
414
|
+
try {
|
|
415
|
+
client = await createClient(args.chain);
|
|
416
|
+
let target;
|
|
417
|
+
if (args.smartAccount) {
|
|
418
|
+
try {
|
|
419
|
+
target = (await resolveSmartAccount(args.smartAccount, client));
|
|
420
|
+
}
|
|
421
|
+
catch (resolveErr) {
|
|
422
|
+
return handleError(resolveErr);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
target = await client.resolveSmartAccount();
|
|
427
|
+
}
|
|
428
|
+
const decimals = args.decimals ?? 18;
|
|
429
|
+
let amount;
|
|
430
|
+
try {
|
|
431
|
+
amount = args.token ? parseUnits(args.amount, decimals) : parseEther(args.amount);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return error('INVALID_INPUT', 'Invalid amount format — must be a valid decimal number.');
|
|
435
|
+
}
|
|
436
|
+
const result = await client.deposit({
|
|
437
|
+
to: target,
|
|
438
|
+
amount,
|
|
439
|
+
token: args.token,
|
|
440
|
+
});
|
|
441
|
+
return success({
|
|
442
|
+
txHash: result.txHash,
|
|
443
|
+
from: result.from,
|
|
444
|
+
to: result.to,
|
|
445
|
+
amount: args.amount,
|
|
446
|
+
token: result.token,
|
|
447
|
+
}, { txHash: result.txHash });
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
return handleError(err);
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
try {
|
|
454
|
+
await client?.destroy();
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
// ──────────────────────────────────────────────
|
|
462
|
+
// azeth_accounts
|
|
463
|
+
// ──────────────────────────────────────────────
|
|
464
|
+
server.registerTool('azeth_accounts', {
|
|
465
|
+
description: [
|
|
466
|
+
'List all your Azeth smart accounts with their names, addresses, and trust registry token IDs.',
|
|
467
|
+
'',
|
|
468
|
+
'Use this when: You want to see all your accounts at a glance, find an account by name,',
|
|
469
|
+
'or get the "#N" index for use in other tools.',
|
|
470
|
+
'',
|
|
471
|
+
'Returns: Your EOA owner address and an indexed list of smart accounts.',
|
|
472
|
+
'Each account shows its #N index (usable in other tools), name, address, and tokenId.',
|
|
473
|
+
'',
|
|
474
|
+
'Note: This is a read-only operation. Names come from the trust registry.',
|
|
475
|
+
'The owner is determined by the AZETH_PRIVATE_KEY environment variable.',
|
|
476
|
+
].join('\n'),
|
|
477
|
+
inputSchema: z.object({
|
|
478
|
+
chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
|
|
479
|
+
}),
|
|
480
|
+
}, async (args) => {
|
|
481
|
+
let client;
|
|
482
|
+
try {
|
|
483
|
+
client = await createClient(args.chain);
|
|
484
|
+
const accounts = await client.getSmartAccounts();
|
|
485
|
+
if (accounts.length === 0) {
|
|
486
|
+
return success({
|
|
487
|
+
owner: client.address,
|
|
488
|
+
accounts: [],
|
|
489
|
+
message: 'No smart accounts found. Use azeth_create_account to create one.',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// Look up name and tokenId for each account on-chain via TrustRegistryModule + ERC-8004
|
|
493
|
+
const chain = resolveChain(args.chain);
|
|
494
|
+
const trustRegistryAddr = AZETH_CONTRACTS[chain].trustRegistryModule;
|
|
495
|
+
const identityRegistryAddr = ERC8004_REGISTRY[chain];
|
|
496
|
+
const accountDetails = [];
|
|
497
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
498
|
+
const addr = accounts[i];
|
|
499
|
+
let name = '(unregistered)';
|
|
500
|
+
let entityType = 'agent';
|
|
501
|
+
let tokenIdStr = '(none)';
|
|
502
|
+
try {
|
|
503
|
+
// Step 1: Get tokenId from TrustRegistryModule (on-chain)
|
|
504
|
+
const tokenId = await client.publicClient.readContract({
|
|
505
|
+
address: trustRegistryAddr,
|
|
506
|
+
abi: TrustRegistryModuleAbi,
|
|
507
|
+
functionName: 'getTokenId',
|
|
508
|
+
args: [addr],
|
|
509
|
+
});
|
|
510
|
+
if (tokenId > 0n) {
|
|
511
|
+
tokenIdStr = tokenId.toString();
|
|
512
|
+
// Step 2: Get tokenURI from ERC-8004 Identity Registry (on-chain)
|
|
513
|
+
try {
|
|
514
|
+
const uri = await client.publicClient.readContract({
|
|
515
|
+
address: identityRegistryAddr,
|
|
516
|
+
abi: ERC8004_TOKEN_URI_ABI,
|
|
517
|
+
functionName: 'tokenURI',
|
|
518
|
+
args: [tokenId],
|
|
519
|
+
});
|
|
520
|
+
const meta = parseAgentMetadata(uri);
|
|
521
|
+
name = meta.name;
|
|
522
|
+
entityType = meta.entityType;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// tokenURI call failed — token exists but URI unreadable
|
|
526
|
+
name = '(registered)';
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// getTokenId call failed — account not registered on this module
|
|
532
|
+
}
|
|
533
|
+
accountDetails.push({
|
|
534
|
+
index: i + 1,
|
|
535
|
+
address: addr,
|
|
536
|
+
name,
|
|
537
|
+
entityType,
|
|
538
|
+
tokenId: tokenIdStr,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return success({
|
|
542
|
+
owner: client.address,
|
|
543
|
+
accounts: accountDetails,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
return handleError(err);
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
try {
|
|
551
|
+
await client?.destroy();
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
// ──────────────────────────────────────────────
|
|
559
|
+
// azeth_whitelist_token
|
|
560
|
+
// ──────────────────────────────────────────────
|
|
561
|
+
server.registerTool('azeth_whitelist_token', {
|
|
562
|
+
description: [
|
|
563
|
+
'Add or remove a token from your smart account\'s guardian whitelist.',
|
|
564
|
+
'',
|
|
565
|
+
'Use this when: You need to whitelist a new token for payment agreements or other executor-module operations.',
|
|
566
|
+
'Newly created accounts already have ETH, USDC, and WETH whitelisted by default.',
|
|
567
|
+
'',
|
|
568
|
+
'Why it matters: The GuardianModule enforces a token whitelist for automated operations',
|
|
569
|
+
'(payment agreements, swap execution). Owner-signed transfers bypass the whitelist,',
|
|
570
|
+
'but executor modules like PaymentAgreementModule require the token to be whitelisted.',
|
|
571
|
+
'',
|
|
572
|
+
'Returns: Transaction hash confirming the whitelist update.',
|
|
573
|
+
'',
|
|
574
|
+
'Note: Only the account owner can update their own whitelist.',
|
|
575
|
+
'',
|
|
576
|
+
'Example: { "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "allowed": true }',
|
|
577
|
+
].join('\n'),
|
|
578
|
+
inputSchema: z.object({
|
|
579
|
+
chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
|
|
580
|
+
token: z.string().describe('Token contract address to whitelist/delist. Use "0x0000000000000000000000000000000000000000" for native ETH.'),
|
|
581
|
+
allowed: z.boolean().describe('true to whitelist the token, false to remove it from the whitelist.'),
|
|
582
|
+
smartAccount: z.string().optional().describe('Smart account address, name, or "#N". Defaults to first smart account.'),
|
|
583
|
+
}),
|
|
584
|
+
}, async (args) => {
|
|
585
|
+
if (!validateAddress(args.token)) {
|
|
586
|
+
return error('INVALID_INPUT', `Invalid token address: "${args.token}".`, 'Must be 0x-prefixed followed by 40 hex characters.');
|
|
587
|
+
}
|
|
588
|
+
let client;
|
|
589
|
+
try {
|
|
590
|
+
client = await createClient(args.chain);
|
|
591
|
+
let account;
|
|
592
|
+
if (args.smartAccount) {
|
|
593
|
+
try {
|
|
594
|
+
account = (await resolveSmartAccount(args.smartAccount, client));
|
|
595
|
+
}
|
|
596
|
+
catch (resolveErr) {
|
|
597
|
+
return handleError(resolveErr);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
account = await client.resolveSmartAccount();
|
|
602
|
+
}
|
|
603
|
+
const txHash = await client.setTokenWhitelist(args.token, args.allowed, account);
|
|
604
|
+
const action = args.allowed ? 'whitelisted' : 'removed from whitelist';
|
|
605
|
+
// Resolve token symbol for display
|
|
606
|
+
const chain = resolveChain(args.chain);
|
|
607
|
+
const tokens = TOKENS[chain];
|
|
608
|
+
const tokenLower = args.token.toLowerCase();
|
|
609
|
+
let tokenSymbol = args.token.slice(0, 6) + '...' + args.token.slice(-4);
|
|
610
|
+
if (args.token === '0x0000000000000000000000000000000000000000')
|
|
611
|
+
tokenSymbol = 'ETH';
|
|
612
|
+
else if (tokenLower === tokens.USDC.toLowerCase())
|
|
613
|
+
tokenSymbol = 'USDC';
|
|
614
|
+
else if (tokenLower === tokens.WETH.toLowerCase())
|
|
615
|
+
tokenSymbol = 'WETH';
|
|
616
|
+
return success({
|
|
617
|
+
token: args.token,
|
|
618
|
+
tokenSymbol,
|
|
619
|
+
allowed: args.allowed,
|
|
620
|
+
message: `${tokenSymbol} (${args.token}) ${action} on account ${account}.`,
|
|
621
|
+
}, { txHash });
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
// Detect AA24 signature validation failure — common for guardian-gated operations
|
|
625
|
+
if (err instanceof Error && /AA24/.test(err.message)) {
|
|
626
|
+
return guardianRequiredError('Token whitelisting requires guardian co-signature (guardrail-loosening change).', { operation: 'whitelist_token' });
|
|
627
|
+
}
|
|
628
|
+
return handleError(err);
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
try {
|
|
632
|
+
await client?.destroy();
|
|
633
|
+
}
|
|
634
|
+
catch (e) {
|
|
635
|
+
process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
//# sourceMappingURL=account.js.map
|