Skip to content

Commit 4f19f12

Browse files
committed
feat: adding ENS support
1 parent 0e91efe commit 4f19f12

File tree

7 files changed

+280
-216
lines changed

7 files changed

+280
-216
lines changed

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The MCP EVM Server leverages the Model Context Protocol to provide blockchain op
3333
- Transferring tokens (native, ERC20, ERC721, ERC1155)
3434
- Querying token metadata and balances
3535
- Chain-specific operations across 30+ EVM networks
36+
- ENS name resolution for all address parameters
3637

3738
All operations are exposed through a consistent interface of MCP tools and resources, making it easy for AI agents to discover and use blockchain functionality.
3839

@@ -45,6 +46,7 @@ All operations are exposed through a consistent interface of MCP tools and resou
4546
- **Block data** access by number, hash, or latest
4647
- **Transaction details** and receipts with decoded logs
4748
- **Address balances** for native tokens and all token standards
49+
- **ENS resolution** for human-readable Ethereum addresses
4850

4951
### Token Operations
5052

@@ -195,22 +197,22 @@ bun dev:http
195197

196198
Connect to this MCP server using any MCP-compatible client. For testing and debugging, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
197199

198-
### Example: Getting a Token Balance
200+
### Example: Getting a Token Balance with ENS
199201

