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

155 lines (154 loc) 7.84 kB
import { z } from "zod"; import axios from "axios"; import { API_CONFIG } from "./config.js"; import { createRequestLogger } from "../utils/logger.js"; export function registerContentsTool(server, config) { server.tool("contents_exa", "Retrieve content from specific URLs using Exa AI. Supports text extraction, highlights, summaries, subpage crawling, and structured output. Use this when you have specific URLs to fetch content from (not for searching).", { urls: z.array(z.string()).describe("Array of URLs to crawl and extract content from"), text: z.union([ z.boolean(), z.object({ maxCharacters: z.number().optional().describe("Maximum characters to return (default: 3000)") }) ]).optional().describe("If true, returns full page text. If object, can specify maxCharacters"), highlights: z.object({ numSentences: z.number().optional().describe("Number of sentences per highlight (default: 5)"), highlightsPerUrl: z.number().optional().describe("Number of highlights per URL (default: 1)"), query: z.string().optional().describe("Custom query to guide highlight selection") }).optional().describe("Extract relevant highlights from the content"), summary: z.object({ query: z.string().optional().describe("Custom query for summary generation"), schema: z.any().optional().describe("JSON schema for structured summary output") }).optional().describe("Generate summaries of the content"), livecrawl: z.enum(['never', 'fallback', 'always', 'preferred']).optional() .describe("Livecrawling options: 'never' (use cache only), 'fallback' (use cache first), 'always' (force livecrawl), 'preferred' (try livecrawl, fallback to cache)"), livecrawlTimeout: z.number().optional().describe("Timeout for livecrawling in milliseconds (default: 10000)"), subpages: z.number().optional().describe("Number of subpages to crawl from each URL"), subpageTarget: z.union([z.string(), z.array(z.string())]).optional() .describe("Keywords to find specific subpages"), extras: z.object({ links: z.number().optional().describe("Number of links to extract from each page"), imageLinks: z.number().optional().describe("Number of image links to extract") }).optional().describe("Extract additional data from pages"), context: z.union([z.boolean(), z.object({})]).optional() .describe("Format results as a context string ready for LLMs") }, async ({ urls, text, highlights, summary, livecrawl, livecrawlTimeout, subpages, subpageTarget, extras, context }) => { const requestId = `contents_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; const logger = createRequestLogger(requestId, 'contents_exa'); logger.start(`Fetching content from ${urls.length} URLs`); try { const apiKey = config?.exaApiKey || process.env.EXA_API_KEY; if (!apiKey) { logger.error('No API key found'); return { content: [{ type: "text", text: "EXA_API_KEY environment variable is not set" }], isError: true }; } // Create a fresh axios instance for each request const axiosInstance = axios.create({ baseURL: API_CONFIG.BASE_URL, headers: { 'accept': 'application/json', 'content-type': 'application/json', 'x-api-key': apiKey }, timeout: 30000 }); // Build the request body const requestBody = { urls, ...(text !== undefined && { text }), ...(highlights && { highlights }), ...(summary && { summary }), ...(livecrawl && { livecrawl }), ...(livecrawlTimeout && { livecrawlTimeout }), ...(subpages !== undefined && { subpages }), ...(subpageTarget && { subpageTarget }), ...(extras && { extras }), ...(context !== undefined && { context }) }; logger.log("Sending request to Exa API /contents endpoint"); logger.log(`Request body: ${JSON.stringify(requestBody, null, 2)}`); const response = await axiosInstance.post('/contents', requestBody, { maxBodyLength: Infinity, maxContentLength: Infinity }); logger.log(`Response received - Status: ${response.status}`); logger.complete(); // Format the response const results = response.data.results.map(result => { const formattedResult = { title: result.title, url: result.url, id: result.id }; if (result.publishedDate) formattedResult.publishedDate = result.publishedDate; if (result.author) formattedResult.author = result.author; if (result.score !== null) formattedResult.score = result.score; if (result.image) formattedResult.image = result.image; if (result.favicon) formattedResult.favicon = result.favicon; if (result.text) formattedResult.text = result.text; if (result.highlights && result.highlights.length > 0) { formattedResult.highlights = result.highlights; if (result.highlightScores) formattedResult.highlightScores = result.highlightScores; } if (result.summary) formattedResult.summary = result.summary; if (result.subpages && result.subpages.length > 0) { formattedResult.subpages = result.subpages; } if (result.extras) formattedResult.extras = result.extras; return formattedResult; }); const responseData = { requestId: response.data.requestId, results }; if (response.data.context) responseData.context = response.data.context; if (response.data.statuses) responseData.statuses = response.data.statuses; if (response.data.costDollars) responseData.costDollars = response.data.costDollars; return { content: [{ type: "text", text: JSON.stringify(responseData, null, 2) }] }; } catch (error) { logger.error(error); if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || error.message; logger.log(`API Error: ${errorMessage}`); return { content: [{ type: "text", text: `Failed to fetch content: ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Failed to fetch content: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); }