UNPKG

atom-marketplace-mcp-server

Version:

MCP server for Atom marketplace APIs - domain search, availability, trademarks, and appraisals

696 lines (654 loc) 26.4 kB
import { AtomMarketplaceMCPServer } from '../server.js'; // Simple in-memory rate limiting const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute const RATE_LIMIT_MAX_REQUESTS = 100; // 100 requests per minute function checkRateLimit(ip) { const now = Date.now(); const windowStart = now - RATE_LIMIT_WINDOW; if (!rateLimitStore.has(ip)) { rateLimitStore.set(ip, []); } const requests = rateLimitStore.get(ip); const recentRequests = requests.filter(timestamp => timestamp > windowStart); if (recentRequests.length >= RATE_LIMIT_MAX_REQUESTS) { return false; } recentRequests.push(now); rateLimitStore.set(ip, recentRequests); return true; } function getClientIP(req) { return req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.connection?.remoteAddress || 'unknown'; } // Vercel serverless function handler for ChatGPT Custom GPTs export default async (req, res) => { const clientIP = getClientIP(req); // Validate API key - support both ChatGPT Custom GPT and direct API calls // GET requests (schema discovery) don't require authentication // POST requests (actual API calls) require authentication const providedKey = req.headers['x-mcp-api-key'] || req.headers['X-MCP-API-Key'] || req.headers['authorization']?.replace('Bearer ', '') || req.headers['Authorization']?.replace('Bearer ', ''); // Only require API key for POST requests (actual API calls) if (req.method === 'POST' && !providedKey) { res.status(401).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32600, message: 'Missing API key. Please configure API Key authentication in your Custom GPT or provide X-MCP-API-Key header.' } }); return; } // Enhanced key validation with partner support (only for POST requests) if (req.method === 'POST') { const validKeys = process.env.VALID_API_KEYS ? process.env.VALID_API_KEYS.split(',').map(key => key.trim()) : []; // If no whitelist is configured, fall back to basic validation if (validKeys.length === 0) { if (providedKey.length < 8) { res.status(401).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32600, message: 'Invalid API key. Key must be at least 8 characters.' } }); return; } } else { // Validate against whitelist if (!validKeys.includes(providedKey)) { res.status(401).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32600, message: 'Invalid API key. Key not found in authorized list.' } }); return; } } } // Rate limiting if (!checkRateLimit(clientIP)) { res.status(429).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32600, message: 'Rate limit exceeded. Please try again later.' } }); return; } // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-MCP-API-Key'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); // Handle preflight requests if (req.method === 'OPTIONS') { res.status(200).end(); return; } // Handle GET requests for schema discovery (ChatGPT compatibility) if (req.method === 'GET') { try { // Simple health check first console.log('GET request received - checking environment variables...'); console.log('ATOM_BASE_URL:', process.env.ATOM_BASE_URL ? 'SET' : 'NOT SET'); console.log('ATOM_API_TOKEN:', process.env.ATOM_API_TOKEN ? 'SET' : 'NOT SET'); console.log('ATOM_USER_ID:', process.env.ATOM_USER_ID ? 'SET' : 'NOT SET'); const mcpServer = new AtomMarketplaceMCPServer(); const tools = await mcpServer.getTools(); res.json({ openapi: '3.1.0', info: { title: 'Atom Marketplace Domain Services', description: 'Domain search, availability, appraisals, and trademark services', version: '1.0.0' }, servers: [ { url: 'https://mcp.atom.com', description: 'Production server' } ], paths: { '/semantic-search': { post: { operationId: 'semanticSearchDomains', summary: 'AI-Powered Semantic Domain Search', description: 'Search for domain names using AI-powered semantic search with embeddings and Pinecone', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language search query (e.g., "tech startup names", "healthcare companies")', example: 'tech startup names' }, filters: { type: 'object', description: 'Optional filters to apply to the search', properties: { extensions: { type: 'array', items: { type: 'string' }, description: 'Domain extensions to filter by', example: ['.com', '.io'] }, min_price: { type: 'number', description: 'Minimum price filter', example: 1000 }, max_price: { type: 'number', description: 'Maximum price filter', example: 10000 }, type_of_name: { type: 'array', items: { type: 'string' }, description: 'Type of domain name', example: ['One Word', 'Two Words'] }, emotions: { type: 'array', items: { type: 'string', enum: ['Innovation', 'Excitement', 'Trust', 'Efficiency', 'Creativity', 'Adventure', 'Elegance', 'Empowerment', 'Luxury', 'Confidence', 'Convenience', 'Connection', 'Professionalism', 'Playfulness', 'Trustworthiness', 'Curiosity', 'Community', 'Reliability', 'Strength', 'Joy', 'Security', 'Simplicity', 'Inspiration', 'Exploration', 'Sophistication', 'Comfort', 'Growth', 'Wellness', 'Relaxation', 'Unity'] }, description: 'Array of emotions to filter by (e.g., Innovation, Trust, Elegance, Confidence)', example: ['Innovation', 'Trust', 'Efficiency'] } } }, page: { type: 'integer', description: 'Page number for pagination', default: 1, minimum: 1, example: 1 }, page_size: { type: 'integer', description: 'Number of results per page', default: 20, minimum: 1, maximum: 100, example: 20 } }, required: ['query'] } } } }, responses: { '200': { description: 'Successful search results', content: { 'application/json': { schema: { type: 'object', properties: { success: { type: 'boolean' }, query: { type: 'string' }, page: { type: 'integer' }, page_size: { type: 'integer' }, total: { type: 'integer' }, total_pages: { type: 'integer' }, processing_time_ms: { type: 'number' }, domains: { type: 'array', items: { type: 'object', properties: { domain_name: { type: 'string' }, tld: { type: 'string' }, full_domain: { type: 'string' }, selling_price: { type: 'number' }, primary_category: { type: 'string' }, status: { type: 'string' }, description: { type: 'string' }, created_at: { type: 'string' }, owner_name: { type: 'string' }, semantic_score: { type: 'number' }, rerank_score: { type: 'number' }, logo_url: { type: 'string', format: 'uri', description: 'Direct HTTPS URL to the logo image' }, metadata: { type: 'object' } } } } } } } } } } } }, '/availability': { post: { operationId: 'checkDomainAvailability', summary: 'Check Domain Availability', description: 'Check if domain names are available for registration or for sale on Atom.com marketplace', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { domains: { type: 'array', description: 'Array of domain objects to check', items: { type: 'object', properties: { domain_name: { type: 'string', description: 'Domain name without TLD' }, tld: { type: 'string', description: 'Top-level domain (e.g., com, net, io)', default: 'com' } }, required: ['domain_name'] }, maxItems: 50 } }, required: ['domains'] } } } }, responses: { '200': { description: 'Availability results', content: { 'application/json': { schema: { type: 'object', properties: { success: { type: 'boolean' }, results: { type: 'array', items: { type: 'object', properties: { domain_name: { type: 'string' }, tld: { type: 'string' }, full_domain: { type: 'string' }, available: { type: 'boolean' }, definitive: { type: 'boolean' }, price: { type: 'number' }, currency: { type: 'string' }, marketplace_available: { type: 'boolean' }, marketplace_price: { type: 'number' }, marketplace_data: { type: 'object' }, registrar: { type: 'string' }, error: { type: 'string' } } } }, processing_time_ms: { type: 'number' } } } } } } } } }, '/domain-appraisal': { post: { operationId: 'getDomainAppraisal', summary: 'Get Domain Appraisal', description: 'Get an AI-powered appraisal for a domain name', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { domain_name: { type: 'string', description: 'Domain name to appraise (with or without TLD)', example: 'example.com' } }, required: ['domain_name'] } } } }, responses: { '200': { description: 'Domain appraisal results', content: { 'application/json': { schema: { type: 'object', properties: { success: { type: 'boolean' }, domain_name: { type: 'string' }, appraisal: { type: 'object', properties: { estimated_value: { type: 'string' }, domain_score: { type: 'number' }, positive_signals: { type: 'array' }, negative_signals: { type: 'array' }, root_words: { type: 'array' }, tld_taken_count: { type: 'number' }, tm_conflicts: { type: 'number' }, date_registered: { type: 'string' }, external_message: { type: 'string' }, appraisal_token: { type: 'string' }, is_listed: { type: 'boolean' }, bin_price: { type: 'number' } } } } } } } } } } }, '/trademark-search': { post: { operationId: 'searchTrademarks', summary: 'Search Trademarks', description: 'Search for existing trademarks that might conflict with a domain name or brand', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { query: { type: 'string', description: 'Trademark search query (domain name or brand name)', example: 'example' }, statuses: { type: 'array', description: 'Trademark statuses to search for', items: { type: 'string', enum: ['Registered', 'Pending', 'Abandoned', 'Cancelled'] }, default: ['Registered', 'Pending'] }, classifications: { type: 'array', description: 'Trademark classifications to filter by (array format)', items: { type: 'string' }, default: [] }, class: { type: 'string', description: 'Single trademark class to filter by (1-45, e.g., "9" for computer software, "35" for business services)', example: '9' }, mode: { type: 'string', description: 'Search mode', enum: ['phrase', 'fuzzy', 'exact'], default: 'phrase' }, page: { type: 'integer', description: 'Page number for pagination', default: 1, minimum: 1 }, page_size: { type: 'integer', description: 'Number of results per page', default: 20, minimum: 1, maximum: 100 } }, required: ['query'] } } } }, responses: { '200': { description: 'Trademark search results', content: { 'application/json': { schema: { type: 'object', properties: { success: { type: 'boolean' }, query: { type: 'string' }, total: { type: 'integer' }, page: { type: 'integer' }, page_size: { type: 'integer' }, total_pages: { type: 'integer' }, trademarks: { type: 'array', items: { type: 'object', properties: { mark_text: { type: 'string' }, status: { type: 'string' }, registration_number: { type: 'string' }, filing_date: { type: 'string' }, registration_date: { type: 'string' }, owner_name: { type: 'string' }, classifications: { type: 'array' }, goods_services: { type: 'string' }, attorney_name: { type: 'string' }, similarity_score: { type: 'number' } } } } } } } } } } } } } }); return; } catch (error) { console.error('Schema discovery error:', error); res.status(500).json({ error: 'Failed to load schema', message: 'Internal server error' }); return; } } // Validate request method if (req.method !== 'POST') { res.status(405).json({ error: 'Method not allowed', message: 'Only GET and POST requests are supported' }); return; } // Validate request body if (!req.body || typeof req.body !== 'object') { res.status(400).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32700, message: 'Invalid JSON-RPC request' } }); return; } try { // Debug: Log the complete request body first console.log('=== FULL REQUEST DEBUG ==='); console.log('Request method:', req.method); console.log('Request headers:', req.headers); console.log('Request body (raw):', req.body); console.log('Request body type:', typeof req.body); console.log('Request body keys:', Object.keys(req.body || {})); console.log('=== END REQUEST DEBUG ==='); // Debug: Log environment variables console.log('POST request received - checking environment variables...'); console.log('ATOM_BASE_URL:', process.env.ATOM_BASE_URL ? 'SET' : 'NOT SET'); console.log('ATOM_API_TOKEN:', process.env.ATOM_API_TOKEN ? 'SET' : 'NOT SET'); console.log('ATOM_USER_ID:', process.env.ATOM_USER_ID ? 'SET' : 'NOT SET'); // Check if we have the required environment variables if (!process.env.ATOM_BASE_URL || !process.env.ATOM_API_TOKEN || !process.env.ATOM_USER_ID) { console.error('Missing required environment variables'); res.status(500).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32603, message: 'Server configuration error - missing environment variables', data: { ATOM_BASE_URL: process.env.ATOM_BASE_URL ? 'SET' : 'NOT SET', ATOM_API_TOKEN: process.env.ATOM_API_TOKEN ? 'SET' : 'NOT SET', ATOM_USER_ID: process.env.ATOM_USER_ID ? 'SET' : 'NOT SET' } } }); return; } // Initialize MCP server console.log('Initializing MCP server...'); let mcpServer; try { mcpServer = new AtomMarketplaceMCPServer(); console.log('MCP server initialized successfully'); } catch (error) { console.error('MCP server initialization failed:', error); res.status(500).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32603, message: 'MCP server initialization failed', data: error.message } }); return; } // Handle REST endpoints instead of JSON-RPC const path = req.url; console.log('REST endpoint called:', path); console.log('Request body:', req.body); let result; try { if (path === '/semantic-search') { // Handle semantic search endpoint const { query, filters = {}, page = 1, page_size = 20 } = req.body; if (!query) { res.status(400).json({ error: 'Query parameter is required' }); return; } console.log('Calling semantic search with:', { query, filters, page, page_size }); result = await mcpServer.semanticSearchDomains(query, filters, page, page_size); } else if (path === '/availability') { // Handle domain availability endpoint const { domains } = req.body; if (!domains || !Array.isArray(domains)) { res.status(400).json({ error: 'Domains array is required' }); return; } console.log('Calling domain availability with:', { domains }); result = await mcpServer.handleRequest('check_domain_availability', { domains }); } else if (path === '/domain-appraisal') { // Handle domain appraisal endpoint const { domain_name } = req.body; if (!domain_name) { res.status(400).json({ error: 'Domain name is required' }); return; } console.log('Calling domain appraisal with:', { domain_name }); result = await mcpServer.getDomainAppraisal({ domain_name }); } else if (path === '/trademark-search') { // Handle trademark search endpoint const { query, statuses = ['Registered', 'Pending'], classifications = [], class: singleClass, mode = 'phrase', page = 1, page_size = 20 } = req.body; if (!query) { res.status(400).json({ error: 'Query parameter is required' }); return; } console.log('Calling trademark search with:', { query, statuses, classifications, class: singleClass, mode, page, page_size }); result = await mcpServer.searchTrademarks({ query, statuses, classifications, class: singleClass, mode, page, page_size }); } else { res.status(404).json({ error: 'Endpoint not found', availableEndpoints: ['/semantic-search', '/availability', '/domain-appraisal', '/trademark-search'] }); return; } // Return the result directly (no JSON-RPC wrapper) res.json(result); } catch (error) { console.error('REST endpoint error:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); return; } } catch (error) { console.error('Request processing error:', error); res.status(500).json({ jsonrpc: '2.0', id: req.body?.id, error: { code: -32603, message: 'Internal server error', data: error.message } }); return; } };