200202
```javascript
201-
// Example of using the MCP client to check a token balance
203+
// Example of using the MCP client to check a token balance using ENS
202204
const mcp = new McpClient("http://localhost:3000");
203205

204206
const result = await mcp.invokeTool("get-token-balance", {
205207
tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
206-
ownerAddress: "0x1234567890abcdef1234567890abcdef12345678",
208+
ownerAddress: "vitalik.eth", // ENS name instead of address
207209
network: "ethereum"
208210
});
209211

210212
console.log(result);
211213
// {
212214
// tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
213-
// owner: "0x1234567890abcdef1234567890abcdef12345678",
215+
// owner: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
214216
// network: "ethereum",
215217
// raw: "1000000000",
216218
// formatted: "1000",

src/core/operations/balance.ts

+75-42
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'viem';
88
import { getPublicClient } from './clients.js';
99
import { readContract } from './contracts.js';
10+
import { resolveAddress } from './ens.js';
1011

1112
// Standard ERC20 ABI (minimal for reading)
1213
const erc20Abi = [
@@ -66,27 +67,37 @@ const erc1155Abi = [
6667
] as const;
6768

6869
/**
69-
* Get the ETH balance of an address for a specific network
70+
* Get the ETH balance for an address
71+
* @param addressOrEns Ethereum address or ENS name
72+
* @param network Network name or chain ID
73+
* @returns Balance in wei and ether
7074
*/
7175
export async function getETHBalance(
72-
address: Address,
76+
addressOrEns: string,
7377
network = 'ethereum'
7478
): Promise<{ wei: bigint; ether: string }> {
79+
// Resolve ENS name to address if needed
80+
const address = await resolveAddress(addressOrEns, network);
81+
7582
const client = getPublicClient(network);
76-
const balanceWei = await client.getBalance({ address });
83+
const balance = await client.getBalance({ address });
7784

7885
return {
79-
wei: balanceWei,
80-
ether: formatEther(balanceWei)
86+
wei: balance,
87+
ether: formatEther(balance)
8188
};
8289
}
8390

8491
/**
85-
* Get the ERC20 token balance of an address for a specific network
92+
* Get the balance of an ERC20 token for an address
93+
* @param tokenAddressOrEns Token contract address or ENS name
94+
* @param ownerAddressOrEns Owner address or ENS name
95+
* @param network Network name or chain ID
96+
* @returns Token balance with formatting information
8697
*/
8798
export async function getERC20Balance(
88-
tokenAddress: Address,
89-
ownerAddress: Address,
99+
tokenAddressOrEns: string,
100+
ownerAddressOrEns: string,
90101
network = 'ethereum'
91102
): Promise<{
92103
raw: bigint;
@@ -96,6 +107,10 @@ export async function getERC20Balance(
96107
decimals: number;
97108
}
98109
}> {
110+
// Resolve ENS names to addresses if needed
111+
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
112+
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
113+
99114
const publicClient = getPublicClient(network);
100115

101116
const contract = getContract({
@@ -122,65 +137,83 @@ export async function getERC20Balance(
122137

123138
/**
124139
* Check if an address owns a specific NFT
140+
* @param tokenAddressOrEns NFT contract address or ENS name
141+
* @param ownerAddressOrEns Owner address or ENS name
142+
* @param tokenId Token ID to check
143+
* @param network Network name or chain ID
144+
* @returns True if the address owns the NFT
125145
*/
126146
export async function isNFTOwner(
127-
tokenAddress: Address,
128-
ownerAddress: Address,
147+
tokenAddressOrEns: string,
148+
ownerAddressOrEns: string,
129149
tokenId: bigint,
130150
network = 'ethereum'
131151
): Promise<boolean> {
132-
const publicClient = getPublicClient(network);
133-
134-
const contract = getContract({
135-
address: tokenAddress,
136-
abi: erc721Abi,
137-
client: publicClient,
138-
});
139-
152+
// Resolve ENS names to addresses if needed
153+
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
154+
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
155+
140156
try {
141-
const owner = await contract.read.ownerOf([tokenId]);
142-
return owner.toLowerCase() === ownerAddress.toLowerCase();
143-
} catch (error) {
144-
// If the token doesn't exist or there's an error, return false
157+
const actualOwner = await readContract({
158+
address: tokenAddress,
159+
abi: erc721Abi,
160+
functionName: 'ownerOf',
161+
args: [tokenId]
162+
}, network) as Address;
163+
164+
return actualOwner.toLowerCase() === ownerAddress.toLowerCase();
165+
} catch (error: any) {
166+
console.error(`Error checking NFT ownership: ${error.message}`);
145167
return false;
146168
}
147169
}
148170

149171
/**
150-
* Get ERC721 NFT balance for an address (number of NFTs owned)
172+
* Get the number of NFTs owned by an address for a specific collection
173+
* @param tokenAddressOrEns NFT contract address or ENS name
174+
* @param ownerAddressOrEns Owner address or ENS name
175+
* @param network Network name or chain ID
176+
* @returns Number of NFTs owned
151177
*/
152178
export async function getERC721Balance(
153-
tokenAddress: Address,
154-
ownerAddress: Address,
179+
tokenAddressOrEns: string,
180+
ownerAddressOrEns: string,
155181
network = 'ethereum'
156182
): Promise<bigint> {
157-
const publicClient = getPublicClient(network);
158-
159-
const contract = getContract({
183+
// Resolve ENS names to addresses if needed
184+
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
185+
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
186+
187+
return readContract({
160188
address: tokenAddress,
161189
abi: erc721Abi,
162-
client: publicClient,
163-
});
164-
165-
return contract.read.balanceOf([ownerAddress]);
190+
functionName: 'balanceOf',
191+
args: [ownerAddress]
192+
}, network) as Promise<bigint>;
166193
}
167194

168195
/**
169-
* Get ERC1155 token balance
196+
* Get the balance of an ERC1155 token for an address
197+
* @param tokenAddressOrEns ERC1155 contract address or ENS name
198+
* @param ownerAddressOrEns Owner address or ENS name
199+
* @param tokenId Token ID to check
200+
* @param network Network name or chain ID
201+
* @returns Token balance
170202
*/
171203
export async function getERC1155Balance(
172-
tokenAddress: Address,
173-
ownerAddress: Address,
204+
tokenAddressOrEns: string,
205+
ownerAddressOrEns: string,
174206
tokenId: bigint,
175207
network = 'ethereum'
176208
): Promise<bigint> {
177-
const publicClient = getPublicClient(network);
178-
179-
const contract = getContract({
209+
// Resolve ENS names to addresses if needed
210+
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
211+
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
212+
213+
return readContract({
180214
address: tokenAddress,
181215
abi: erc1155Abi,
182-
client: publicClient,
183-
});
184-
185-
return contract.read.balanceOf([ownerAddress, tokenId]);
216+
functionName: 'balanceOf',
217+
args: [ownerAddress, tokenId]
218+
}, network) as Promise<bigint>;
186219
}

src/core/operations/contracts.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type Log
88
} from 'viem';
99
import { getPublicClient, getWalletClient } from './clients.js';
10+
import { resolveAddress } from './ens.js';
1011

1112
/**
1213
* Read from a contract for a specific network
@@ -38,8 +39,14 @@ export async function getLogs(params: GetLogsParameters, network = 'ethereum'):
3839

3940
/**
4041
* Check if an address is a contract
42+
* @param addressOrEns Address or ENS name to check
43+
* @param network Network name or chain ID
44+
* @returns True if the address is a contract, false if it's an EOA
4145
*/
42-
export async function isContract(address: Address, network = 'ethereum'): Promise<boolean> {
46+
export async function isContract(addressOrEns: string, network = 'ethereum'): Promise<boolean> {
47+
// Resolve ENS name to address if needed
48+
const address = await resolveAddress(addressOrEns, network);
49+
4350
const client = getPublicClient(network);
4451
const code = await client.getBytecode({ address });
4552
return code !== undefined && code !== '0x';

src/core/operations/ens.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { normalize } from 'viem/ens';
2+
import { getPublicClient } from './clients.js';
3+
import { type Address } from 'viem';
4+
5+
/**
6+
* Resolves an ENS name to an Ethereum address or returns the original address if it's already valid
7+
* @param addressOrEns An Ethereum address or ENS name
8+
* @param network The network to use for ENS resolution (defaults to Ethereum mainnet)
9+
* @returns The resolved Ethereum address
10+
*/
11+
export async function resolveAddress(
12+
addressOrEns: string,
13+
network = 'ethereum'
14+
): Promise<Address> {
15+
// If it's already a valid Ethereum address (0x followed by 40 hex chars), return it
16+
if (/^0x[a-fA-F0-9]{40}$/.test(addressOrEns)) {
17+
return addressOrEns as Address;
18+
}
19+
20+
// If it looks like an ENS name (contains a dot), try to resolve it
21+
if (addressOrEns.includes('.')) {
22+
try {
23+
// Normalize the ENS name first
24+
const normalizedEns = normalize(addressOrEns);
25+
26+
// Get the public client for the network
27+
const publicClient = getPublicClient(network);
28+
29+
// Resolve the ENS name to an address
30+
const address = await publicClient.getEnsAddress({
31+
name: normalizedEns,
32+
});
33+
34+
if (!address) {
35+
throw new Error(`ENS name ${addressOrEns} could not be resolved to an address`);
36+
}
37+
38+
return address;
39+
} catch (error: any) {
40+
throw new Error(`Failed to resolve ENS name ${addressOrEns}: ${error.message}`);
41+
}
42+
}
43+
44+
// If it's neither a valid address nor an ENS name, throw an error
45+
throw new Error(`Invalid address or ENS name: ${addressOrEns}`);
46+
}

src/core/operations/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './blocks.js';
66
export * from './transactions.js';
77
export * from './contracts.js';
88
export * from './tokens.js';
9+
export * from './ens.js';
910
export { utils as helpers } from './utils.js';
1011

1112
// Re-export common types for convenience

0 commit comments

Comments
 (0)