import { SearchResponse } from "./types.js"; import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; const CONTEXT7_API_BASE_URL = "https://context7.com/api"; const DEFAULT_TYPE = "txt"; // Encryption configuration const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; const ALGORITHM = 'aes-256-cbc'; // Validate encryption key function validateEncryptionKey(key: string): boolean { // Must be exactly 64 hex characters (32 bytes) return /^[0-9a-fA-F]{64}$/.test(key); } function encryptClientIp(clientIp: string): string { if (!validateEncryptionKey(ENCRYPTION_KEY)) { console.error("Invalid encryption key format. Must be 64 hex characters."); return clientIp; // Fallback to unencrypted } try { const iv = randomBytes(16); const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); let encrypted = cipher.update(clientIp, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } catch (error) { console.error("Error encrypting client IP:", error); return clientIp; // Fallback to unencrypted } } export function decryptClientIp(encryptedIp: string): string | null { if (!validateEncryptionKey(ENCRYPTION_KEY)) { console.error("Invalid encryption key format. Cannot decrypt."); return null; } try { const parts = encryptedIp.split(':'); if (parts.length !== 2) { // Not encrypted, return as-is return encryptedIp; } const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { console.error("Error decrypting client IP:", error); return null; } } /** * Searches for libraries matching the given query * @param query The search query * @param clientIp Optional client IP address to include in headers * @returns Search results or null if the request fails */ export async function searchLibraries(query: string, clientIp?: string): Promise { try { const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`); url.searchParams.set("query", query); const headers: Record = {}; if (clientIp) { headers["X-Client-IP"] = encryptClientIp(clientIp); } const response = await fetch(url, { headers }); if (!response.ok) { const errorCode = response.status; if (errorCode === 429) { console.error(`Rate limited due to too many requests. Please try again later.`); return { results: [], error: `Rate limited due to too many requests. Please try again later.`, } as SearchResponse; } console.error(`Failed to search libraries. Please try again later. Error code: ${errorCode}`); return { results: [], error: `Failed to search libraries. Please try again later. Error code: ${errorCode}`, } as SearchResponse; } return await response.json(); } catch (error) { console.error("Error searching libraries:", error); return { results: [], error: `Error searching libraries: ${error}` } as SearchResponse; } } /** * Fetches documentation context for a specific library * @param libraryId The library ID to fetch documentation for * @param options Options for the request * @param clientIp Optional client IP address to include in headers * @returns The documentation text or null if the request fails */ export async function fetchLibraryDocumentation( libraryId: string, options: { tokens?: number; topic?: string; } = {}, clientIp?: string ): Promise { try { if (libraryId.startsWith("/")) { libraryId = libraryId.slice(1); } const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/${libraryId}`); if (options.tokens) url.searchParams.set("tokens", options.tokens.toString()); if (options.topic) url.searchParams.set("topic", options.topic); url.searchParams.set("type", DEFAULT_TYPE); const headers: Record = { "X-Context7-Source": "mcp-server", }; if (clientIp) { headers["X-Client-IP"] = encryptClientIp(clientIp); } const response = await fetch(url, { headers }); if (!response.ok) { const errorCode = response.status; if (errorCode === 429) { const errorMessage = `Rate limited due to too many requests. Please try again later.`; console.error(errorMessage); return errorMessage; } const errorMessage = `Failed to fetch documentation. Please try again later. Error code: ${errorCode}`; console.error(errorMessage); return errorMessage; } const text = await response.text(); if (!text || text === "No content available" || text === "No context data available") { return null; } return text; } catch (error) { const errorMessage = `Error fetching library documentation. Please try again later. ${error}`; console.error(errorMessage); return errorMessage; } }