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
JavaScript
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()}`);
}