const DOCS_BASE_URL = "https://www.reacticx.com/docs/components";
const cache = new Map<string, { content: string; timestamp: number }>();
const CACHE_TTL = 1000 * 60 * 30; // 30 minutes
function htmlToMarkdown(html: string): string {
let text = html;
// Remove script and style tags with content
text = text.replace(/<script[\s\S]*?<\/script>/gi, "");
text = text.replace(/<style[\s\S]*?<\/style>/gi, "");
text = text.replace(/<nav[\s\S]*?<\/nav>/gi, "");
text = text.replace(/<footer[\s\S]*?<\/footer>/gi, "");
text = text.replace(/<header[\s\S]*?<\/header>/gi, "");
// Extract the main content area if possible
const mainMatch = text.match(
/<main[\s\S]*?>([\s\S]*?)<\/main>/i
);
const articleMatch = text.match(
/<article[\s\S]*?>([\s\S]*?)<\/article>/i
);
if (articleMatch) {
text = articleMatch[1];
} else if (mainMatch) {
text = mainMatch[1];
}
// Convert headings
text = text.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n");
text = text.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n");
text = text.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n");
text = text.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, "\n#### $1\n");
// Convert code blocks - handle <pre><code> combinations
text = text.replace(
/<pre[^>]*>\s*<code[^>]*class="[^"]*language-(\w+)"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi,
"\n```$1\n$2\n```\n"
);
text = text.replace(
/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi,
"\n```\n$1\n```\n"
);
text = text.replace(
/<pre[^>]*>([\s\S]*?)<\/pre>/gi,
"\n```\n$1\n```\n"
);
// Convert inline code
text = text.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
// Convert bold and italic
text = text.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
text = text.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
// Convert links
text = text.replace(
/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi,
"[$2]($1)"
);
// Convert list items
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n");
text = text.replace(/<\/?[ou]l[^>]*>/gi, "\n");
// Convert table elements
text = text.replace(/<tr[^>]*>([\s\S]*?)<\/tr>/gi, "$1|\n");
text = text.replace(/<th[^>]*>([\s\S]*?)<\/th>/gi, "| $1 ");
text = text.replace(/<td[^>]*>([\s\S]*?)<\/td>/gi, "| $1 ");
text = text.replace(/<\/?table[^>]*>/gi, "\n");
text = text.replace(/<\/?thead[^>]*>/gi, "");
text = text.replace(/<\/?tbody[^>]*>/gi, "");
// Convert paragraphs and breaks
text = text.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "\n$1\n");
text = text.replace(/<br\s*\/?>/gi, "\n");
text = text.replace(/<hr\s*\/?>/gi, "\n---\n");
// Convert divs to newlines
text = text.replace(/<div[^>]*>/gi, "\n");
text = text.replace(/<\/div>/gi, "\n");
// Remove remaining HTML tags
text = text.replace(/<[^>]+>/g, "");
// Decode HTML entities
text = text.replace(/&/g, "&");
text = text.replace(/</g, "<");
text = text.replace(/>/g, ">");
text = text.replace(/"/g, '"');
text = text.replace(/'/g, "'");
text = text.replace(/ /g, " ");
text = text.replace(/&#(\d+);/g, (_, code) =>
String.fromCharCode(parseInt(code))
);
// Clean up whitespace
text = text.replace(/\n{3,}/g, "\n\n");
text = text.trim();
return text;
}
export async function fetchComponentDocs(
slug: string
): Promise<string> {
const cached = cache.get(slug);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.content;
}
const url = `${DOCS_BASE_URL}/${slug}`;
try {
const response = await fetch(url, {
headers: {
"User-Agent": "ReacticxMCP/1.0",
Accept: "text/html",
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const markdown = htmlToMarkdown(html);
const content = [
`# ${slug} - Reacticx Component Documentation`,
`Source: ${url}`,
"",
markdown,
"",
"---",
`Installation: \`bunx --bun reacticx add ${slug}\``,
].join("\n");
cache.set(slug, { content, timestamp: Date.now() });
return content;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to fetch docs for "${slug}" from ${url}: ${message}`
);
}
}
export async function fetchGettingStarted(): Promise<string> {
const cached = cache.get("__getting_started__");
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.content;
}
try {
const response = await fetch("https://www.reacticx.com/docs", {
headers: {
"User-Agent": "ReacticxMCP/1.0",
Accept: "text/html",
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const html = await response.text();
const markdown = htmlToMarkdown(html);
const content = [
"# Reacticx - Getting Started",
"Source: https://www.reacticx.com/docs",
"",
markdown,
].join("\n");
cache.set("__getting_started__", { content, timestamp: Date.now() });
return content;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch getting started docs: ${message}`);
}
}