UNPKG

octagon-mcp

Version:

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

198 lines (197 loc) 6.45 kB
import { DOCS_DEFAULT_CACHE_TTL_MS, DOCS_PRIMARY_INDEX_URL, } from "./config.js"; const PRIMARY_DOCS_ENTRIES = [ { title: "Octagon Claude Plugin", url: "https://octagonai.co/docs/guide/claude-plugin", section: "Guides", summary: "Claude plugin setup, connector authentication, hosted Octagon tools, routing, and MCP integration guidance.", }, { title: "Octagon Agents Guide", url: "https://octagonai.co/docs/guide/agents/", section: "Guides", summary: "Overview of Octagon public market, private market, deep research, and prediction markets agents.", }, { title: "Octagon MCP Server", url: "https://octagonai.co/docs/guide/mcp-server", section: "Guides", summary: "MCP server installation, available tools, Claude Desktop and Cursor setup, and integration behavior.", }, ]; function slugify(value) { const slug = value .toLowerCase() .replace(/[`*_~()[\]{}.:,/?#!$&'"|]+/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || "section"; } function cleanFetchedMarkdown(markdown) { return markdown .replace(/^Source URL:[^\n]*\nTitle:[^\n]*\n\n/, "") .replace(/\r\n/g, "\n") .trim(); } function normalizeDocsUrl(link, sourceUrl) { try { return new URL(link, sourceUrl).toString(); } catch { return new URL(link, DOCS_PRIMARY_INDEX_URL).toString(); } } function pathFromUrl(url) { try { const parsed = new URL(url); return `${parsed.pathname}${parsed.hash}`; } catch { return url; } } function uniqueId(base, seen) { const priorCount = seen.get(base) ?? 0; seen.set(base, priorCount + 1); return priorCount === 0 ? base : `${base}-${priorCount + 1}`; } function extractHeadings(lines) { const headings = []; lines.forEach((line, lineIndex) => { const match = /^(#{1,3})\s+(.+?)\s*$/.exec(line); if (!match) { return; } headings.push({ level: match[1].length, title: match[2].trim(), lineIndex, }); }); return headings; } function sectionForHeading(headings, headingIndex) { const heading = headings[headingIndex]; if (heading.level === 1) { return heading.title; } for (let i = headingIndex - 1; i >= 0; i -= 1) { if (headings[i].level < heading.level) { return headings[i].title; } } return heading.title; } function headingEntries(markdown, sourceUrl, source, seenIds) { const lines = markdown.split("\n"); const headings = extractHeadings(lines); const entries = []; headings.forEach((heading, index) => { const nextHeading = headings[index + 1]; const endLine = nextHeading?.lineIndex ?? lines.length; const content = lines.slice(heading.lineIndex, endLine).join("\n").trim(); if (content.length === 0) { return; } const slug = slugify(heading.title); const url = `${sourceUrl}#${slug}`; const id = uniqueId(`${source}:${slug}`, seenIds); entries.push({ id, title: heading.title, url, path: pathFromUrl(url), section: sectionForHeading(headings, index), source, kind: "section", summary: content .split("\n") .slice(1) .find(line => line.trim().length > 0) ?.replace(/^[-*>#\s]+/, "") .slice(0, 280), content, }); }); return entries; } function linkEntries(markdown, sourceUrl, source, seenIds) { const lines = markdown.split("\n"); const entries = []; let currentSection = "Docs"; for (const line of lines) { const headingMatch = /^(#{1,3})\s+(.+?)\s*$/.exec(line); if (headingMatch) { currentSection = headingMatch[2].trim(); } const linkPattern = /\[([^\]]+)\]\(([^)]+)\)(?::\s*(.*))?/g; let match; while ((match = linkPattern.exec(line)) !== null) { const title = match[1].trim(); const url = normalizeDocsUrl(match[2].trim(), sourceUrl); const id = uniqueId(`${source}:${slugify(title)}`, seenIds); const summary = match[3]?.trim(); entries.push({ id, title, url, path: pathFromUrl(url), section: currentSection, source, kind: "link", summary: summary && summary.length > 0 ? summary : undefined, }); } } return entries; } function dedupeEntries(entries) { const seen = new Set(); const deduped = []; for (const entry of entries) { const key = `${entry.kind}:${entry.url}:${entry.title}`.toLowerCase(); if (seen.has(key)) { continue; } seen.add(key); deduped.push(entry); } return deduped; } function primaryDocsEntries(source, seenIds) { if (source !== "docs") { return []; } return PRIMARY_DOCS_ENTRIES.map(entry => ({ id: uniqueId(`${source}:${slugify(entry.title)}`, seenIds), title: entry.title, url: entry.url, path: pathFromUrl(entry.url), section: entry.section, source, kind: "link", summary: entry.summary, })); } export function parseDocsCatalog(fetched, { source, cacheTtlMs = DOCS_DEFAULT_CACHE_TTL_MS, }) { const rawMarkdown = cleanFetchedMarkdown(fetched.text); const seenIds = new Map(); const entries = dedupeEntries([ ...primaryDocsEntries(source, seenIds), ...headingEntries(rawMarkdown, fetched.finalUrl, source, seenIds), ...linkEntries(rawMarkdown, fetched.finalUrl, source, seenIds), ]); const sections = Array.from(new Set(entries.map(entry => entry.section))).sort((a, b) => a.localeCompare(b)); const fetchedAtMs = Date.parse(fetched.fetchedAt); return { sourceUrl: fetched.finalUrl, source, fetchedAt: fetched.fetchedAt, expiresAt: new Date(fetchedAtMs + cacheTtlMs).toISOString(), entries, sections, rawMarkdown, etag: fetched.etag, lastModified: fetched.lastModified, }; }