Skip to main content
Glama
waldzellai

Exa Websets MCP Server

by waldzellai
index.ts20.2 kB
#!/usr/bin/env node import { config } from "dotenv"; import express from "express"; import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; // Import only the tools we need import websetsGuideTool from "./tools/websetsGuide.js"; import { toolRegistry } from "./tools/config.js"; // Import tools to register them import "./tools/webSearch.js"; import "./tools/websetsManager.js"; import "./tools/knowledgeGraph.js"; // Import feature flags import { featureFlags } from "./config/features.js"; // Import prompts import { crmOrchestration, enrichmentWorkflow, hiringOrchestration, horizontalProcess, integrationProcess, iterativeIntelligence, listMcpAssets, marketingOrchestration, quickStart, websetAnalysisGuide, websetDiscovery, websetPortal, websetStatusCheck, webhookSetupGuide } from "./prompts/index.js"; // Import resources import { resolveOrchestrationResource, listOrchestrationResources } from "./resources/index.js"; // Load environment variables config(); // Color codes for console output const colors = { reset: "\x1b[0m", bright: "\x1b[1m", green: "\x1b[32m", blue: "\x1b[34m", cyan: "\x1b[36m", yellow: "\x1b[33m", magenta: "\x1b[35m", red: "\x1b[31m" }; /** * Exa AI Websets MCP Server * * This MCP server provides Exa AI's websets management capabilities and basic web search * functionality to AI assistants through the Model Context Protocol. * * The server provides four essential tools: * - websets_manager: Comprehensive websets management * - web_search_exa: Real-time web searching capabilities * - websets_guide: Helpful guidance for using websets * - knowledge_graph: Onboard graph to track connections between webset results */ export class ExaWebsetsServer { private app: express.Application; private server: McpServer; private activeSessions = new Map<string, StreamableHTTPServerTransport>(); /** * Creates a new ExaWebsetsServer instance * * @param apiKey Optional Exa API key */ constructor(apiKey?: string) { // Set API key if provided if (apiKey) { process.env.EXA_API_KEY = apiKey; } // Build server capabilities based on feature flags const capabilities: any = {}; if (featureFlags.isEnabled('logging')) { capabilities.logging = {}; } if (featureFlags.isEnabled('sampling')) { capabilities.sampling = {}; } // Initialize MCP server with capabilities this.server = new McpServer({ name: "exa-websets-server", version: "1.0.4" }, { capabilities }); // Initialize Express app this.app = express(); this.app.use(express.json()); // Setup server components this.registerTools(); this.registerPrompts(); this.registerResources(); this.registerProtocolHandlers(); } /** * Register all tools with the MCP server */ private registerTools(): void { // Create our simplified tool registry with three tools const simplifiedRegistry = { web_search_exa: toolRegistry["web_search_exa"], websets_manager: toolRegistry["websets_manager"], websets_guide: websetsGuideTool, knowledge_graph: toolRegistry["knowledge_graph"], }; // Register our tools Object.values(simplifiedRegistry).forEach(tool => { if (tool) { this.server.tool( tool.name, tool.description, tool.schema, tool.handler ); } }); } /** * Register all prompts with the MCP server */ private registerPrompts(): void { // Register prompts this.server.prompt("list_mcp_assets", "List all available MCP server capabilities including prompts, tools, and resources", async () => ({ messages: [{ role: "user", content: { type: "text", text: await listMcpAssets() } }] })); this.server.prompt("webset_discovery", "Discover and explore available websets", async () => ({ messages: [{ role: "user", content: { type: "text", text: await websetDiscovery() } }] })); this.server.prompt("webset_status_check", "Check status of async webset operations", { websetId: z.string().describe("The ID of the webset to check") }, async ({ websetId }) => ({ messages: [{ role: "user", content: { type: "text", text: await websetStatusCheck(websetId) } }] }) ); this.server.prompt("webset_analysis_guide", "Guide for analyzing completed websets", { websetId: z.string().describe("The ID of the webset to analyze") }, async ({ websetId }) => ({ messages: [{ role: "user", content: { type: "text", text: await websetAnalysisGuide(websetId) } }] }) ); this.server.prompt("webhook_setup_guide", "Configure webhooks for webset notifications", async () => ({ messages: [{ role: "user", content: { type: "text", text: await webhookSetupGuide() } }] })); this.server.prompt("quick_start", "Get started quickly with creating your first webset", async () => ({ messages: [{ role: "user", content: { type: "text", text: await quickStart() } }] })); this.server.prompt("enrichment_workflow", "Workflow for enriching webset data", { websetId: z.string().describe("The ID of the webset to enrich") }, async ({ websetId }) => ({ messages: [{ role: "user", content: { type: "text", text: await enrichmentWorkflow(websetId) } }] }) ); this.server.prompt("integration_process", "Process for integrating a webset with external systems", { websetId: z.string().describe("The ID of the webset to integrate"), targetSystem: z.string().describe("The target system for integration") }, async ({ websetId, targetSystem }) => ({ messages: [{ role: "user", content: { type: "text", text: await integrationProcess(websetId, targetSystem) } }] }) ); this.server.prompt("horizontal_process", "Process for analyzing multiple websets horizontally", { searchCriteria: z.string().describe("Comma-separated list of search terms to create and analyze websets"), projectName: z.string().optional().describe("Name for the horizontal analysis project") }, async ({ searchCriteria, projectName }) => { // Transform comma-separated string to array inline const criteriaArray = searchCriteria.split(',').map(s => s.trim()); return { messages: [{ role: "user", content: { type: "text", text: await horizontalProcess(criteriaArray, projectName) } }] }; } ); this.server.prompt("webset_portal", "Portal for webset management", { websetId: z.string().describe("The ID of the webset to manage"), researchQuery: z.string().optional().describe("Research query for analyzing the webset") }, async ({ websetId, researchQuery }) => { // Provide a default value when researchQuery is undefined const queryToUse = researchQuery || "General webset analysis"; return { messages: [{ role: "user", content: { type: "text", text: await websetPortal(websetId, queryToUse) } }] }; } ); this.server.prompt("iterative_intelligence", "Research assistant for iterative webset improvement", { researchTopic: z.string().describe("Topic to research"), iterations: z.string().optional().describe("Number of research iterations"), registryPath: z.string().optional().describe("Optional registry path") }, async ({ researchTopic, iterations, registryPath }) => ({ messages: [{ role: "user", content: { type: "text", text: await iterativeIntelligence( researchTopic, iterations ? parseInt(iterations) : undefined, registryPath ) } }] }) ); // Orchestration Prompts - User-focused workflows for common use cases this.server.prompt("marketing_orchestration", "Competitor and brand monitoring orchestration", { companyName: z.string().describe("Your company name"), targetAudience: z.string().optional().describe("Target audience for monitoring"), timeframe: z.string().optional().describe("Time range for monitoring (e.g., '30d', '90d')") }, async ({ companyName, targetAudience, timeframe }) => ({ messages: [{ role: "user", content: { type: "text", text: await marketingOrchestration(companyName, targetAudience, timeframe) } }] }) ); this.server.prompt("crm_orchestration", "Lead discovery and account-based marketing orchestration", { idealCustomerProfile: z.string().describe("Description of your ideal customer profile (ICP)"), region: z.string().optional().describe("Geographic region for lead search"), intentSignals: z.string().optional().describe("Intent signals to search for (e.g., hiring, funding, product launches)") }, async ({ idealCustomerProfile, region, intentSignals }) => ({ messages: [{ role: "user", content: { type: "text", text: await crmOrchestration(idealCustomerProfile, region, intentSignals) } }] }) ); this.server.prompt("hiring_orchestration", "Recruiter and job seeker hiring orchestration", { mode: z.enum(["recruiter", "job_seeker"]).describe("Mode: 'recruiter' for candidate sourcing or 'job_seeker' for target company tracking"), role: z.string().describe("Job title or role to search for"), location: z.string().optional().describe("Location preference (e.g., 'Bay Area', 'Remote', 'NYC')"), seniority: z.string().optional().describe("Seniority level (e.g., 'junior', 'mid-level', 'senior', 'staff')"), keywords: z.string().optional().describe("Key skills or interests (comma-separated)") }, async ({ mode, role, location, seniority, keywords }) => ({ messages: [{ role: "user", content: { type: "text", text: await hiringOrchestration(mode, role, location, seniority, keywords) } }] }) ); } /** * Register resources for orchestrations */ private registerResources(): void { // Register a catch-all resource handler for websets:// URIs this.server.resource( "websets://orchestrations/*", "Orchestration resources (marketing, CRM, hiring)", async (uri: URL) => { const resource = await resolveOrchestrationResource(uri.toString()); if (!resource) { throw new Error(`Failed to resolve resource: ${uri}`); } return { contents: [{ uri: resource.uri, mimeType: resource.mimeType, text: resource.text }] }; } ); } /** * Register protocol handlers for MCP compliance */ private registerProtocolHandlers(): void { // Access the underlying server protocol for advanced handlers const protocol = this.server.server; // Always register ping handler (required by MCP) protocol.setRequestHandler( z.object({ method: z.literal('ping') }), async () => { return {}; } ); // Register logging handler only if feature is enabled if (featureFlags.isEnabled('logging')) { protocol.setRequestHandler( z.object({ method: z.literal('logging/setLevel'), params: z.object({ level: z.string() }).optional() }), async (request) => { // TODO: Implement logging level changes return { success: true, message: "Logging level change acknowledged (not yet implemented)" }; } ); } // Register sampling handler only if feature is enabled if (featureFlags.isEnabled('sampling')) { protocol.setRequestHandler( z.object({ method: z.literal('sampling/createMessage'), params: z.any().optional() }), async (request) => { // TODO: Implement sampling return { success: true, message: "Sampling acknowledged (not yet implemented)" }; } ); } } /** * Get the internal MCP server instance */ public getMcpServer(): McpServer { return this.server; } /** * Start the server with HTTP transport */ public async startHttpServer(port: number = 3000): Promise<void> { try { // Handle POST requests for client-to-server communication this.app.post('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; // Case 1: Existing session - reuse transport if (sessionId && this.activeSessions.has(sessionId)) { const existingTransport = this.activeSessions.get(sessionId)!; await existingTransport.handleRequest(req, res, req.body); return; } // Case 2: New session - create transport if (!sessionId && isInitializeRequest(req.body)) { const newTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { // Store the new session this.activeSessions.set(newSessionId, newTransport); } }); // Clean up when session closes newTransport.onclose = () => { if (newTransport.sessionId) { this.activeSessions.delete(newTransport.sessionId); } }; // Connect the MCP server to the transport await this.server.connect(newTransport); // Handle the initialization request await newTransport.handleRequest(req, res, req.body); return; } // Case 3: Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided or not an initialization request', }, id: null, }); }); // Handle GET requests for server-to-client notifications via SSE this.app.get('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !this.activeSessions.has(sessionId)) { res.status(400).send('Invalid or missing session ID'); return; } const transport = this.activeSessions.get(sessionId)!; await transport.handleRequest(req, res); }); // Handle DELETE requests for session termination this.app.delete('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !this.activeSessions.has(sessionId)) { res.status(400).send('Invalid or missing session ID'); return; } const transport = this.activeSessions.get(sessionId)!; await transport.handleRequest(req, res); }); // Add health endpoint like in reddit-mcp this.app.get('/health', (req, res) => { const baseUrl = `${req.protocol}://${req.get("host")}`; res.json({ service: "Exa Websets MCP Server", version: "1.0.4", transport: "http", endpoints: { mcp: `${baseUrl}/mcp`, health: `${baseUrl}/health`, }, status: "healthy" }); }); this.app.listen(port, () => { console.log(`${colors.bright}${colors.cyan}Exa Websets MCP Server ${colors.reset}${colors.bright}(HTTP)${colors.reset} ${colors.green}listening on port ${port}${colors.reset}`); console.log(`${colors.bright}${colors.blue}Connect via: ${colors.reset}http://localhost:${port}/mcp`); console.log(`${colors.bright}${colors.green}Health check: ${colors.reset}http://localhost:${port}/health`); }); } catch (error) { console.error(`${colors.bright}${colors.red}Failed to start HTTP server:${colors.reset}`, error); process.exit(1); } } /** * Start the server with STDIO transport */ public async startStdioServer(): Promise<void> { try { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`${colors.bright}${colors.magenta}Exa Websets MCP Server${colors.reset} started in ${colors.bright}STDIO mode${colors.reset}`); } catch (error) { console.error(`${colors.bright}${colors.red}Failed to start STDIO server:${colors.reset}`, error); process.exit(1); } } } /** * Main execution function */ async function main(): Promise<void> { try { const mode = process.argv[2]; const server = new ExaWebsetsServer(process.env.EXA_API_KEY); if (mode === '--http') { // HTTP mode with optional port const port = process.env.PORT ? parseInt(process.env.PORT) : process.argv[3] ? parseInt(process.argv[3]) : 3000; console.log(`${colors.bright}${colors.yellow}Starting in HTTP mode on port ${port}${colors.reset}`); await server.startHttpServer(port); } else if (mode === '--stdio') { // STDIO mode when explicitly requested - use stderr for logging console.error(`${colors.bright}${colors.yellow}Starting in STDIO mode${colors.reset}`); await server.startStdioServer(); } else { // Default to STDIO mode (MCP standard) - use stderr for logging console.error(`${colors.bright}${colors.yellow}Starting in STDIO mode (default)${colors.reset}`); await server.startStdioServer(); } } catch (error) { console.error(`${colors.bright}${colors.red}Fatal error:${colors.reset}`, error); process.exit(1); } } // Run when this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error(`${colors.bright}${colors.red}Unhandled error:${colors.reset}`, error); process.exit(1); }); } /** * Create and configure the MCP server instance * This function is maintained for backward compatibility */ function createServer(apiKey?: string): McpServer { const server = new ExaWebsetsServer(apiKey); return server.getMcpServer(); } // Export configSchema for Smithery export const configSchema = z.object({ exaApiKey: z.string().describe("The API key for accessing the Exa AI Websets and Search API") }); // Default export for Smithery - function that accepts config export default function ({ config }: { config: z.infer<typeof configSchema> }) { try { // Create server with API key from config const server = new ExaWebsetsServer(config.exaApiKey); return server.getMcpServer(); } catch (e) { console.error(e); throw e; } }

Implementation Reference

Latest Blog Posts

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/waldzellai/exa-mcp-server-websets'

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