UNPKG

markov-exa-mcp-server

Version:

A Model Context Protocol server with Exa for web search, academic paper search, and Twitter/X.com search. Provides real-time web searches with configurable tool selection, allowing users to enable or disable specific search capabilities. Supports customiz

420 lines (419 loc) 19 kB
import { z } from "zod"; import axios from "axios"; import { API_CONFIG } from "./config.js"; import { createRequestLogger } from "../utils/logger.js"; // Zod schemas for validation const entityTypeSchema = z.enum(['company', 'person', 'article', 'research_paper', 'custom']); const entityConfigSchema = z.object({ type: entityTypeSchema, customType: z.string().optional() }).refine(data => data.type !== 'custom' || (data.type === 'custom' && data.customType), { message: "customType is required when type is 'custom'" }); const searchConfigSchema = z.object({ query: z.string().describe("Search query to find matching items"), count: z.number().describe("Number of results to retrieve"), criteria: z.array(z.object({ description: z.string() })).optional().describe("Verification criteria for results"), behavior: z.enum(['append', 'replace']).optional().describe("How to handle new results") }); const enrichmentConfigSchema = z.object({ description: z.string().describe("What data to extract"), format: z.enum(['text', 'number', 'boolean', 'json']).describe("Expected output format"), schema: z.any().optional().describe("JSON schema for structured output (when format is 'json')") }); export function registerWebsetsTools(server, config) { const getApiKey = () => config?.exaApiKey || process.env.EXA_API_KEY; const createAxiosInstance = (apiKey) => { return axios.create({ baseURL: API_CONFIG.WEBSETS_ENDPOINT, headers: { 'accept': 'application/json', 'content-type': 'application/json', 'x-api-key': apiKey }, timeout: API_CONFIG.REQUEST_TIMEOUT }); }; // Tool to create a new webset server.tool("websets_create_exa", "Create a new Webset to automatically search, verify, and enrich web data. Returns a webset ID that can be used to track progress and retrieve results.", { search: searchConfigSchema.optional().describe("Initial search configuration"), enrichments: z.array(enrichmentConfigSchema).optional() .describe("Data extraction configurations to run on each item"), entity: entityConfigSchema.optional() .describe("Entity type configuration for the webset"), metadata: z.record(z.any()).optional() .describe("Custom metadata to attach to the webset"), webhookUrl: z.string().url().optional() .describe("URL to receive webhook notifications for events") }, async ({ search, enrichments, entity, metadata, webhookUrl }) => { const requestId = `websets_create-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'websets_create_exa'); try { const apiKey = getApiKey(); if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } const axiosInstance = createAxiosInstance(apiKey); const requestBody = { ...(search && { search }), ...(enrichments && { enrichments }), ...(entity && { entity }), ...(metadata && { metadata }), ...(webhookUrl && { webhookUrl }) }; logger.log(`Creating webset with config: ${JSON.stringify(requestBody, null, 2)}`); const response = await axiosInstance.post('/websets', requestBody); logger.log(`Webset created successfully - ID: ${response.data.id}`); logger.complete(); return { content: [{ type: "text", text: JSON.stringify({ websetId: response.data.id, status: response.data.status, message: "Webset created successfully. Use 'websets_get_exa' to check status or 'websets_items_list_exa' to retrieve items.", webset: response.data }, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; const statusCode = error.response?.status; // Handle specific case for Pro account requirement if (errorMessage.includes('does not have access to the API') || errorMessage.includes('Upgrade to a Pro plan')) { return { content: [{ type: "text", text: `Websets functionality requires an Exa Pro subscription ($449/month). Your current account tier does not have access to the Websets API. Consider using a combination of web_search_exa and contents_exa tools for similar functionality, or upgrade your Exa account at https://dashboard.exa.ai/` }], isError: true }; } return { content: [{ type: "text", text: `Failed to create webset (${statusCode}): ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to create webset: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); // Tool to list websets server.tool("websets_list_exa", "List all websets with pagination support. Returns websets in reverse chronological order.", { limit: z.number().optional().describe("Number of results per page (default: 20, max: 100)"), cursor: z.string().optional().describe("Pagination cursor from previous response"), status: z.enum(['idle', 'running', 'canceled']).optional() .describe("Filter by webset status") }, async ({ limit = 20, cursor, status }) => { const requestId = `websets_list-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'websets_list_exa'); try { const apiKey = getApiKey(); if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } const axiosInstance = createAxiosInstance(apiKey); const params = { limit }; if (cursor) params.cursor = cursor; if (status) params.status = status; logger.log(`Listing websets with params: ${JSON.stringify(params)}`); const response = await axiosInstance.get('/websets', { params }); logger.log(`Retrieved ${response.data.data.length} websets`); logger.complete(); return { content: [{ type: "text", text: JSON.stringify({ websets: response.data.data, hasMore: response.data.hasMore, cursor: response.data.cursor, count: response.data.data.length }, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: "text", text: `Failed to list websets: ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to list websets: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); // Tool to get webset details server.tool("websets_get_exa", "Get details of a specific webset including status, item count, and current operations.", { websetId: z.string().describe("The webset ID to retrieve"), expand: z.enum(['items']).optional() .describe("Expand the response to include the latest 100 items") }, async ({ websetId, expand }) => { const requestId = `websets_get-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'websets_get_exa'); try { const apiKey = getApiKey(); if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } const axiosInstance = createAxiosInstance(apiKey); const params = {}; if (expand) params.expand = expand; logger.log(`Getting webset ${websetId}`); const response = await axiosInstance.get(`/websets/${websetId}`, { params }); logger.log(`Retrieved webset - Status: ${response.data.status}, Items: ${response.data.itemCount || 0}`); logger.complete(); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: "text", text: `Failed to get webset: ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to get webset: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); // Tool to list items in a webset server.tool("websets_items_list_exa", "List items in a webset with pagination. Items are available immediately as they're discovered.", { websetId: z.string().describe("The webset ID to list items from"), limit: z.number().optional().describe("Number of items per page (default: 20, max: 100)"), cursor: z.string().optional().describe("Pagination cursor from previous response"), enriched: z.boolean().optional().describe("Filter for only enriched items") }, async ({ websetId, limit = 20, cursor, enriched }) => { const requestId = `websets_items_list-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'websets_items_list_exa'); try { const apiKey = getApiKey(); if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } const axiosInstance = createAxiosInstance(apiKey); const params = { limit }; if (cursor) params.cursor = cursor; if (enriched !== undefined) params.enriched = enriched; logger.log(`Listing items for webset ${websetId}`); const response = await axiosInstance.get(`/websets/${websetId}/items`, { params }); logger.log(`Retrieved ${response.data.data.length} items`); logger.complete(); return { content: [{ type: "text", text: JSON.stringify({ items: response.data.data, hasMore: response.data.hasMore, cursor: response.data.cursor, count: response.data.data.length }, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: "text", text: `Failed to list items: ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to list items: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); // Tool to create additional search on existing webset server.tool("websets_search_create_exa", "Create an additional search on an existing webset. New results will be added to the webset.", { websetId: z.string().describe("The webset ID to add search to"), search: searchConfigSchema.describe("Search configuration") }, async ({ websetId, search }) => { const requestId = `websets_search_create-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'websets_search_create_exa'); try { const apiKey = getApiKey(); if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } const axiosInstance = createAxiosInstance(apiKey); const requestBody = { websetId, search }; logger.log(`Creating search for webset ${websetId}`); const response = await axiosInstance.post(`/websets/${websetId}/searches`, requestBody); logger.log(`Search created successfully - ID: ${response.data.id}`); logger.complete(); return { content: [{ type: "text", text: JSON.stringify({ searchId: response.data.id, status: response.data.status, message: "Search created successfully. Results will be added to the webset as they're discovered.", search: response.data }, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: "text", text: `Failed to create search: ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to create search: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); // Tool to cancel a webset or its operations server.tool("websets_cancel_exa", "Cancel all running operations on a webset. This will stop any active searches or enrichments.", { websetId: z.string().describe("The webset ID to cancel operations for") }, async ({ websetId }) => { const requestId = `websets_cancel-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'websets_cancel_exa'); try { const apiKey = getApiKey(); if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } const axiosInstance = createAxiosInstance(apiKey); logger.log(`Canceling operations for webset ${websetId}`); const response = await axiosInstance.post(`/websets/${websetId}/cancel`); logger.log(`Operations canceled successfully`); logger.complete(); return { content: [{ type: "text", text: JSON.stringify({ websetId, message: "All running operations have been canceled", response: response.data }, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: "text", text: `Failed to cancel operations: ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to cancel operations: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); }