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);