# 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 | ```