UNPKG

octagon-mcp

Version:

MCP server for Octagon API. Provides specialized AI agents for investment research of public and private markets.

106 lines (105 loc) 3.44 kB
import { DOCS_ALLOWED_HOSTS, DOCS_DEFAULT_RETRY_COUNT, DOCS_DEFAULT_RETRY_DELAY_MS, DOCS_DEFAULT_TIMEOUT_MS, DOCS_USER_AGENT, } from "./config.js"; class DocsHttpError extends Error { status; constructor(status) { super(`Docs fetch failed with HTTP ${status}`); this.status = status; } } export function assertAllowedDocsUrl(value) { let url; try { url = new URL(value); } catch { throw new Error(`Invalid docs URL: ${value}`); } if (!["http:", "https:"].includes(url.protocol)) { throw new Error(`Unsupported docs URL protocol: ${url.protocol}`); } if (!DOCS_ALLOWED_HOSTS.has(url.hostname)) { throw new Error(`Unsupported docs URL host: ${url.hostname}`); } return url; } function createTimeoutSignal(timeoutMs) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); return { signal: controller.signal, clear: () => clearTimeout(timeout), }; } function shouldRetry(error) { if (error instanceof DocsHttpError) { return error.status === 429 || error.status >= 500; } return true; } async function wait(ms) { if (ms <= 0) { return; } await new Promise(resolve => { setTimeout(resolve, ms); }); } async function fetchDocsTextOnce(parsedUrl, timeoutMs, headers) { const timeout = createTimeoutSignal(timeoutMs); try { const response = await fetch(parsedUrl, { redirect: "follow", signal: timeout.signal, headers: { "User-Agent": DOCS_USER_AGENT, Accept: "text/markdown,text/plain,text/html;q=0.9,*/*;q=0.1", ...headers, }, }); if (!response.ok) { throw new DocsHttpError(response.status); } const contentType = response.headers.get("content-type") ?? undefined; const text = await response.text(); return { url: parsedUrl.toString(), finalUrl: response.url || parsedUrl.toString(), text, contentType, etag: response.headers.get("etag") ?? undefined, lastModified: response.headers.get("last-modified") ?? undefined, fetchedAt: new Date().toISOString(), }; } catch (error) { if (timeout.signal.aborted) { throw new Error(`Docs fetch timed out after ${timeoutMs}ms`); } throw error; } finally { timeout.clear(); } } export async function fetchDocsText(url, options = {}) { const parsedUrl = assertAllowedDocsUrl(url); const timeoutMs = options.timeoutMs ?? DOCS_DEFAULT_TIMEOUT_MS; const retryCount = options.retryCount ?? DOCS_DEFAULT_RETRY_COUNT; const retryDelayMs = options.retryDelayMs ?? DOCS_DEFAULT_RETRY_DELAY_MS; let lastError; for (let attempt = 0; attempt <= retryCount; attempt += 1) { try { return await fetchDocsTextOnce(parsedUrl, timeoutMs, options.headers); } catch (error) { lastError = error; if (attempt >= retryCount || !shouldRetry(error)) { break; } await wait(retryDelayMs * (attempt + 1)); } } throw lastError instanceof Error ? lastError : new Error(`Failed to fetch ${parsedUrl.toString()}`); }