#
tokens: 5910/50000 4/4 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ![License](https://img.shields.io/badge/license-MIT-blue.svg)
  6 | ![Node.js](https://img.shields.io/badge/Node.js-18.x-green.svg)
  7 | ![Status](https://img.shields.io/badge/status-active-brightgreen.svg)
  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 | 
```