UNPKG

octagon-mcp

Version:

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

97 lines (96 loc) 3.25 kB
const STOP_WORDS = new Set([ "and", "api", "can", "complete", "docs", "example", "examples", "for", "from", "guide", "how", "integration", "the", "this", "use", "using", "with", ]); function tokenize(value) { return Array.from(new Set(value .toLowerCase() .split(/[^a-z0-9]+/) .filter(token => token.length >= 2))); } function distinctiveTokens(tokens) { const distinctive = tokens.filter(token => token.length >= 3 && !STOP_WORDS.has(token)); return distinctive.length > 0 ? distinctive : tokens; } function countMatches(value, tokens) { const lower = value.toLowerCase(); return tokens.reduce((count, token) => count + (lower.includes(token) ? 1 : 0), 0); } function createSnippet(entry, tokens) { const haystack = entry.content ?? entry.summary; if (!haystack) { return undefined; } const lower = haystack.toLowerCase(); const index = tokens .map(token => lower.indexOf(token)) .filter(position => position >= 0) .sort((a, b) => a - b)[0]; if (index === undefined) { return haystack.slice(0, 240).trim(); } const start = Math.max(0, index - 100); const end = Math.min(haystack.length, index + 180); const prefix = start > 0 ? "..." : ""; const suffix = end < haystack.length ? "..." : ""; return `${prefix}${haystack.slice(start, end).trim()}${suffix}`; } export function searchDocsEntries(entries, { query, section, limit, includeSnippets, }) { const tokens = tokenize(query); const normalizedSection = section?.trim().toLowerCase(); if (tokens.length === 0) { return []; } const requiredTokens = distinctiveTokens(tokens); return entries .filter(entry => normalizedSection ? entry.section.toLowerCase().includes(normalizedSection) : true) .map(entry => { const titleScore = countMatches(entry.title, tokens) * 8; const sectionScore = countMatches(entry.section, tokens) * 4; const summaryScore = countMatches(entry.summary ?? "", tokens) * 3; const contentScore = countMatches(entry.content ?? "", tokens); const distinctiveScore = countMatches(entry.title, requiredTokens) * 10 + countMatches(entry.section, requiredTokens) * 4 + countMatches(entry.summary ?? "", requiredTokens) * 3 + countMatches(entry.content ?? "", requiredTokens); const exactTitleBonus = entry.title .toLowerCase() .includes(query.toLowerCase()) ? 12 : 0; const hasDistinctiveMatch = distinctiveScore > 0; const score = hasDistinctiveMatch ? titleScore + sectionScore + summaryScore + contentScore + distinctiveScore + exactTitleBonus : 0; return { entry, score, snippet: includeSnippets ? createSnippet(entry, tokens) : undefined, }; }) .filter(result => result.score > 0) .sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title)) .slice(0, limit); }