agentwallet-mcp 1.0.0 → 1.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.
Files changed (2) hide show
  1. package/build/index.js +298 -2
  2. package/package.json +7 -1
package/build/index.js CHANGED
@@ -41,16 +41,54 @@ async function api(path, method = 'GET', body) {
41
41
  function jsonResponse(data) {
42
42
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
43
43
  }
44
+ // ─── EVM Helpers ──────────────────────────────────────────────────
45
+ /**
46
+ * Convert a human-readable amount (e.g. "0.1") to raw units string given decimals.
47
+ * Uses BigInt for precision — no floating-point errors.
48
+ */
49
+ function parseUnits(amount, decimals) {
50
+ if (!/^\d+(\.\d+)?$/.test(amount)) {
51
+ throw new Error(`Invalid amount "${amount}". Must be a positive number (e.g. "0.1" or "100").`);
52
+ }
53
+ const [whole, frac = ''] = amount.split('.');
54
+ const fracPadded = frac.slice(0, decimals).padEnd(decimals, '0');
55
+ const raw = BigInt(whole + fracPadded);
56
+ return raw.toString();
57
+ }
58
+ /**
59
+ * Format raw units to human-readable string given decimals.
60
+ */
61
+ function formatUnits(raw, decimals) {
62
+ const padded = raw.padStart(decimals + 1, '0');
63
+ const whole = padded.slice(0, padded.length - decimals) || '0';
64
+ const frac = padded.slice(padded.length - decimals);
65
+ // Trim trailing zeros but keep at least one decimal
66
+ const trimmed = frac.replace(/0+$/, '') || '0';
67
+ return `${whole}.${trimmed}`;
68
+ }
69
+ /**
70
+ * Pad an address to 32 bytes (64 hex chars) for ABI encoding.
71
+ */
72
+ function padAddress(address) {
73
+ return address.slice(2).toLowerCase().padStart(64, '0');
74
+ }
75
+ /**
76
+ * Encode a uint256 as 32 bytes hex (64 chars).
77
+ */
78
+ function encodeUint256(value) {
79
+ const hex = BigInt(value).toString(16);
80
+ return hex.padStart(64, '0');
81
+ }
44
82
  // ─── Server ──────────────────────────────────────────────────────
45
83
  const server = new McpServer({
46
84
  name: 'agentwallet',
47
- version: '1.0.0',
85
+ version: '1.2.0',
48
86
  });
49
87
  // ─── Tool: create_wallet ─────────────────────────────────────────
50
88
  server.tool('create_wallet', 'Create a new EVM wallet. Returns the wallet ID and address. ' +
51
89
  'Private key is encrypted server-side and never exposed.', {
52
90
  label: z.string().default('').describe('Friendly name for the wallet'),
53
- chain_id: z.number().int().default(1).describe('Default chain ID (1=Ethereum, 8453=Base, 137=Polygon, 56=BSC, 369=PulseChain)'),
91
+ chain_id: z.number().int().default(8453).describe('Default chain ID (1=Ethereum, 8453=Base, 42161=Arbitrum, 10=Optimism, 137=Polygon, 43114=Avalanche, 56=BSC, 7777777=Zora, 369=PulseChain)'),
54
92
  }, async ({ label, chain_id }) => {
55
93
  const data = await api('/wallets', 'POST', { label, chain_id });
56
94
  return jsonResponse(data);
@@ -129,6 +167,264 @@ server.tool('send_transaction', 'Sign and broadcast an EVM transaction. ' +
129
167
  const result = await api(`/wallets/${wallet_id}/send`, 'POST', body);
130
168
  return jsonResponse(result);
131
169
  });
170
+ // ─── Tool: transfer ─────────────────────────────────────────────
171
+ server.tool('transfer', 'Send native tokens (ETH, AVAX, BNB, POL, PLS) to an address. ' +
172
+ 'Specify the amount in human-readable format (e.g. "0.1" for 0.1 ETH). ' +
173
+ 'The amount is converted to wei automatically. Signs and broadcasts the transaction.', {
174
+ wallet_id: z.number().int().describe('Wallet ID to send from'),
175
+ to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Destination address'),
176
+ amount: z.string().describe('Amount to send in human-readable format (e.g. "0.1" for 0.1 ETH)'),
177
+ chain_id: z.number().int().describe('Chain ID (1=Ethereum, 8453=Base, 42161=Arbitrum, 10=Optimism, 137=Polygon, 43114=Avalanche, 56=BSC, 7777777=Zora, 369=PulseChain)'),
178
+ }, async ({ wallet_id, to, amount, chain_id }) => {
179
+ // Convert human-readable to wei (18 decimals for all native tokens)
180
+ const valueWei = parseUnits(amount, 18);
181
+ const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
182
+ to,
183
+ value: valueWei,
184
+ chain_id,
185
+ data: '',
186
+ });
187
+ return jsonResponse({
188
+ ...result,
189
+ amount,
190
+ chain_id,
191
+ });
192
+ });
193
+ // ─── Tool: get_token_balance ────────────────────────────────────
194
+ server.tool('get_token_balance', 'Get the ERC-20 token balance for a wallet on a specific chain. ' +
195
+ 'Returns the raw balance and human-readable balance. ' +
196
+ 'Use get_chains to find stablecoin addresses for each chain.', {
197
+ wallet_id: z.number().int().describe('Wallet ID to check'),
198
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
199
+ chain_id: z.number().int().describe('Chain ID to check on'),
200
+ decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
201
+ }, async ({ wallet_id, token, chain_id, decimals }) => {
202
+ const params = `?chain_id=${chain_id}&token=${token}`;
203
+ const data = await api(`/wallets/${wallet_id}/token-balance${params}`);
204
+ const balanceFormatted = formatUnits(data.balance_raw, decimals);
205
+ return jsonResponse({
206
+ ...data,
207
+ balance: balanceFormatted,
208
+ decimals,
209
+ });
210
+ });
211
+ // ─── Tool: transfer_token ───────────────────────────────────────
212
+ server.tool('transfer_token', 'Send ERC-20 tokens (USDC, USDT, etc.) to an address. ' +
213
+ 'Specify the amount in human-readable format (e.g. "100" for 100 USDC). ' +
214
+ 'Signs and broadcasts the transaction. Use get_chains to find stablecoin addresses.', {
215
+ wallet_id: z.number().int().describe('Wallet ID to send from'),
216
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
217
+ to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Recipient address'),
218
+ amount: z.string().describe('Amount in human-readable format (e.g. "100" for 100 USDC)'),
219
+ chain_id: z.number().int().describe('Chain ID'),
220
+ decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
221
+ }, async ({ wallet_id, token, to, amount, chain_id, decimals }) => {
222
+ // Encode ERC-20 transfer(address, uint256) calldata
223
+ const rawAmount = parseUnits(amount, decimals);
224
+ const calldata = '0xa9059cbb' + padAddress(to) + encodeUint256(rawAmount);
225
+ const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
226
+ to: token, // Send TX to the token contract
227
+ value: '0', // No native value for token transfers
228
+ data: calldata,
229
+ chain_id,
230
+ });
231
+ return jsonResponse({
232
+ ...result,
233
+ token,
234
+ recipient: to,
235
+ amount,
236
+ decimals,
237
+ });
238
+ });
239
+ // ─── Tool: call_contract ────────────────────────────────────────
240
+ server.tool('call_contract', 'Execute a read-only call against a smart contract (eth_call). ' +
241
+ 'Returns the raw hex result. Does not cost gas or modify state. ' +
242
+ 'Useful for reading on-chain data like token balances, prices, positions.', {
243
+ chain_id: z.number().int().describe('Chain ID'),
244
+ to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address'),
245
+ data: z.string().describe('ABI-encoded calldata (0x-prefixed hex)'),
246
+ }, async ({ chain_id, to, data }) => {
247
+ const result = await api('/eth-call', 'POST', { chain_id, to, data });
248
+ return jsonResponse(result);
249
+ });
250
+ // ─── Tool: approve_token ────────────────────────────────────────
251
+ server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 tokens on your behalf. ' +
252
+ 'Required before interacting with any DeFi protocol (DEXs, lending, etc.). ' +
253
+ 'Use amount "max" for unlimited approval, or specify an exact amount.', {
254
+ wallet_id: z.number().int().describe('Wallet ID'),
255
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
256
+ spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address to approve as spender'),
257
+ amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"), or "max" for unlimited'),
258
+ chain_id: z.number().int().describe('Chain ID'),
259
+ decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
260
+ }, async ({ wallet_id, token, spender, amount, chain_id, decimals }) => {
261
+ // approve(address spender, uint256 amount) — selector: 0x095ea7b3
262
+ let rawAmount;
263
+ if (amount.toLowerCase() === 'max') {
264
+ rawAmount = (BigInt(2) ** BigInt(256) - BigInt(1)).toString();
265
+ }
266
+ else {
267
+ rawAmount = parseUnits(amount, decimals);
268
+ }
269
+ const calldata = '0x095ea7b3' + padAddress(spender) + encodeUint256(rawAmount);
270
+ const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
271
+ to: token,
272
+ value: '0',
273
+ data: calldata,
274
+ chain_id,
275
+ });
276
+ return jsonResponse({
277
+ ...result,
278
+ token,
279
+ spender,
280
+ amount: amount.toLowerCase() === 'max' ? 'unlimited' : amount,
281
+ });
282
+ });
283
+ // ─── Tool: get_allowance ────────────────────────────────────────
284
+ server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved to transfer. ' +
285
+ 'Returns the allowance in both raw and human-readable format. ' +
286
+ 'Use this to check if an approval is needed before a DeFi transaction.', {
287
+ wallet_id: z.number().int().describe('Wallet ID (used to determine the owner address)'),
288
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
289
+ spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Spender contract address to check'),
290
+ chain_id: z.number().int().describe('Chain ID'),
291
+ decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
292
+ }, async ({ wallet_id, token, spender, chain_id, decimals }) => {
293
+ // First get the wallet address
294
+ const wallet = await api(`/wallets/${wallet_id}`);
295
+ // allowance(address owner, address spender) — selector: 0xdd62ed3e
296
+ const calldata = '0xdd62ed3e' + padAddress(wallet.address) + padAddress(spender);
297
+ const result = await api('/eth-call', 'POST', { chain_id, to: token, data: calldata });
298
+ // Parse uint256 result
299
+ const rawHex = result.result.replace('0x', '');
300
+ const raw = BigInt('0x' + (rawHex || '0')).toString();
301
+ const maxUint256 = (BigInt(2) ** BigInt(256) - BigInt(1)).toString();
302
+ return jsonResponse({
303
+ token,
304
+ spender,
305
+ allowance_raw: raw,
306
+ allowance: raw === maxUint256 ? 'unlimited' : formatUnits(raw, decimals),
307
+ is_unlimited: raw === maxUint256,
308
+ decimals,
309
+ });
310
+ });
311
+ // ─── WETH / Wrapped Native Token addresses ──────────────────────
312
+ const WRAPPED_NATIVE = {
313
+ 1: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH' },
314
+ 8453: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
315
+ 42161: { address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', symbol: 'WETH' },
316
+ 10: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
317
+ 137: { address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', symbol: 'WPOL' },
318
+ 56: { address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', symbol: 'WBNB' },
319
+ 43114: { address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', symbol: 'WAVAX' },
320
+ 7777777: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
321
+ 369: { address: '0xA1077a294dDE1B09bB078844df40758a5D0f9a27', symbol: 'WPLS' },
322
+ };
323
+ // ─── Tool: wrap_eth ─────────────────────────────────────────────
324
+ server.tool('wrap_eth', 'Wrap native tokens (ETH, AVAX, BNB, POL, PLS) into their wrapped ERC-20 version (WETH, WAVAX, etc.). ' +
325
+ 'Required for most DeFi protocols that use ERC-20 tokens instead of raw native tokens. ' +
326
+ 'Specify amount in human-readable format (e.g. "0.5" for 0.5 ETH).', {
327
+ wallet_id: z.number().int().describe('Wallet ID'),
328
+ amount: z.string().describe('Amount to wrap in human-readable format (e.g. "0.5")'),
329
+ chain_id: z.number().int().describe('Chain ID'),
330
+ }, async ({ wallet_id, amount, chain_id }) => {
331
+ const wrapped = WRAPPED_NATIVE[chain_id];
332
+ if (!wrapped) {
333
+ throw new Error(`No wrapped native token configured for chain ${chain_id}`);
334
+ }
335
+ // WETH deposit() payable — selector: 0xd0e30db0
336
+ const valueWei = parseUnits(amount, 18);
337
+ const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
338
+ to: wrapped.address,
339
+ value: valueWei,
340
+ data: '0xd0e30db0',
341
+ chain_id,
342
+ });
343
+ return jsonResponse({
344
+ ...result,
345
+ wrapped_token: wrapped.symbol,
346
+ wrapped_address: wrapped.address,
347
+ amount,
348
+ });
349
+ });
350
+ // ─── Tool: unwrap_eth ───────────────────────────────────────────
351
+ server.tool('unwrap_eth', 'Unwrap wrapped tokens (WETH, WAVAX, WBNB, etc.) back to native tokens. ' +
352
+ 'Specify amount in human-readable format (e.g. "0.5" for 0.5 WETH).', {
353
+ wallet_id: z.number().int().describe('Wallet ID'),
354
+ amount: z.string().describe('Amount to unwrap in human-readable format (e.g. "0.5")'),
355
+ chain_id: z.number().int().describe('Chain ID'),
356
+ }, async ({ wallet_id, amount, chain_id }) => {
357
+ const wrapped = WRAPPED_NATIVE[chain_id];
358
+ if (!wrapped) {
359
+ throw new Error(`No wrapped native token configured for chain ${chain_id}`);
360
+ }
361
+ // WETH withdraw(uint256) — selector: 0x2e1a7d4d
362
+ const rawAmount = parseUnits(amount, 18);
363
+ const calldata = '0x2e1a7d4d' + encodeUint256(rawAmount);
364
+ const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
365
+ to: wrapped.address,
366
+ value: '0',
367
+ data: calldata,
368
+ chain_id,
369
+ });
370
+ return jsonResponse({
371
+ ...result,
372
+ unwrapped_token: wrapped.symbol,
373
+ amount,
374
+ });
375
+ });
376
+ // ─── Tool: get_token_info ───────────────────────────────────────
377
+ /**
378
+ * Decode an ABI-encoded string return value from hex.
379
+ * Handles malformed data gracefully — returns empty string on any parsing failure.
380
+ */
381
+ function decodeAbiString(hex) {
382
+ try {
383
+ const clean = hex.replace('0x', '');
384
+ if (clean.length < 128)
385
+ return ''; // offset + length minimum
386
+ // First 32 bytes = offset, next 32 bytes at that offset = length
387
+ const offset = parseInt(clean.slice(0, 64), 16) * 2;
388
+ if (isNaN(offset) || offset + 64 > clean.length)
389
+ return '';
390
+ const length = parseInt(clean.slice(offset, offset + 64), 16);
391
+ if (isNaN(length) || length === 0)
392
+ return '';
393
+ const dataHex = clean.slice(offset + 64, offset + 64 + length * 2);
394
+ const pairs = dataHex.match(/.{2}/g);
395
+ if (!pairs)
396
+ return '';
397
+ // Convert hex to UTF-8
398
+ const bytes = new Uint8Array(pairs.map(b => parseInt(b, 16)));
399
+ return new TextDecoder().decode(bytes);
400
+ }
401
+ catch {
402
+ return '';
403
+ }
404
+ }
405
+ server.tool('get_token_info', 'Get the name, symbol, and decimals of any ERC-20 token by its contract address. ' +
406
+ 'Useful for discovering token details before transfers or approvals.', {
407
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
408
+ chain_id: z.number().int().describe('Chain ID'),
409
+ }, async ({ token, chain_id }) => {
410
+ // Make 3 parallel eth_call requests: name(), symbol(), decimals()
411
+ const [nameResult, symbolResult, decimalsResult] = await Promise.all([
412
+ api('/eth-call', 'POST', { chain_id, to: token, data: '0x06fdde03' }).catch(() => ({ result: '0x' })),
413
+ api('/eth-call', 'POST', { chain_id, to: token, data: '0x95d89b41' }).catch(() => ({ result: '0x' })),
414
+ api('/eth-call', 'POST', { chain_id, to: token, data: '0x313ce567' }).catch(() => ({ result: '0x' })),
415
+ ]);
416
+ const name = decodeAbiString(nameResult.result);
417
+ const symbol = decodeAbiString(symbolResult.result);
418
+ const decimalsHex = decimalsResult.result.replace('0x', '');
419
+ const decimals = decimalsHex ? parseInt(decimalsHex, 16) : 0;
420
+ return jsonResponse({
421
+ token,
422
+ chain_id,
423
+ name: name || 'Unknown',
424
+ symbol: symbol || 'Unknown',
425
+ decimals,
426
+ });
427
+ });
132
428
  // ─── Tool: get_usage ─────────────────────────────────────────────
133
429
  server.tool('get_usage', 'Get the current month\'s usage statistics. ' +
134
430
  'Returns operations count, tier info, remaining quota, and fees.', {}, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwallet-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for AgentWallet — lets AI agents create wallets, sign transactions, and broadcast on any EVM chain",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -26,9 +26,15 @@
26
26
  "evm",
27
27
  "ethereum",
28
28
  "base",
29
+ "arbitrum",
30
+ "optimism",
29
31
  "polygon",
32
+ "avalanche",
30
33
  "bsc",
34
+ "zora",
31
35
  "pulsechain",
36
+ "erc20",
37
+ "token-transfer",
32
38
  "transaction-signing",
33
39
  "agentwallet"
34
40
  ],