#
tokens: 4859/50000 4/4 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── index.js
├── LICENSE
├── package.json
├── README.md
└── yarn.lock
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules
.env
deprecated

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Token Revoke MCP

An MCP server for checking and revoking ERC-20 token allowances, enhancing security and control.

![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Node.js](https://img.shields.io/badge/Node.js-18.x-green.svg)
![Status](https://img.shields.io/badge/status-active-brightgreen.svg)

## Features

- **Fetch Token Approvals**: Retrieve all ERC20 token approvals for a wallet on a specified chain, including token details, balances, and USD values at risk.
- **Revoke Allowances**: Submit transactions to revoke ERC20 token allowances for specific spenders.
- **Check Transaction Status**: Verify the success or failure of submitted transactions using transaction hashes.
- **Multi-Chain Support**: Supports over 50 EVM-compatible chains, including mainnets (e.g., Ethereum, Polygon, BSC) and testnets (e.g., Goerli, Mumbai).

## Prerequisites

- **Node.js**: Version 18 or higher (for native `fetch` support).
- **Moralis API Key**: Required for fetching token approval data.
- **Private Key**: An Ethereum-compatible private key for signing revocation transactions.

## Installation

1. **Clone the Repository**:
   ```bash
   git clone https://github.com/kukapay/token-revoke-mcp.git
   cd token-revoke-mcp
   ```

2. **Install Dependencies**:
   ```bash
   npm install
   ```
   
3. **Client Configuration**:

    ```json
    {
      "mcpServers": {
        "token-revoke-mcp": {
          "command": "node",
          "args": ["path/to/token-revoke-mcp/index.js"],
          "env": {
            "MORALIS_API_KEY": "your moralis api key",
            "PRIVATE_KEY": "your wallet private key"
          }
        }
      }
    }   
    ```

## Usage

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.

### Example 1: Fetch Token Approvals
**Input Prompt**:  
> "Show me all the token approvals for my wallet on Polygon."

**Output Response**:  
```
[
  {
    "tokenAddress": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
    "tokenSymbol": "USDC",
    "balance": "100.5",
    "usdPrice": "1.00",
    "usdValueAtRisk": "50.25",
    "spenderAddress": "0x1111111254eeb25477b68fb85ed929f73a960582",
    "approvedAmount": "1000.0",
    "transactionHash": "0xabc...",
    "timestamp": "2023-10-01T12:00:00Z"
  }
]
```

### Example 2: Revoke an Allowance
**Input Prompt**:  
> "Revoke the allowance for token 0x2791bca1f2de4661ed88a30c99a7a9449aa84174 to spender 0x1111111254eeb25477b68fb85ed929f73a960582 on BSC."

**Output Response**:  
```
Allowance revocation submitted on bsc. Transaction hash: 0x123.... Note: Transaction is not yet confirmed.
```

### Example 3: Check Transaction Status
**Input Prompt**:  
> "Did my transaction 0x123... on BSC go through?"

**Output Response** (possible outputs):  
- **Pending**:  
  ```
  Transaction 0x123... on bsc is still pending or not found.
  ```
- **Success**:  
  ```
  Transaction 0x123... on bsc has completed with status: successful. Block number: 12345.
  ```
- **Failure**:  
  ```
  Transaction 0x123... on bsc has completed with status: failed. Block number: 12345.
  ```

## Supported Chains

The server supports a wide range of EVM-compatible chains based on the Moralis JS SDK’s `chaindata.ts`. Examples include:
- Mainnets: `ethereum`, `polygon`, `bsc`, `avalanche`, `fantom`, `arbitrum`, `optimism`, etc.
- Testnets: `goerli`, `mumbai`, `bsc testnet`, `arbitrum goerli`, `optimism sepolia`, etc.
- Full list: See `SUPPORTED_CHAINS` in `server.js`.


## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.


```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "token-revoke-mcp",
  "version": "1.0.0",
  "main": "index.js",
  "homepage": "https://github.com/kukapay/token-revoke-mcp",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.8.0",
    "dotenv": "^16.4.7",
    "ethers": "^6.13.5"
  }
}

```

--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------

```javascript
require('dotenv').config(); // Load environment variables
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const { z } = require('zod');
const { ethers } = require('ethers');

// Configuration
const MORALIS_API_KEY = process.env.MORALIS_API_KEY;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const MORALIS_API_BASE_URL = 'https://deep-index.moralis.io/api/v2.2';

// Chain configurations based on Moralis-JS-SDK chaindata.ts with hex string chain IDs
const SUPPORTED_CHAINS = {
  "ethereum": { chainId: "0x1", rpcUrl: "https://rpc.ankr.com/eth" }, // Ethereum Mainnet
  "ropsten": { chainId: "0x3", rpcUrl: "https://rpc.ankr.com/eth_ropsten" }, // Ethereum Testnet Ropsten
  "rinkeby": { chainId: "0x4", rpcUrl: "https://rpc.ankr.com/eth_rinkeby" }, // Ethereum Testnet Rinkeby
  "goerli": { chainId: "0x5", rpcUrl: "https://rpc.ankr.com/eth_goerli" }, // Ethereum Testnet Goerli
  "kovan": { chainId: "0x2a", rpcUrl: "https://rpc.ankr.com/eth_kovan" }, // Ethereum Testnet Kovan
  "polygon": { chainId: "0x89", rpcUrl: "https://polygon-rpc.com" }, // Polygon Mainnet
  "mumbai": { chainId: "0x13881", rpcUrl: "https://rpc-mumbai.maticvigil.com" }, // Polygon Testnet Mumbai
  "bsc": { chainId: "0x38", rpcUrl: "https://bsc-dataseed.binance.org" }, // Binance Smart Chain Mainnet
  "bsc testnet": { chainId: "0x61", rpcUrl: "https://data-seed-prebsc-1-s1.binance.org:8545" }, // Binance Smart Chain Testnet
  "avalanche": { chainId: "0xa86a", rpcUrl: "https://api.avax.network/ext/bc/C/rpc" }, // Avalanche C-Chain Mainnet
  "avalanche testnet": { chainId: "0xa869", rpcUrl: "https://api.avax-test.network/ext/bc/C/rpc" }, // Avalanche Testnet
  "fantom": { chainId: "0xfa", rpcUrl: "https://rpc.ftm.tools" }, // Fantom Opera Mainnet
  "cronos": { chainId: "0x19", rpcUrl: "https://evm.cronos.org" }, // Cronos Mainnet
  "cronos testnet": { chainId: "0x152", rpcUrl: "https://evm-t3.cronos.org" }, // Cronos Testnet
  "palm": { chainId: "0x2a15c308d", rpcUrl: "https://palm-mainnet.public.blastapi.io" }, // Palm Mainnet
  "arbitrum": { chainId: "0xa4b1", rpcUrl: "https://arb1.arbitrum.io/rpc" }, // Arbitrum One Mainnet
  "arbitrum goerli": { chainId: "0x66eed", rpcUrl: "https://goerli-rollup.arbitrum.io/rpc" }, // Arbitrum Testnet Goerli
  "chiliz": { chainId: "0x15b38", rpcUrl: "https://rpc.ankr.com/chiliz" }, // Chiliz Mainnet
  "chiliz testnet": { chainId: "0x15b32", rpcUrl: "https://testnet-rpc.chiliz.com" }, // Chiliz Testnet
  "gnosis": { chainId: "0x64", rpcUrl: "https://rpc.gnosischain.com" }, // Gnosis Chain Mainnet
  "base": { chainId: "0x2105", rpcUrl: "https://mainnet.base.org" }, // Base Mainnet
  "base goerli": { chainId: "0x14a33", rpcUrl: "https://goerli.base.org" }, // Base Testnet Goerli
  "base sepolia": { chainId: "0x14a34", rpcUrl: "https://sepolia.base.org" }, // Base Testnet Sepolia
  "scroll": { chainId: "0x82750", rpcUrl: "https://rpc.scroll.io" }, // Scroll Mainnet
  "scroll sepolia": { chainId: "0x8274f", rpcUrl: "https://sepolia-rpc.scroll.io" }, // Scroll Testnet Sepolia
  "optimism": { chainId: "0xa", rpcUrl: "https://mainnet.optimism.io" }, // Optimism Mainnet
  "optimism goerli": { chainId: "0x1a4", rpcUrl: "https://goerli.optimism.io" }, // Optimism Testnet Goerli
  "optimism sepolia": { chainId: "0xaa37dc", rpcUrl: "https://sepolia.optimism.io" }, // Optimism Testnet Sepolia
  "klaytn": { chainId: "0x2019", rpcUrl: "https://public-en-cypress.klaytn.net" }, // Klaytn Mainnet Cypress
  "zksync": { chainId: "0x144", rpcUrl: "https://mainnet.era.zksync.io" }, // zkSync Era Mainnet
  "zksync sepolia": { chainId: "0x12c", rpcUrl: "https://sepolia.era.zksync.dev" }, // zkSync Era Testnet Sepolia
  "polygonzkevm": { chainId: "0x44d", rpcUrl: "https://zkevm-rpc.com" }, // Polygon zkEVM Mainnet
  "polygonzkevm testnet": { chainId: "0x585", rpcUrl: "https://rpc.public.zkevm-test.net" }, // Polygon zkEVM Testnet
  "moonriver": { chainId: "0x505", rpcUrl: "https://rpc.api.moonriver.moonbeam.network" }, // Moonriver Mainnet
  "moonbeam": { chainId: "0x504", rpcUrl: "https://rpc.api.moonbeam.network" }, // Moonbeam Mainnet
  "moonbase": { chainId: "0x507", rpcUrl: "https://rpc.api.moonbase.moonbeam.network" }, // Moonbase Alpha Testnet
  "linea": { chainId: "0xe708", rpcUrl: "https://rpc.linea.build" }, // Linea Mainnet
  "linea goerli": { chainId: "0xe704", rpcUrl: "https://rpc.goerli.linea.build" }, // Linea Testnet Goerli
  "core": { chainId: "0x45c", rpcUrl: "https://rpc.coredao.org" }, // Core Blockchain Mainnet
  "aurora": { chainId: "0x4e454152", rpcUrl: "https://mainnet.aurora.dev" }, // Aurora Mainnet
  "aurora testnet": { chainId: "0x4e454153", rpcUrl: "https://testnet.aurora.dev" }, // Aurora Testnet
  "celo": { chainId: "0xa4ec", rpcUrl: "https://forno.celo.org" }, // Celo Mainnet
  "celo alfajores": { chainId: "0xaef3", rpcUrl: "https://alfajores-forno.celo-testnet.org" }, // Celo Alfajores Testnet
  "blast": { chainId: "0x13e31", rpcUrl: "https://rpc.blast.io" }, // Blast Mainnet
  "blast sepolia": { chainId: "0xa0c71fd", rpcUrl: "https://sepolia.blast.io" }, // Blast Sepolia Testnet
  "mantle": { chainId: "0x1388", rpcUrl: "https://rpc.mantle.xyz" }, // Mantle Mainnet
  "mantle sepolia": { chainId: "0x1389", rpcUrl: "https://rpc.sepolia.mantle.xyz" }, // Mantle Sepolia Testnet
  "sei": { chainId: "0x531", rpcUrl: "https://evm-rpc.sei-apis.com" }, // Sei Mainnet
  "sei testnet": { chainId: "0x15e25", rpcUrl: "https://evm-rpc-testnet.sei-apis.com" }, // Sei Testnet
  "rootstock": { chainId: "0x1e", rpcUrl: "https://public-node.rsk.co" }, // Rootstock Mainnet
  "rootstock testnet": { chainId: "0x1f", rpcUrl: "https://public-node.testnet.rsk.co" }, // Rootstock Testnet
  "holesky": { chainId: "0x4268", rpcUrl: "https://rpc.holesky.ethpandaops.io" }, // Holesky Testnet
};

// ERC20 ABI
const ERC20_ABI = [
  "function allowance(address owner, address spender) view returns (uint256)",
  "function approve(address spender, uint256 amount) returns (bool)",
];

// Create wallet instance from private key
function createWallet(provider) {
  if (!PRIVATE_KEY) {
    throw new Error("PRIVATE_KEY not set in environment variables");
  }
  return new ethers.Wallet(PRIVATE_KEY, provider);
}

// Get default wallet address from private key
const DEFAULT_WALLET_ADDRESS = PRIVATE_KEY ? new ethers.Wallet(PRIVATE_KEY).address : null;

async function main() {
  // Ensure PRIVATE_KEY and MORALIS_API_KEY are set
  if (!DEFAULT_WALLET_ADDRESS) {
    throw new Error("PRIVATE_KEY not set in environment variables");
  }
  if (!MORALIS_API_KEY) {
    throw new Error("MORALIS_API_KEY not set in environment variables");
  }

  // Create MCP Server
  const server = new McpServer({
    name: "TokenRevokeMcp", // Name corresponds to "token-revoke-mcp"
    version: "1.0.0",
    description: "Multi-chain ERC20 token allowance management",
  });

  server.tool(
    "getApprovals",
    "Fetches all ERC20 token approvals for a wallet on a specified chain",
    {
      chain: z.string().optional().default("ethereum").describe(`Blockchain network (e.g., ${Object.keys(SUPPORTED_CHAINS).join(", ")})`),
      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)"),
    },
    async ({ chain, walletAddress }) => {
      try {
        const selectedChain = SUPPORTED_CHAINS[chain.toLowerCase()];
        if (!selectedChain) {
          throw new Error(`Unsupported chain: ${chain}. Supported chains: ${Object.keys(SUPPORTED_CHAINS).join(", ")}`);
        }
        
        if(!walletAddress) {
          walletAddress = DEFAULT_WALLET_ADDRESS
        }

        // Make HTTP request to Moralis API using fetch
        const url = `${MORALIS_API_BASE_URL}/wallets/${walletAddress}/approvals?chain=${selectedChain.chainId}`;
        const response = await fetch(url, {
          method: 'GET',
          headers: {
            "X-API-Key": MORALIS_API_KEY,
            "Accept": "application/json",
          },
        });

        if (!response.ok) {
          const errorData = await response.json();
          throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        const allowances = data.result.map(approval => ({
          tokenAddress: approval.token.address,
          tokenSymbol: approval.token.symbol || "Unknown",
          balance: approval.token.current_balance_formatted || "0",
          usdPrice: approval.token.usd_price || "N/A",
          usdValueAtRisk: approval.token.usd_at_risk || "0",
          spenderAddress: approval.spender.address,
          approvedAmount: approval.value_formatted,
          transactionHash: approval.transaction_hash,
          timestamp: approval.block_timestamp
        }));

        return {
          content: [{
            type: "text",
            text: JSON.stringify(allowances, null, 2),
          }],
        };
      } catch (error) {
        return {
          content: [{
            type: "text",
            text: `Error fetching allowances: ${error.message}`,
          }],
          isError: true,
        };
      }
    }
  );

  // Tool 2: Revoke a specific token allowance
  server.tool(
    "revokeAllowance",
    "Revokes an ERC20 token allowance for a specific spender on a specified chain",
    {
      chain: z.string().optional().default("ethereum").describe(`Blockchain network (e.g., ${Object.keys(SUPPORTED_CHAINS).join(", ")})`),
      tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address").describe("Token contract address"),
      spenderAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address").describe("Spender address to revoke"),
    },
    async ({ chain, tokenAddress, spenderAddress }) => {
      try {
        const selectedChain = SUPPORTED_CHAINS[chain.toLowerCase()];
        if (!selectedChain) {
          throw new Error(`Unsupported chain: ${chain}. Supported chains: ${Object.keys(SUPPORTED_CHAINS).join(", ")}`);
        }

        const provider = new ethers.JsonRpcProvider(selectedChain.rpcUrl);
        const wallet = createWallet(provider);
        const signedContract = new ethers.Contract(tokenAddress, ERC20_ABI, wallet);

        const tx = await signedContract.approve(spenderAddress, 0);

        return {
          content: [{
            type: "text",
            text: `Allowance revocation submitted on ${chain}. Transaction hash: ${tx.hash}. Note: Transaction is not yet confirmed.`,
          }],
        };
      } catch (error) {
        return {
          content: [{
            type: "text",
            text: `Error revoking allowance: ${error.message}`,
          }],
          isError: true,
        };
      }
    }
  );

server.tool(
    "checkTransactionStatus",
    "Checks the status of a transaction on a specified chain",
    {
      chain: z.string().optional().default("ethereum").describe(`Blockchain network (e.g., ${Object.keys(SUPPORTED_CHAINS).join(", ")})`),
      txHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").describe("Transaction hash to check"),
    },
    async ({ chain, txHash }) => {
      try {
        const selectedChain = SUPPORTED_CHAINS[chain.toLowerCase()];
        if (!selectedChain) {
          throw new Error(`Unsupported chain: ${chain}. Supported chains: ${Object.keys(SUPPORTED_CHAINS).join(", ")}`);
        }

        const provider = new ethers.JsonRpcProvider(selectedChain.rpcUrl);
        const receipt = await provider.getTransactionReceipt(txHash);

        if (!receipt) {
          return {
            content: [{
              type: "text",
              text: `Transaction ${txHash} on ${chain} is still pending or not found.`,
            }],
          };
        }

        const status = receipt.status === 1 ? "successful" : "failed";
        return {
          content: [{
            type: "text",
            text: `Transaction ${txHash} on ${chain} has completed with status: ${status}. Block number: ${receipt.blockNumber}.`,
          }],
        };
      } catch (error) {
        return {
          content: [{
            type: "text",
            text: `Error checking transaction status: ${error.message}`,
          }],
          isError: true,
        };
      }
    }
  );
  
  // Connect to Stdio transport
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);


```