atom-marketplace-mcp-server
Version:
MCP server for Atom marketplace APIs - domain search, availability, trademarks, and appraisals
696 lines (654 loc) • 26.4 kB
JavaScript
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;
}
};