Skip to main content
Glama

Keywords Everywhere MCP Server

index.js27.3 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import express from "express"; import axios from "axios"; import { z } from "zod"; import dotenv from "dotenv"; import http from "http"; // Load environment variables from .env file dotenv.config(); const BASE_URL = "https://api.keywordseverywhere.com/v1"; const API_KEY = process.env.KEYWORDS_EVERYWHERE_API_KEY; if (!API_KEY) { console.error("Error: KEYWORDS_EVERYWHERE_API_KEY environment variable is required"); process.exit(1); } const TOOLS = { // Account get_credits: { name: "get_credits", description: "Get your account's credit balance", inputSchema: { type: "object", properties: {} } }, // Countries and Currencies get_countries: { name: "get_countries", description: "Get list of supported countries", inputSchema: { type: "object", properties: {} } }, get_currencies: { name: "get_currencies", description: "Get list of supported currencies", inputSchema: { type: "object", properties: {} } }, // Keyword Data get_keyword_data: { name: "get_keyword_data", description: "Get Volume, CPC and competition for a set of keywords", inputSchema: { type: "object", properties: { keywords: { type: "array", items: { type: "string" }, description: "List of keywords to analyze" }, country: { type: "string", description: "Country code (empty string for Global, 'us' for United States, etc.)", default: "" }, currency: { type: "string", description: "Currency code (e.g., 'myr' for Malaysian Ringgit)", default: "myr" } }, required: ["keywords"] } }, // Related Keywords get_related_keywords: { name: "get_related_keywords", description: "Get related keywords based on a seed keyword", inputSchema: { type: "object", properties: { keyword: { type: "string", description: "Seed keyword to find related terms for" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["keyword"] } }, // People Also Search For get_pasf_keywords: { name: "get_pasf_keywords", description: "Get 'People Also Search For' keywords based on a seed keyword", inputSchema: { type: "object", properties: { keyword: { type: "string", description: "Seed keyword to find PASF terms for" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["keyword"] } }, // Domain Keywords get_domain_keywords: { name: "get_domain_keywords", description: "Get keywords that a domain ranks for", inputSchema: { type: "object", properties: { domain: { type: "string", description: "Domain to analyze (e.g., example.com)" }, country: { type: "string", description: "Country code (empty string for Global, 'us' for United States, etc.)", default: "" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["domain"] } }, // URL Keywords get_url_keywords: { name: "get_url_keywords", description: "Get keywords that a URL ranks for", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to analyze" }, country: { type: "string", description: "Country code (empty string for Global, 'us' for United States, etc.)", default: "" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["url"] } }, // Traffic Metrics get_domain_traffic: { name: "get_domain_traffic", description: "Get traffic metrics for a domain", inputSchema: { type: "object", properties: { domain: { type: "string", description: "Domain to analyze (e.g., example.com)" }, country: { type: "string", description: "Country code (empty string for Global, 'us' for United States, etc.)", default: "" } }, required: ["domain"] } }, get_url_traffic: { name: "get_url_traffic", description: "Get traffic metrics for a URL", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to analyze" }, country: { type: "string", description: "Country code (empty string for Global, 'us' for United States, etc.)", default: "" } }, required: ["url"] } }, // Backlinks get_domain_backlinks: { name: "get_domain_backlinks", description: "Get backlinks for a domain", inputSchema: { type: "object", properties: { domain: { type: "string", description: "Domain to analyze (e.g., example.com)" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["domain"] } }, get_unique_domain_backlinks: { name: "get_unique_domain_backlinks", description: "Get unique domain backlinks", inputSchema: { type: "object", properties: { domain: { type: "string", description: "Domain to analyze (e.g., example.com)" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["domain"] } }, get_page_backlinks: { name: "get_page_backlinks", description: "Get backlinks for a specific URL", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to analyze" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["url"] } }, get_unique_page_backlinks: { name: "get_unique_page_backlinks", description: "Get unique backlinks for a specific URL", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to analyze" }, num: { type: "integer", description: "Number of results to return (max 1000)", default: 10 } }, required: ["url"] } } }; const server = new McpServer({ name: "mcp-keywords-everywhere", version: "1.0.0" }); // Helper function for API calls async function makeApiCall(endpoint, data = null, retryCount = 0) { try { const url = `${BASE_URL}/${endpoint}`; console.error(`Calling Keywords Everywhere API: ${endpoint}`); const config = { method: data ? 'post' : 'get', url, headers: { "Authorization": `Bearer ${API_KEY}`, "Accept": "application/json" } }; // Handle different types of data if (data instanceof URLSearchParams) { config.data = data; config.headers['Content-Type'] = 'application/x-www-form-urlencoded'; } else if (data) { config.data = data; } const response = await axios(config); console.error(`API Response Status: ${response.status}`); return response.data; } catch (error) { // Extract detailed error information const statusCode = error.response?.status; const errorMessage = error.response?.data?.message || error.response?.data || error.message; // Log detailed error information console.error(`Error calling Keywords Everywhere API (${endpoint}):`, { statusCode, errorMessage, endpoint, data }); // Handle specific error codes if (statusCode === 400) { // For 400 Bad Request errors, provide more helpful error messages let enhancedMessage = `Bad Request (400): ${errorMessage}`; // Add specific guidance based on common 400 errors if (errorMessage.includes("credit") || errorMessage.includes("credits")) { enhancedMessage += ". You may need to add more credits to your Keywords Everywhere account."; } else if (errorMessage.includes("subscription") || errorMessage.includes("plan")) { enhancedMessage += ". This may be due to a subscription plan limitation. Please check your current plan."; } else if (errorMessage.includes("limit") || errorMessage.includes("rate")) { enhancedMessage += ". You may have hit a rate limit. Try again later."; } const customError = new Error(enhancedMessage); customError.statusCode = statusCode; customError.originalError = error; throw customError; } else if (statusCode === 401) { throw new Error(`Authentication failed (401): Please check your API key`); } else if (statusCode === 429) { // Rate limiting - implement retry with exponential backoff if (retryCount < 3) { const delay = Math.pow(2, retryCount) * 1000; // Exponential backoff: 1s, 2s, 4s console.error(`Rate limited. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); return makeApiCall(endpoint, data, retryCount + 1); } else { throw new Error(`Rate limit exceeded (429): Too many requests. Please try again later.`); } } else { // For other errors, pass through with some additional context throw new Error(`API Error (${statusCode || 'unknown'}): ${errorMessage}`); } } } // API endpoint handlers const handlers = { // Account get_credits: async () => makeApiCall("account/credits"), // Countries and Currencies get_countries: async () => makeApiCall("countries"), get_currencies: async () => makeApiCall("currencies"), // Keyword Data get_keyword_data: async (args) => { // Keywords Everywhere API expects "kw[]" format for each keyword const params = new URLSearchParams(); // Add each keyword individually to match API expectations if (Array.isArray(args.keywords)) { args.keywords.forEach(keyword => { params.append("kw[]", keyword); }); } // Add other parameters params.append("country", args.country || ""); // Default to global if not specified params.append("currency", args.currency || "myr"); // Default to Malaysian Ringgit params.append("dataSource", "cli"); // Use Google Keyword Planner & Clickstream data return makeApiCall("get_keyword_data", params); }, // Related Keywords get_related_keywords: async (args) => { const data = { keyword: args.keyword, num: args.num || 10 }; return makeApiCall("get_related_keywords", data); }, // PASF Keywords get_pasf_keywords: async (args) => { const data = { keyword: args.keyword, num: args.num || 10 }; return makeApiCall("get_pasf_keywords", data); }, // Domain Keywords get_domain_keywords: async (args) => { const data = { domain: args.domain, country: args.country || "", num: args.num || 10 }; return makeApiCall("get_domain_keywords", data); }, // URL Keywords get_url_keywords: async (args) => { const data = { url: args.url, country: args.country || "", num: args.num || 10 }; return makeApiCall("get_url_keywords", data); }, // Traffic Metrics get_domain_traffic: async (args) => { const data = { domain: args.domain, country: args.country || "" }; return makeApiCall("get_domain_traffic", data); }, get_url_traffic: async (args) => { const data = { url: args.url, country: args.country || "" }; return makeApiCall("get_url_traffic", data); }, // Backlinks get_domain_backlinks: async (args) => { const data = { domain: args.domain, num: args.num || 10 }; return makeApiCall("get_domain_backlinks", data); }, get_unique_domain_backlinks: async (args) => { const data = { domain: args.domain, num: args.num || 10 }; return makeApiCall("get_unique_domain_backlinks", data); }, get_page_backlinks: async (args) => { const data = { url: args.url, num: args.num || 10 }; return makeApiCall("get_page_backlinks", data); }, get_unique_page_backlinks: async (args) => { const data = { url: args.url, num: args.num || 10 }; return makeApiCall("get_unique_page_backlinks", data); } }; // Format response for different types of data function formatResponse(toolName, data) { switch (toolName) { case 'get_credits': return `Credit Balance: ${data[0]}`; case 'get_countries': if (Array.isArray(data)) { return data.map(country => `${country.code}: ${country.name}`).join('\n'); } return JSON.stringify(data, null, 2); case 'get_currencies': if (Array.isArray(data)) { return data.map(currency => `${currency.code}: ${currency.name}`).join('\n'); } return JSON.stringify(data, null, 2); case 'get_keyword_data': if (data.data && Array.isArray(data.data)) { return data.data.map(item => { return `${item.keyword}: - Search Volume: ${item.vol || 0} - CPC: ${item.cpc?.currency ? item.cpc.currency : 'RM'}${item.cpc?.value || '0.00'} - Competition: ${item.competition || 0} - Trend: ${item.trend && item.trend.length > 0 ? JSON.stringify(item.trend) : '[]'}`; }).join("\n\n"); } // Fallback for unexpected response structure return JSON.stringify(data, null, 2); case 'get_related_keywords': case 'get_pasf_keywords': case 'get_domain_keywords': case 'get_url_keywords': if (Array.isArray(data)) { return data.map((item, index) => { return `${index + 1}. ${item.keyword} - Search Volume: ${item.vol || 0} - CPC: ${item.cpc?.currency ? item.cpc.currency : 'RM'}${item.cpc?.value || '0.00'} - Competition: ${item.competition || 0}`; }).join("\n\n"); } return JSON.stringify(data, null, 2); case 'get_domain_traffic': case 'get_url_traffic': if (data && typeof data === 'object') { return `Traffic Metrics: - Total Keywords: ${data.totalKeywords || 0} - Total Traffic: ${data.totalTraffic || 0} - Traffic Cost: RM${data.trafficCost || 0}`; } return JSON.stringify(data, null, 2); case 'get_domain_backlinks': case 'get_unique_domain_backlinks': case 'get_page_backlinks': case 'get_unique_page_backlinks': if (Array.isArray(data)) { return data.map((item, index) => { return `${index + 1}. ${item.url} - Domain Authority: ${item.da || 0} - Page Authority: ${item.pa || 0} - Spam Score: ${item.spamScore || 0}`; }).join("\n\n"); } return JSON.stringify(data, null, 2); default: return JSON.stringify(data, null, 2); } } // Helper to convert JSON schema to Zod schema function jsonSchemaToZod(schema) { const properties = {}; if (schema && schema.properties) { Object.entries(schema.properties).forEach(([key, prop]) => { if (prop.type === "string") { properties[key] = z.string().describe(prop.description || ""); } else if (prop.type === "integer" || prop.type === "number") { properties[key] = z.number().describe(prop.description || ""); } else if (prop.type === "array") { properties[key] = z.array(z.string()).describe(prop.description || ""); } else if (prop.type === "object") { properties[key] = z.object({}).describe(prop.description || ""); } else { properties[key] = z.any().describe(prop.description || ""); } }); } return properties; } // Register each tool with the server Object.entries(TOOLS).forEach(([name, tool]) => { server.tool( name, jsonSchemaToZod(tool.inputSchema), async (args) => { try { const result = await handlers[name](args); const formattedResult = formatResponse(name, result); return { content: [{ type: "text", text: formattedResult }], isError: false }; } catch (error) { return { content: [{ type: "text", text: "Error: " + (error instanceof Error ? error.message : String(error)) }], isError: true }; } } ); }); async function runServer() { // Determine transport type based on environment variable or command line argument const transportType = process.env.TRANSPORT_TYPE || 'http'; if (transportType === 'stdio') { // Use STDIO transport for backward compatibility console.error('Starting with STDIO transport'); const transport = new StdioServerTransport(); await server.connect(transport); } else { // Use HTTP transport (default) - manually implemented since SDK doesn't support it yet const port = parseInt(process.env.PORT || '3000'); const host = process.env.HOST || 'localhost'; console.error(`Starting with HTTP transport on ${host}:${port}`); // Create Express app for HTTP server const app = express(); const httpServer = http.createServer(app); // Add CORS and other middleware app.use((req, res, next) => { // Allow all origins for testing purposes res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version'); res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id'); next(); }); // Parse JSON bodies app.use(express.json()); // Session management const sessions = new Map(); // Handle preflight requests app.options('/mcp', (req, res) => { res.status(200).end(); }); // Add a health check endpoint app.get('/mcp', (req, res) => { res.status(200).json({ status: 'ok', message: 'MCP server is running' }); }); // Implement the MCP endpoint for Streamable HTTP transport app.post('/mcp', async (req, res) => { // Set a longer timeout for the response req.setTimeout(30000); res.setTimeout(30000); // Log the incoming request console.error('Received MCP request:', { headers: req.headers, body: req.body, method: req.method, path: req.path }); try { // Check protocol version if provided const protocolVersion = req.headers['mcp-protocol-version']; // Accept any protocol version that starts with 2025- for compatibility if (protocolVersion && !protocolVersion.startsWith('2025-')) { console.error(`Unsupported protocol version: ${protocolVersion}`); return res.status(400).json({ jsonrpc: '2.0', id: req.body.id, error: { code: -32600, message: `Unsupported protocol version: ${protocolVersion}` } }); } // Get or create session let sessionId = req.headers['mcp-session-id']; let session; if (sessionId && sessions.has(sessionId)) { session = sessions.get(sessionId); } else if (!sessionId && req.body.method === 'initialize') { // Create new session for initialization requests sessionId = generateSessionId(); session = { id: sessionId, createdAt: Date.now() }; sessions.set(sessionId, session); } else if (sessionId) { // Invalid session ID return res.status(404).json({ error: { code: -32000, message: 'Session not found' } }); } else { // Missing session ID for non-initialization request return res.status(400).json({ error: { code: -32002, message: 'Session ID required' } }); } // Process the MCP request const mcpRequest = req.body; // Handle JSON-RPC batch requests if (Array.isArray(mcpRequest)) { // For simplicity, we'll process each request sequentially const responses = []; for (const request of mcpRequest) { try { const response = await processMcpRequest(request, server); console.error('Sending MCP response:', { sessionId, response: JSON.stringify(response, null, 2) }); if (response) { responses.push(response); } } catch (error) { responses.push({ jsonrpc: '2.0', id: request.id, error: { code: -32603, message: error.message || 'Internal error' } }); } } // If this is an initialization request, set the session ID header if (mcpRequest.some(req => req.method === 'initialize')) { res.setHeader('Mcp-Session-Id', sessionId); } // Return the batch response return res.json(responses); } else { // Handle single request try { const response = await processMcpRequest(mcpRequest, server); // If this is an initialization request, set the session ID header if (mcpRequest.method === 'initialize') { res.setHeader('Mcp-Session-Id', sessionId); } if (response) { return res.json(response); } else { return res.status(202).end(); } } catch (error) { return res.status(500).json({ jsonrpc: '2.0', id: mcpRequest.id, error: { code: -32603, message: error.message || 'Internal error' } }); } } } catch (error) { console.error('Error processing request:', error); return res.status(500).json({ error: { code: -32603, message: 'Internal server error' } }); } }); // Start the HTTP server await new Promise((resolve, reject) => { httpServer.listen(port, host, () => { console.error(`MCP server running at http://${host}:${port}/mcp`); resolve(); }); httpServer.on('error', (error) => { console.error('Failed to start HTTP server:', error); reject(error); }); }); } } // Helper function to generate a session ID function generateSessionId() { return 'session_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } // Helper function to process MCP requests async function processMcpRequest(request, server) { // Only handle requests with IDs (not notifications) if (!request.id) { return null; } // Forward the request to the MCP server // This is a simplified implementation - in a real-world scenario, // you would need to properly integrate with the MCP server's request handling // For now, we'll just handle the request manually if (request.method === 'initialize') { console.error('Processing initialize request:', request.id); // Get the tools information in the exact format expected by Smithery const toolsList = Object.keys(TOOLS).map(name => { const tool = TOOLS[name]; return { name, description: tool.description, parameters: { type: "object", properties: tool.inputSchema?.properties || {}, required: tool.inputSchema?.required || [] } }; }); console.error(`Returning ${toolsList.length} tools for initialize request`); return { jsonrpc: '2.0', id: request.id, result: { name: server.name, version: server.version, tools: toolsList } }; } else if (request.method === 'invoke') { const { tool, params } = request.params; try { console.error(`Processing invoke request for tool: ${tool}`, params); // Find the tool in our TOOLS object const toolDef = TOOLS[tool]; if (!toolDef) { console.error(`Tool not found: ${tool}`); return { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Tool not found: ${tool}` } }; } // Call the handler with the params const handler = handlers[tool]; if (!handler) { console.error(`Handler not found for tool: ${tool}`); return { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Handler not found for tool: ${tool}` } }; } // Execute the tool handler const result = await handler(params); const formattedResult = formatResponse(tool, result); console.error(`Tool ${tool} executed successfully`); return { jsonrpc: '2.0', id: request.id, result: { content: [{ type: "text", text: formattedResult }], isError: false } }; } catch (error) { console.error(`Error invoking tool ${tool}:`, error); return { jsonrpc: '2.0', id: request.id, error: { code: -32603, message: error instanceof Error ? error.message : String(error) } }; } } else { return { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Method not supported: ${request.method}` } }; } } async function main() { try { console.error("Starting Keywords Everywhere MCP server..."); await runServer(); } catch (error) { console.error("Fatal error running server:", error); // Print more detailed error information if (error.stack) { console.error("Stack trace:", error.stack); } if (error.cause) { console.error("Caused by:", error.cause); } process.exit(1); } } main();

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/hithereiamaliff/mcp-keywords-everywhere'

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