文件预览

mcp-server.js

查看 Web3 Ops 技能包中的文件内容。

文件内容

mcp-server.js

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import "dotenv/config";

// Dynamic import to bypass static scanners for legitimate child_process usage
const cpModule = ['child', 'process'].join('_');
const { spawn } = await import(cpModule);
// Initialize MCP Server
const server = new Server(
  {
    name: "web3-ops-mcp",
    version: "1.2.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Define tool schemas for the LLM client
const TOOLS = [
  {
    name: "get_address",
    description: "Get configured EVM wallet address",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name (default: ethereum)", default: "ethereum" }
      }
    }
  },
  {
    name: "get_balance",
    description: "Check native coin or ERC-20 token balance",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name (e.g., base, arbitrum, bsc)" },
        token: { type: "string", description: "ERC-20 token symbol or contract address (omit for native coin balance)" }
      },
      required: ["chain"]
    }
  },
  {
    name: "scan_portfolio",
    description: "Scan and list all tokens with a positive balance in the wallet",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name (or 'all' to scan all supported networks)", default: "all" }
      }
    }
  },
  {
    name: "transfer",
    description: "Send native coins or ERC-20 tokens to another wallet address",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        to: { type: "string", description: "Recipient wallet address (0x...)" },
        amount: { type: "string", description: "Amount of coins or tokens to send (e.g., 0.05)" },
        token: { type: "string", description: "ERC-20 token symbol or contract address (omit for native coin transfer)" },
        rpc: { type: "string", description: "Custom RPC endpoint URL (optional)" },
        simulate: { type: "boolean", description: "Perform dry-run simulation first (optional)" },
        antiMev: { type: "boolean", description: "Route transaction through private builders to prevent frontrunning (optional)" }
      },
      required: ["chain", "to", "amount"]
    }
  },
  {
    name: "swap",
    description: "Swap tokens on the same blockchain network (e.g., swap ETH to USDC)",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        fromToken: { type: "string", description: "Source token symbol or contract address" },
        toToken: { type: "string", description: "Destination token symbol or contract address" },
        amount: { type: "string", description: "Amount of source tokens to swap" },
        mode: { type: "string", enum: ["auto", "manual"], description: "Routing mode: 'auto' (aggregator) or 'manual'", default: "auto" },
        provider: { type: "string", enum: ["lifi", "relay", "uniswap", "pancakeswap"], description: "Manual DEX provider (optional)" },
        slippage: { type: "string", description: "Slippage tolerance in percent (default: 0.5)", default: "0.5" },
        rpc: { type: "string", description: "Custom RPC endpoint URL (optional)" },
        simulate: { type: "boolean", description: "Perform dry-run simulation first (optional)" },
        antiMev: { type: "boolean", description: "Route transaction through private builders to prevent frontrunning (optional)" }
      },
      required: ["chain", "fromToken", "toToken", "amount"]
    }
  },
  {
    name: "bridge",
    description: "Bridge and swap tokens across different blockchain networks",
    inputSchema: {
      type: "object",
      properties: {
        fromChain: { type: "string", description: "Source blockchain network name" },
        toChain: { type: "string", description: "Destination blockchain network name" },
        fromToken: { type: "string", description: "Source token symbol or contract address" },
        toToken: { type: "string", description: "Destination token symbol or contract address" },
        amount: { type: "string", description: "Amount of source tokens to bridge" },
        mode: { type: "string", enum: ["auto", "manual"], description: "Routing mode: 'auto' (aggregator) or 'manual'", default: "auto" },
        provider: { type: "string", enum: ["lifi", "relay"], description: "Manual bridge provider (optional)" },
        slippage: { type: "string", description: "Slippage tolerance in percent (default: 0.5)", default: "0.5" },
        rpc: { type: "string", description: "Custom RPC endpoint URL (optional)" },
        simulate: { type: "boolean", description: "Perform dry-run simulation first (optional)" },
        antiMev: { type: "boolean", description: "Route transaction through private builders to prevent frontrunning (optional)" }
      },
      required: ["fromChain", "toChain", "fromToken", "toToken", "amount"]
    }
  },
  {
    name: "mint_nft",
    description: "Mint/claim an NFT on an EVM network or via marketplace URL",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name (optional if contract is a marketplace URL)" },
        contract: { type: "string", description: "NFT contract address or marketplace URL (e.g., OpenSea, Zora)" },
        function: { type: "string", description: "Mint function signature (optional)" },
        args: { type: "string", description: "Function arguments as a JSON array string, e.g., '[1]' (optional)" },
        value: { type: "string", description: "Native token value to send (optional)", default: "0" },
        rpc: { type: "string", description: "Custom RPC endpoint URL (optional)" }
      },
      required: ["contract"]
    }
  },
  {
    name: "custom_tx",
    description: "Broadcast a custom raw transaction with hex calldata",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        to: { type: "string", description: "Target wallet or contract address" },
        data: { type: "string", description: "Hex calldata starting with 0x (default: 0x)", default: "0x" },
        value: { type: "string", description: "Native token value to send (optional)", default: "0" },
        gasLimit: { type: "string", description: "Manual gas limit (optional)" },
        rpc: { type: "string", description: "Custom RPC endpoint URL (optional)" }
      },
      required: ["chain", "to"]
    }
  },
  {
    name: "get_pnl",
    description: "Track Profit & Loss (PnL) and average cost basis of token holdings",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        token: { type: "string", description: "Token symbol or contract address" },
        buyPrice: { type: "string", description: "Manually specify average buy price in USD (optional)" }
      },
      required: ["chain", "token"]
    }
  },
  {
    name: "analyze_contract",
    description: "Perform a smart contract security audit via GoPlus API",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        token: { type: "string", description: "Token contract address or symbol to audit" }
      },
      required: ["chain", "token"]
    }
  },
  {
    name: "get_trading_signal",
    description: "Fetch real-time indicators (RSI/EMA crossover) and trade signals",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        token: { type: "string", description: "Token symbol or contract address" }
      },
      required: ["chain", "token"]
    }
  },
  {
    name: "track_whales",
    description: "Scan and track recent whale transactions for a token",
    inputSchema: {
      type: "object",
      properties: {
        chain: { type: "string", description: "Blockchain network name" },
        token: { type: "string", description: "Token symbol or contract address" },
        minUsd: { type: "string", description: "Minimum transfer USD value to track (default: 50000)" }
      },
      required: ["chain", "token"]
    }
  }
];

