Skip to main content
Glama

ChainFETCH MCP Server

Official
by chainfetch
index.js•31.7 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import express from 'express'; const API_BASE_URL = 'https://www.chainfetch.app'; class ChainFetchServer { constructor() { this.server = new Server( { name: 'chainfetch-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } setupToolHandlers() { this.currentToken = null; // Store token for current request context this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Address endpoints { name: 'search_addresses_semantic', description: 'Semantic search for Ethereum addresses using AI-powered vector similarity matching', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to search for', }, limit: { type: 'integer', description: 'The number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'search_addresses_json', description: 'JSON search for addresses with 150+ parameters for comprehensive filtering', inputSchema: { type: 'object', properties: { eth_balance_min: { type: 'string', description: 'Minimum ETH balance (in ETH, e.g., "1.5")', }, eth_balance_max: { type: 'string', description: 'Maximum ETH balance (in ETH, e.g., "10.0")', }, is_contract: { type: 'boolean', description: 'Whether the address is a contract', }, is_verified: { type: 'boolean', description: 'Whether the address is verified', }, has_token_transfers: { type: 'boolean', description: 'Whether the address has token transfers', }, transactions_count_min: { type: 'integer', description: 'Minimum transactions count', }, transactions_count_max: { type: 'integer', description: 'Maximum transactions count', }, limit: { type: 'integer', description: 'Number of results to return (default: 10, max: 50)', default: 10, }, offset: { type: 'integer', description: 'Number of results to skip for pagination (default: 0)', default: 0, }, }, required: [], }, }, { name: 'search_addresses_llm', description: 'LLM-powered address search using LLaMA 3.2 3B to intelligently select from 150+ parameters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query for address search', }, }, required: ['query'], }, }, { name: 'get_address_summary', description: 'Get AI-generated summary for a specific address', inputSchema: { type: 'object', properties: { address_hash: { type: 'string', description: 'The address hash to get summary for', }, }, required: ['address_hash'], }, }, { name: 'get_address_info', description: 'Get detailed information about a specific Ethereum address', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'The address hash to get info for', }, }, required: ['address'], }, }, // Transaction endpoints { name: 'search_transactions_semantic', description: 'Semantic search for transactions using AI-powered vector similarity matching', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to search for', }, limit: { type: 'integer', description: 'The number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'search_transactions_json', description: 'JSON search for transactions with 254+ carefully curated parameters', inputSchema: { type: 'object', properties: { hash: { type: 'string', description: 'Transaction hash', }, value_min: { type: 'string', description: 'Minimum transaction value in WEI', }, value_max: { type: 'string', description: 'Maximum transaction value in WEI', }, gas_used_min: { type: 'string', description: 'Minimum gas used', }, gas_used_max: { type: 'string', description: 'Maximum gas used', }, from_hash: { type: 'string', description: 'From address hash', }, to_hash: { type: 'string', description: 'To address hash', }, block_number_min: { type: 'integer', description: 'Minimum block number', }, block_number_max: { type: 'integer', description: 'Maximum block number', }, limit: { type: 'integer', description: 'Number of results to return (default: 10, max: 50)', default: 10, }, offset: { type: 'integer', description: 'Number of results to skip for pagination (default: 0)', default: 0, }, }, required: [], }, }, { name: 'search_transactions_llm', description: 'LLM-powered transaction search using LLaMA 3.2 3B to select from 254 parameters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query for transaction search', }, }, required: ['query'], }, }, { name: 'get_transaction_summary', description: 'Get AI-generated summary for a specific transaction', inputSchema: { type: 'object', properties: { transaction_hash: { type: 'string', description: 'The transaction hash to get summary for', }, }, required: ['transaction_hash'], }, }, { name: 'get_transaction_info', description: 'Get detailed information about a specific transaction', inputSchema: { type: 'object', properties: { transaction: { type: 'string', description: 'The transaction hash to get info for', }, }, required: ['transaction'], }, }, // Block endpoints { name: 'search_blocks_semantic', description: 'Semantic search for blocks using AI-powered vector similarity matching', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to search for', }, limit: { type: 'integer', description: 'The number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'search_blocks_json', description: 'JSON search for blocks with 120+ parameters', inputSchema: { type: 'object', properties: { hash: { type: 'string', description: 'Block hash', }, height_min: { type: 'integer', description: 'Minimum block height', }, height_max: { type: 'integer', description: 'Maximum block height', }, gas_used_min: { type: 'string', description: 'Minimum gas used', }, gas_used_max: { type: 'string', description: 'Maximum gas used', }, transaction_count_min: { type: 'integer', description: 'Minimum transaction count', }, transaction_count_max: { type: 'integer', description: 'Maximum transaction count', }, miner_hash: { type: 'string', description: 'Miner address hash', }, limit: { type: 'integer', description: 'Number of results to return (default: 10, max: 50)', default: 10, }, offset: { type: 'integer', description: 'Number of results to skip for pagination (default: 0)', default: 0, }, }, required: [], }, }, { name: 'search_blocks_llm', description: 'LLM-powered block search using LLaMA 3.2 3B to select from 120+ parameters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query for block search', }, }, required: ['query'], }, }, { name: 'get_block_summary', description: 'Get AI-generated summary for a specific block', inputSchema: { type: 'object', properties: { block_number: { type: 'string', description: 'The block number to get summary for', }, }, required: ['block_number'], }, }, { name: 'get_block_info', description: 'Get detailed information about a specific block', inputSchema: { type: 'object', properties: { block: { type: 'string', description: 'The block number to get info for', }, }, required: ['block'], }, }, // Token endpoints { name: 'search_tokens_semantic', description: 'Semantic search for tokens using AI-powered vector similarity matching', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to search for', }, limit: { type: 'integer', description: 'The number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'search_tokens_json', description: 'JSON search for tokens with comprehensive search parameters', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Token name', }, symbol: { type: 'string', description: 'Token symbol', }, address: { type: 'string', description: 'Token contract address', }, type: { type: 'string', description: 'Token type (ERC-20, ERC-721, ERC-1155)', }, holders_count_min: { type: 'integer', description: 'Minimum holder count', }, holders_count_max: { type: 'integer', description: 'Maximum holder count', }, limit: { type: 'integer', description: 'Number of results to return (default: 10, max: 50)', default: 10, }, offset: { type: 'integer', description: 'Number of results to skip for pagination (default: 0)', default: 0, }, }, required: [], }, }, { name: 'search_tokens_llm', description: 'LLM-powered token search using AI to select optimal parameters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query for token search', }, }, required: ['query'], }, }, { name: 'get_token_summary', description: 'Get AI-generated summary for a specific token', inputSchema: { type: 'object', properties: { token_address: { type: 'string', description: 'The token address hash to get summary for', }, }, required: ['token_address'], }, }, { name: 'get_token_info', description: 'Get detailed information about a specific token', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'The token address hash', }, }, required: ['token'], }, }, { name: 'get_nft_instance_info', description: 'Get NFT instance information', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'The token address', }, instance_id: { type: 'string', description: 'The instance ID', }, }, required: ['token', 'instance_id'], }, }, // Smart Contract endpoints { name: 'search_smart_contracts_semantic', description: 'Semantic search for smart contracts using AI-powered vector similarity matching', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to search for', }, limit: { type: 'integer', description: 'The number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'search_smart_contracts_json', description: 'JSON search for smart contracts with 50+ parameters', inputSchema: { type: 'object', properties: { is_verified: { type: 'boolean', description: 'Whether the contract is verified', }, name: { type: 'string', description: 'Contract name', }, language: { type: 'string', description: 'Programming language (e.g., "solidity")', }, proxy_type: { type: 'string', description: 'Proxy type (e.g., "eip1967")', }, optimization_enabled: { type: 'boolean', description: 'Whether optimization is enabled', }, limit: { type: 'integer', description: 'Number of results to return (default: 10, max: 50)', default: 10, }, offset: { type: 'integer', description: 'Number of results to skip for pagination (default: 0)', default: 0, }, }, required: [], }, }, { name: 'search_smart_contracts_llm', description: 'LLM-powered smart contract search using AI to select from 150+ parameters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query for smart contract search', }, }, required: ['query'], }, }, { name: 'get_smart_contract_summary', description: 'Get AI-generated summary for a specific smart contract', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'The smart contract address to get summary for', }, }, required: ['address'], }, }, { name: 'get_smart_contract_info', description: 'Get detailed information about a specific smart contract', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'The smart contract address', }, }, required: ['address'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const result = await this.handleToolCall(name, args, this.currentToken); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error.message}` ); } }); } async makeRequest(endpoint, method = 'GET', params = {}, body = null, token = null) { const chainfetchToken = token || process.env.CHAINFETCH_API_TOKEN; if (!chainfetchToken) { throw new McpError( ErrorCode.InvalidRequest, 'CHAINFETCH_API_TOKEN is required' ); } const url = new URL(`${API_BASE_URL}${endpoint}`); // Add query parameters for GET requests if (method === 'GET' && Object.keys(params).length > 0) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach(v => url.searchParams.append(`${key}[]`, v)); } else { url.searchParams.append(key, value.toString()); } } }); } const fetchOptions = { method, headers: { 'Authorization': `Bearer ${chainfetchToken}`, 'Content-Type': 'application/json', }, }; if (body && method !== 'GET') { fetchOptions.body = JSON.stringify(body); } const response = await fetch(url.toString(), fetchOptions); if (!response.ok) { const errorText = await response.text(); throw new McpError( ErrorCode.InternalError, `API request failed: ${response.status} ${response.statusText} - ${errorText}` ); } return await response.json(); } async handleToolCall(name, args, token = null) { switch (name) { // Address endpoints case 'search_addresses_semantic': return await this.makeRequest('/api/v1/ethereum/addresses/semantic_search', 'GET', args, null, token); case 'search_addresses_json': return await this.makeRequest('/api/v1/ethereum/addresses/json_search', 'GET', args, null, token); case 'search_addresses_llm': return await this.makeRequest('/api/v1/ethereum/addresses/llm_search', 'GET', args, null, token); case 'get_address_summary': return await this.makeRequest('/api/v1/ethereum/addresses/summary', 'GET', args, null, token); case 'get_address_info': const { address } = args; return await this.makeRequest(`/api/v1/ethereum/addresses/${address}`, 'GET', {}, null, token); // Transaction endpoints case 'search_transactions_semantic': return await this.makeRequest('/api/v1/ethereum/transactions/semantic_search', 'GET', args, null, token); case 'search_transactions_json': return await this.makeRequest('/api/v1/ethereum/transactions/json_search', 'GET', args, null, token); case 'search_transactions_llm': return await this.makeRequest('/api/v1/ethereum/transactions/llm_search', 'GET', args, null, token); case 'get_transaction_summary': return await this.makeRequest('/api/v1/ethereum/transactions/summary', 'GET', args, null, token); case 'get_transaction_info': const { transaction } = args; return await this.makeRequest(`/api/v1/ethereum/transactions/${transaction}`, 'GET', {}, null, token); // Block endpoints case 'search_blocks_semantic': return await this.makeRequest('/api/v1/ethereum/blocks/semantic_search', 'GET', args, null, token); case 'search_blocks_json': return await this.makeRequest('/api/v1/ethereum/blocks/json_search', 'GET', args, null, token); case 'search_blocks_llm': return await this.makeRequest('/api/v1/ethereum/blocks/llm_search', 'GET', args, null, token); case 'get_block_summary': return await this.makeRequest('/api/v1/ethereum/blocks/summary', 'GET', args, null, token); case 'get_block_info': const { block } = args; return await this.makeRequest(`/api/v1/ethereum/blocks/${block}`, 'GET', {}, null, token); // Token endpoints case 'search_tokens_semantic': return await this.makeRequest('/api/v1/ethereum/tokens/semantic_search', 'GET', args, null, token); case 'search_tokens_json': return await this.makeRequest('/api/v1/ethereum/tokens/json_search', 'GET', args, null, token); case 'search_tokens_llm': return await this.makeRequest('/api/v1/ethereum/tokens/llm_search', 'GET', args, null, token); case 'get_token_summary': return await this.makeRequest('/api/v1/ethereum/tokens/summary', 'GET', args, null, token); case 'get_token_info': const { token: tokenAddress } = args; return await this.makeRequest(`/api/v1/ethereum/tokens/${tokenAddress}`, 'GET', {}, null, token); case 'get_nft_instance_info': const { token: nftToken, instance_id } = args; return await this.makeRequest(`/api/v1/ethereum/token-instances/${nftToken}/${instance_id}`, 'GET', {}, null, token); // Smart Contract endpoints case 'search_smart_contracts_semantic': return await this.makeRequest('/api/v1/ethereum/smart-contracts/semantic_search', 'GET', args, null, token); case 'search_smart_contracts_json': return await this.makeRequest('/api/v1/ethereum/smart-contracts/json_search', 'GET', args, null, token); case 'search_smart_contracts_llm': return await this.makeRequest('/api/v1/ethereum/smart-contracts/llm_search', 'GET', args, null, token); case 'get_smart_contract_summary': return await this.makeRequest('/api/v1/ethereum/smart-contracts/summary', 'GET', args, null, token); case 'get_smart_contract_info': const { address: contractAddress } = args; return await this.makeRequest(`/api/v1/ethereum/smart-contracts/${contractAddress}`, 'GET', {}, null, token); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } async run() { // Check if we should run as HTTP server (for ngrok) or stdio const useHttp = process.env.MCP_HTTP_MODE === 'true'; if (useHttp) { // HTTP mode for ngrok const port = process.env.PORT || 8000; console.log('Starting HTTP server for ngrok...'); console.log(`Port: ${port}`); const app = express(); app.use(express.json()); // Map to store transports by session ID const transports = {}; // SSE endpoint for Claude MCP Connector app.all('/streamable-http', async (req, res) => { try { console.log(`Received ${req.method} MCP request from Claude via ngrok`); // Extract CHAINFETCH_API_TOKEN from Authorization header const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized - Bearer token required' }); } const token = authHeader.substring(7); // Remove 'Bearer ' prefix this.currentToken = token; // Set token for this request // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && this.isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => Math.random().toString(36).substring(2, 15), }); // Connect to the MCP server await this.server.connect(transport); // Handle the request first, then store the transport await transport.handleRequest(req, res, req.body); // Store the transport by session ID after handling the request if (transport.sessionId) { transports[transport.sessionId] = transport; console.log(`āœ… New session created and stored: ${transport.sessionId}`); } return; // Return early as the request has been handled } else { return res.status(400).json({ error: 'Invalid request - missing session or not an initialize request' }); } // For existing sessions, handle the request await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); return res.status(500).json({ error: 'Internal server error' }); } }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', server: 'chainfetch-mcp-server', version: '1.0.0', transport: 'StreamableHTTP', protocol: 'http', port: port, note: 'Use ngrok for HTTPS tunneling', endpoint: '/streamable-http - StreamableHTTP transport for Claude MCP Connector' }); }); // Root endpoint with info app.get('/', (req, res) => { res.json({ name: 'ChainFETCH MCP Server', version: '1.0.0', description: 'MCP server for ChainFETCH API with HTTP transport for ngrok tunneling', protocol: 'http', port: port, endpoints: { streamableHttp: `/streamable-http - StreamableHTTP transport for Claude MCP Connector`, health: `/health - Health check` }, usage: 'Use ngrok to create HTTPS tunnel, then connect Claude to the ngrok URL + /streamable-http', note: 'Start with: ngrok http ' + port }); }); app.listen(port, () => { console.log(`\nāœ… HTTP server listening on port ${port}`); console.log(`šŸš‡ Ready for ngrok tunneling`); console.log(`šŸ’” Start ngrok with: ngrok http ${port}`); console.log(`StreamableHTTP endpoint: http://localhost:${port}/streamable-http`); console.log(`Health check: http://localhost:${port}/health`); console.log('\nReady for Claude MCP Connector integration via ngrok\n'); }); } else { // Stdio mode (default) - for Claude Desktop const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('ChainFETCH MCP server running on stdio'); } } // Helper method to check if request is an initialize request isInitializeRequest(body) { if (Array.isArray(body)) { return body.some(request => request.method === 'initialize'); } return body && body.method === 'initialize'; } } const server = new ChainFetchServer(); server.run().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chainfetch/chainfetch-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server