# Directory Structure
```
├── .gitignore
├── index.js
├── LICENSE
├── package.json
├── README.md
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | .env
3 | deprecated
4 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Token Revoke MCP
2 |
3 | An MCP server for checking and revoking ERC-20 token allowances, enhancing security and control.
4 |
5 | 
6 | 
7 | 
8 |
9 | ## Features
10 |
11 | - **Fetch Token Approvals**: Retrieve all ERC20 token approvals for a wallet on a specified chain, including token details, balances, and USD values at risk.
12 | - **Revoke Allowances**: Submit transactions to revoke ERC20 token allowances for specific spenders.
13 | - **Check Transaction Status**: Verify the success or failure of submitted transactions using transaction hashes.
14 | - **Multi-Chain Support**: Supports over 50 EVM-compatible chains, including mainnets (e.g., Ethereum, Polygon, BSC) and testnets (e.g., Goerli, Mumbai).
15 |
16 | ## Prerequisites
17 |
18 | - **Node.js**: Version 18 or higher (for native `fetch` support).
19 | - **Moralis API Key**: Required for fetching token approval data.
20 | - **Private Key**: An Ethereum-compatible private key for signing revocation transactions.
21 |
22 | ## Installation
23 |
24 | 1. **Clone the Repository**:
25 | ```bash
26 | git clone https://github.com/kukapay/token-revoke-mcp.git
27 | cd token-revoke-mcp
28 | ```
29 |
30 | 2. **Install Dependencies**:
31 | ```bash
32 | npm install
33 | ```
34 |
35 | 3. **Client Configuration**:
36 |
37 | ```json
38 | {
39 | "mcpServers": {
40 | "token-revoke-mcp": {
41 | "command": "node",
42 | "args": ["path/to/token-revoke-mcp/index.js"],
43 | "env": {
44 | "MORALIS_API_KEY": "your moralis api key",
45 | "PRIVATE_KEY": "your wallet private key"
46 | }
47 | }
48 | }
49 | }
50 | ```
51 |
52 | ## Usage
53 |
54 | Below are examples of how you might interact with the server using natural language prompts as input. The outputs are the raw `text` values from the `content` array returned by the server, assuming a client translates the prompts into tool calls.
55 |
56 | ### Example 1: Fetch Token Approvals
57 | **Input Prompt**:
58 | > "Show me all the token approvals for my wallet on Polygon."
59 |
60 | **Output Response**:
61 | ```
62 | [
63 | {
64 | "tokenAddress": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
65 | "tokenSymbol": "USDC",
66 | "balance": "100.5",
67 | "usdPrice": "1.00",
68 | "usdValueAtRisk": "50.25",
69 | "spenderAddress": "0x1111111254eeb25477b68fb85ed929f73a960582",
70 | "approvedAmount": "1000.0",
71 | "transactionHash": "0xabc...",
72 | "timestamp": "2023-10-01T12:00:00Z"
73 | }
74 | ]
75 | ```
76 |
77 | ### Example 2: Revoke an Allowance
78 | **Input Prompt**:
79 | > "Revoke the allowance for token 0x2791bca1f2de4661ed88a30c99a7a9449aa84174 to spender 0x1111111254eeb25477b68fb85ed929f73a960582 on BSC."
80 |
81 | **Output Response**:
82 | ```
83 | Allowance revocation submitted on bsc. Transaction hash: 0x123.... Note: Transaction is not yet confirmed.
84 | ```
85 |
86 | ### Example 3: Check Transaction Status
87 | **Input Prompt**:
88 | > "Did my transaction 0x123... on BSC go through?"
89 |
90 | **Output Response** (possible outputs):
91 | - **Pending**:
92 | ```
93 | Transaction 0x123... on bsc is still pending or not found.
94 | ```
95 | - **Success**:
96 | ```
97 | Transaction 0x123... on bsc has completed with status: successful. Block number: 12345.
98 | ```
99 | - **Failure**:
100 | ```
101 | Transaction 0x123... on bsc has completed with status: failed. Block number: 12345.
102 | ```
103 |
104 | ## Supported Chains
105 |
106 | The server supports a wide range of EVM-compatible chains based on the Moralis JS SDK’s `chaindata.ts`. Examples include:
107 | - Mainnets: `ethereum`, `polygon`, `bsc`, `avalanche`, `fantom`, `arbitrum`, `optimism`, etc.
108 | - Testnets: `goerli`, `mumbai`, `bsc testnet`, `arbitrum goerli`, `optimism sepolia`, etc.
109 | - Full list: See `SUPPORTED_CHAINS` in `server.js`.
110 |
111 |
112 | ## License
113 |
114 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
115 |
116 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "token-revoke-mcp",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "homepage": "https://github.com/kukapay/token-revoke-mcp",
6 | "license": "MIT",
7 | "dependencies": {
8 | "@modelcontextprotocol/sdk": "^1.8.0",
9 | "dotenv": "^16.4.7",
10 | "ethers": "^6.13.5"
11 | }
12 | }
13 |
```
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
```javascript
1 | require('dotenv').config(); // Load environment variables
2 | const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
3 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
4 | const { z } = require('zod');
5 | const { ethers } = require('ethers');
6 |
7 | // Configuration
8 | const MORALIS_API_KEY = process.env.MORALIS_API_KEY;
9 | const PRIVATE_KEY = process.env.PRIVATE_KEY;
10 | const MORALIS_API_BASE_URL = 'https://deep-index.moralis.io/api/v2.2';
11 |
12 | // Chain configurations based on Moralis-JS-SDK chaindata.ts with hex string chain IDs
13 | const SUPPORTED_CHAINS = {
14 | "ethereum": { chainId: "0x1", rpcUrl: "https://rpc.ankr.com/eth" }, // Ethereum Mainnet
15 | "ropsten": { chainId: "0x3", rpcUrl: "https://rpc.ankr.com/eth_ropsten" }, // Ethereum Testnet Ropsten
16 | "rinkeby": { chainId: "0x4", rpcUrl: "https://rpc.ankr.com/eth_rinkeby" }, // Ethereum Testnet Rinkeby
17 | "goerli": { chainId: "0x5", rpcUrl: "https://rpc.ankr.com/eth_goerli" }, // Ethereum Testnet Goerli
18 | "kovan": { chainId: "0x2a", rpcUrl: "https://rpc.ankr.com/eth_kovan" }, // Ethereum Testnet Kovan
19 | "polygon": { chainId: "0x89", rpcUrl: "https://polygon-rpc.com" }, // Polygon Mainnet
20 | "mumbai": { chainId: "0x13881", rpcUrl: "https://rpc-mumbai.maticvigil.com" }, // Polygon Testnet Mumbai
21 | "bsc": { chainId: "0x38", rpcUrl: "https://bsc-dataseed.binance.org" }, // Binance Smart Chain Mainnet
22 | "bsc testnet": { chainId: "0x61", rpcUrl: "https://data-seed-prebsc-1-s1.binance.org:8545" }, // Binance Smart Chain Testnet
23 | "avalanche": { chainId: "0xa86a", rpcUrl: "https://api.avax.network/ext/bc/C/rpc" }, // Avalanche C-Chain Mainnet
24 | "avalanche testnet": { chainId: "0xa869", rpcUrl: "https://api.avax-test.network/ext/bc/C/rpc" }, // Avalanche Testnet
25 | "fantom": { chainId: "0xfa", rpcUrl: "https://rpc.ftm.tools" }, // Fantom Opera Mainnet
26 | "cronos": { chainId: "0x19", rpcUrl: "https://evm.cronos.org" }, // Cronos Mainnet
27 | "cronos testnet": { chainId: "0x152", rpcUrl: "https://evm-t3.cronos.org" }, // Cronos Testnet
28 | "palm": { chainId: "0x2a15c308d", rpcUrl: "https://palm-mainnet.public.blastapi.io" }, // Palm Mainnet
29 | "arbitrum": { chainId: "0xa4b1", rpcUrl: "https://arb1.arbitrum.io/rpc" }, // Arbitrum One Mainnet
30 | "arbitrum goerli": { chainId: "0x66eed", rpcUrl: "https://goerli-rollup.arbitrum.io/rpc" }, // Arbitrum Testnet Goerli
31 | "chiliz": { chainId: "0x15b38", rpcUrl: "https://rpc.ankr.com/chiliz" }, // Chiliz Mainnet
32 | "chiliz testnet": { chainId: "0x15b32", rpcUrl: "https://testnet-rpc.chiliz.com" }, // Chiliz Testnet
33 | "gnosis": { chainId: "0x64", rpcUrl: "https://rpc.gnosischain.com" }, // Gnosis Chain Mainnet
34 | "base": { chainId: "0x2105", rpcUrl: "https://mainnet.base.org" }, // Base Mainnet
35 | "base goerli": { chainId: "0x14a33", rpcUrl: "https://goerli.base.org" }, // Base Testnet Goerli
36 | "base sepolia": { chainId: "0x14a34", rpcUrl: "https://sepolia.base.org" }, // Base Testnet Sepolia
37 | "scroll": { chainId: "0x82750", rpcUrl: "https://rpc.scroll.io" }, // Scroll Mainnet
38 | "scroll sepolia": { chainId: "0x8274f", rpcUrl: "https://sepolia-rpc.scroll.io" }, // Scroll Testnet Sepolia
39 | "optimism": { chainId: "0xa", rpcUrl: "https://mainnet.optimism.io" }, // Optimism Mainnet
40 | "optimism goerli": { chainId: "0x1a4", rpcUrl: "https://goerli.optimism.io" }, // Optimism Testnet Goerli
41 | "optimism sepolia": { chainId: "0xaa37dc", rpcUrl: "https://sepolia.optimism.io" }, // Optimism Testnet Sepolia
42 | "klaytn": { chainId: "0x2019", rpcUrl: "https://public-en-cypress.klaytn.net" }, // Klaytn Mainnet Cypress
43 | "zksync": { chainId: "0x144", rpcUrl: "https://mainnet.era.zksync.io" }, // zkSync Era Mainnet
44 | "zksync sepolia": { chainId: "0x12c", rpcUrl: "https://sepolia.era.zksync.dev" }, // zkSync Era Testnet Sepolia
45 | "polygonzkevm": { chainId: "0x44d", rpcUrl: "https://zkevm-rpc.com" }, // Polygon zkEVM Mainnet
46 | "polygonzkevm testnet": { chainId: "0x585", rpcUrl: "https://rpc.public.zkevm-test.net" }, // Polygon zkEVM Testnet
47 | "moonriver": { chainId: "0x505", rpcUrl: "https://rpc.api.moonriver.moonbeam.network" }, // Moonriver Mainnet
48 | "moonbeam": { chainId: "0x504", rpcUrl: "https://rpc.api.moonbeam.network" }, // Moonbeam Mainnet
49 | "moonbase": { chainId: "0x507", rpcUrl: "https://rpc.api.moonbase.moonbeam.network" }, // Moonbase Alpha Testnet
50 | "linea": { chainId: "0xe708", rpcUrl: "https://rpc.linea.build" }, // Linea Mainnet
51 | "linea goerli": { chainId: "0xe704", rpcUrl: "https://rpc.goerli.linea.build" }, // Linea Testnet Goerli
52 | "core": { chainId: "0x45c", rpcUrl: "https://rpc.coredao.org" }, // Core Blockchain Mainnet
53 | "aurora": { chainId: "0x4e454152", rpcUrl: "https://mainnet.aurora.dev" }, // Aurora Mainnet
54 | "aurora testnet": { chainId: "0x4e454153", rpcUrl: "https://testnet.aurora.dev" }, // Aurora Testnet
55 | "celo": { chainId: "0xa4ec", rpcUrl: "https://forno.celo.org" }, // Celo Mainnet
56 | "celo alfajores": { chainId: "0xaef3", rpcUrl: "https://alfajores-forno.celo-testnet.org" }, // Celo Alfajores Testnet
57 | "blast": { chainId: "0x13e31", rpcUrl: "https://rpc.blast.io" }, // Blast Mainnet
58 | "blast sepolia": { chainId: "0xa0c71fd", rpcUrl: "https://sepolia.blast.io" }, // Blast Sepolia Testnet
59 | "mantle": { chainId: "0x1388", rpcUrl: "https://rpc.mantle.xyz" }, // Mantle Mainnet
60 | "mantle sepolia": { chainId: "0x1389", rpcUrl: "https://rpc.sepolia.mantle.xyz" }, // Mantle Sepolia Testnet
61 | "sei": { chainId: "0x531", rpcUrl: "https://evm-rpc.sei-apis.com" }, // Sei Mainnet
62 | "sei testnet": { chainId: "0x15e25", rpcUrl: "https://evm-rpc-testnet.sei-apis.com" }, // Sei Testnet
63 | "rootstock": { chainId: "0x1e", rpcUrl: "https://public-node.rsk.co" }, // Rootstock Mainnet
64 | "rootstock testnet": { chainId: "0x1f", rpcUrl: "https://public-node.testnet.rsk.co" }, // Rootstock Testnet
65 | "holesky": { chainId: "0x4268", rpcUrl: "https://rpc.holesky.ethpandaops.io" }, // Holesky Testnet
66 | };
67 |
68 | // ERC20 ABI
69 | const ERC20_ABI = [
70 | "function allowance(address owner, address spender) view returns (uint256)",
71 | "function approve(address spender, uint256 amount) returns (bool)",
72 | ];
73 |
74 | // Create wallet instance from private key
75 | function createWallet(provider) {
76 | if (!PRIVATE_KEY) {
77 | throw new Error("PRIVATE_KEY not set in environment variables");
78 | }
79 | return new ethers.Wallet(PRIVATE_KEY, provider);
80 | }
81 |
82 | // Get default wallet address from private key
83 | const DEFAULT_WALLET_ADDRESS = PRIVATE_KEY ? new ethers.Wallet(PRIVATE_KEY).address : null;
84 |
85 | async function main() {
86 | // Ensure PRIVATE_KEY and MORALIS_API_KEY are set
87 | if (!DEFAULT_WALLET_ADDRESS) {
88 | throw new Error("PRIVATE_KEY not set in environment variables");
89 | }
90 | if (!MORALIS_API_KEY) {
91 | throw new Error("MORALIS_API_KEY not set in environment variables");
92 | }
93 |
94 | // Create MCP Server
95 | const server = new McpServer({
96 | name: "TokenRevokeMcp", // Name corresponds to "token-revoke-mcp"
97 | version: "1.0.0",
98 | description: "Multi-chain ERC20 token allowance management",
99 | });
100 |
101 | server.tool(
102 | "getApprovals",
103 | "Fetches all ERC20 token approvals for a wallet on a specified chain",
104 | {
105 | chain: z.string().optional().default("ethereum").describe(`Blockchain network (e.g., ${Object.keys(SUPPORTED_CHAINS).join(", ")})`),
106 | walletAddress: z.string().regex(/^(0x[a-fA-F0-9]{40})?$/, "Invalid Ethereum address").optional().default("").describe("Wallet address to check (DEFAULT_WALLET_ADDRESS will be used if not set)"),
107 | },
108 | async ({ chain, walletAddress }) => {
109 | try {
110 | const selectedChain = SUPPORTED_CHAINS[chain.toLowerCase()];
111 | if (!selectedChain) {
112 | throw new Error(`Unsupported chain: ${chain}. Supported chains: ${Object.keys(SUPPORTED_CHAINS).join(", ")}`);
113 | }
114 |
115 | if(!walletAddress) {
116 | walletAddress = DEFAULT_WALLET_ADDRESS
117 | }
118 |
119 | // Make HTTP request to Moralis API using fetch
120 | const url = `${MORALIS_API_BASE_URL}/wallets/${walletAddress}/approvals?chain=${selectedChain.chainId}`;
121 | const response = await fetch(url, {
122 | method: 'GET',
123 | headers: {
124 | "X-API-Key": MORALIS_API_KEY,
125 | "Accept": "application/json",
126 | },
127 | });
128 |
129 | if (!response.ok) {
130 | const errorData = await response.json();
131 | throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
132 | }
133 |
134 | const data = await response.json();
135 | const allowances = data.result.map(approval => ({
136 | tokenAddress: approval.token.address,
137 | tokenSymbol: approval.token.symbol || "Unknown",
138 | balance: approval.token.current_balance_formatted || "0",
139 | usdPrice: approval.token.usd_price || "N/A",
140 | usdValueAtRisk: approval.token.usd_at_risk || "0",
141 | spenderAddress: approval.spender.address,
142 | approvedAmount: approval.value_formatted,
143 | transactionHash: approval.transaction_hash,
144 | timestamp: approval.block_timestamp
145 | }));
146 |
147 | return {
148 | content: [{
149 | type: "text",
150 | text: JSON.stringify(allowances, null, 2),
151 | }],
152 | };
153 | } catch (error) {
154 | return {
155 | content: [{
156 | type: "text",
157 | text: `Error fetching allowances: ${error.message}`,
158 | }],
159 | isError: true,
160 | };
161 | }
162 | }
163 | );
164 |
165 | // Tool 2: Revoke a specific token allowance
166 | server.tool(
167 | "revokeAllowance",
168 | "Revokes an ERC20 token allowance for a specific spender on a specified chain",
169 | {
170 | chain: z.string().optional().default("ethereum").describe(`Blockchain network (e.g., ${Object.keys(SUPPORTED_CHAINS).join(", ")})`),
171 | tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address").describe("Token contract address"),
172 | spenderAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address").describe("Spender address to revoke"),
173 | },
174 | async ({ chain, tokenAddress, spenderAddress }) => {
175 | try {
176 | const selectedChain = SUPPORTED_CHAINS[chain.toLowerCase()];
177 | if (!selectedChain) {
178 | throw new Error(`Unsupported chain: ${chain}. Supported chains: ${Object.keys(SUPPORTED_CHAINS).join(", ")}`);
179 | }
180 |
181 | const provider = new ethers.JsonRpcProvider(selectedChain.rpcUrl);
182 | const wallet = createWallet(provider);
183 | const signedContract = new ethers.Contract(tokenAddress, ERC20_ABI, wallet);
184 |
185 | const tx = await signedContract.approve(spenderAddress, 0);
186 |
187 | return {
188 | content: [{
189 | type: "text",
190 | text: `Allowance revocation submitted on ${chain}. Transaction hash: ${tx.hash}. Note: Transaction is not yet confirmed.`,
191 | }],
192 | };
193 | } catch (error) {
194 | return {
195 | content: [{
196 | type: "text",
197 | text: `Error revoking allowance: ${error.message}`,
198 | }],
199 | isError: true,
200 | };
201 | }
202 | }
203 | );
204 |
205 | server.tool(
206 | "checkTransactionStatus",
207 | "Checks the status of a transaction on a specified chain",
208 | {
209 | chain: z.string().optional().default("ethereum").describe(`Blockchain network (e.g., ${Object.keys(SUPPORTED_CHAINS).join(", ")})`),
210 | txHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").describe("Transaction hash to check"),
211 | },
212 | async ({ chain, txHash }) => {
213 | try {
214 | const selectedChain = SUPPORTED_CHAINS[chain.toLowerCase()];
215 | if (!selectedChain) {
216 | throw new Error(`Unsupported chain: ${chain}. Supported chains: ${Object.keys(SUPPORTED_CHAINS).join(", ")}`);
217 | }
218 |
219 | const provider = new ethers.JsonRpcProvider(selectedChain.rpcUrl);
220 | const receipt = await provider.getTransactionReceipt(txHash);
221 |
222 | if (!receipt) {
223 | return {
224 | content: [{
225 | type: "text",
226 | text: `Transaction ${txHash} on ${chain} is still pending or not found.`,
227 | }],
228 | };
229 | }
230 |
231 | const status = receipt.status === 1 ? "successful" : "failed";
232 | return {
233 | content: [{
234 | type: "text",
235 | text: `Transaction ${txHash} on ${chain} has completed with status: ${status}. Block number: ${receipt.blockNumber}.`,
236 | }],
237 | };
238 | } catch (error) {
239 | return {
240 | content: [{
241 | type: "text",
242 | text: `Error checking transaction status: ${error.message}`,
243 | }],
244 | isError: true,
245 | };
246 | }
247 | }
248 | );
249 |
250 | // Connect to Stdio transport
251 | const transport = new StdioServerTransport();
252 | await server.connect(transport);
253 | }
254 |
255 | main().catch(console.error);
256 |
257 |
```