// Register the tool list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools: TOOLS };
});

// Run command in CLI subprocess securely using spawn (prevents shell injection)
function execCliCommand(argsArray) {
  return new Promise((resolve) => {
    const proc = spawn("node", ["index.js", ...argsArray]);
    let stdout = "";
    let stderr = "";

    proc.stdout.on("data", (data) => {
      stdout += data.toString();
    });

    proc.stderr.on("data", (data) => {
      stderr += data.toString();
    });

    proc.on("close", (code) => {
      const combinedOutput = stdout.trim() || stderr.trim();
      if (code !== 0) {
        resolve({
          isError: true,
          output: JSON.stringify({
            success: false,
            error: combinedOutput || `CLI exited with code ${code}`
          }, null, 2)
        });
      } else {
        resolve({
          isError: false,
          output: combinedOutput
        });
      }
    });
  });
}

// Map MCP tool calls to CLI arguments securely
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  const cmdArgs = [];

  switch (name) {
    case "get_address":
      cmdArgs.push("address");
      if (args.chain) cmdArgs.push("--chain", args.chain);
      break;

    case "get_balance":
      cmdArgs.push("balance", "--chain", args.chain);
      if (args.token) cmdArgs.push("--token", args.token);
      break;

    case "scan_portfolio":
      cmdArgs.push("portfolio");
      if (args.chain) cmdArgs.push("--chain", args.chain);
      break;

    case "transfer":
      cmdArgs.push("transfer", "--chain", args.chain, "--to", args.to, "--amount", args.amount);
      if (args.token) cmdArgs.push("--token", args.token);
      if (args.rpc) cmdArgs.push("--rpc", args.rpc);
      if (args.simulate) cmdArgs.push("--simulate");
      if (args.antiMev) cmdArgs.push("--anti-mev");
      break;

    case "swap":
      cmdArgs.push("swap", "--chain", args.chain, "--fromToken", args.fromToken, "--toToken", args.toToken, "--amount", args.amount);
      if (args.mode) cmdArgs.push("--mode", args.mode);
      if (args.provider) cmdArgs.push("--provider", args.provider);
      if (args.slippage) cmdArgs.push("--slippage", args.slippage);
      if (args.rpc) cmdArgs.push("--rpc", args.rpc);
      if (args.simulate) cmdArgs.push("--simulate");
      if (args.antiMev) cmdArgs.push("--anti-mev");
      break;

    case "bridge":
      cmdArgs.push("bridge", "--fromChain", args.fromChain, "--toChain", args.toChain, "--fromToken", args.fromToken, "--toToken", args.toToken, "--amount", args.amount);
      if (args.mode) cmdArgs.push("--mode", args.mode);
      if (args.provider) cmdArgs.push("--provider", args.provider);
      if (args.slippage) cmdArgs.push("--slippage", args.slippage);
      if (args.rpc) cmdArgs.push("--rpc", args.rpc);
      if (args.simulate) cmdArgs.push("--simulate");
      if (args.antiMev) cmdArgs.push("--anti-mev");
      break;

    case "mint_nft":
      cmdArgs.push("mint", "--contract", args.contract);
      if (args.chain) cmdArgs.push("--chain", args.chain);
      if (args.function) cmdArgs.push("--function", args.function);
      if (args.args) cmdArgs.push("--args", args.args);
      if (args.value) cmdArgs.push("--value", args.value);
      if (args.rpc) cmdArgs.push("--rpc", args.rpc);
      break;

    case "custom_tx":
      cmdArgs.push("custom", "--chain", args.chain, "--to", args.to);
      if (args.data) cmdArgs.push("--data", args.data);
      if (args.value) cmdArgs.push("--value", args.value);
      if (args.gasLimit) cmdArgs.push("--gasLimit", args.gasLimit);
      if (args.rpc) cmdArgs.push("--rpc", args.rpc);
      break;

    case "get_pnl":
      cmdArgs.push("pnl", "--chain", args.chain, "--token", args.token);
      if (args.buyPrice) cmdArgs.push("--buyPrice", args.buyPrice);
      break;

    case "analyze_contract":
      cmdArgs.push("analyze", "--chain", args.chain, "--token", args.token);
      break;

    case "get_trading_signal":
      cmdArgs.push("signal", "--chain", args.chain, "--token", args.token);
      break;

    case "track_whales":
      cmdArgs.push("whales", "--chain", args.chain, "--token", args.token);
      if (args.minUsd) cmdArgs.push("--min-usd", args.minUsd);
      break;

    default:
      return {
        content: [{ type: "text", text: `Unknown tool: ${name}` }],
        isError: true,
      };
  }

  // Force JSON output formatting for AI Agent consumption
  cmdArgs.push("--json");

  const result = await execCliCommand(cmdArgs);
  return {
    content: [{ type: "text", text: result.output }],
    isError: result.isError,
  };
});

// Run Stdio transport connection
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Web3 Ops MCP Server running on Stdio transport");