#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import FlexSearch from "flexsearch";
//#region server/lib/doc-id.ts
var InvalidDocIdError = class extends Error {
	constructor(id, reason) {
		super(`Invalid doc id "${id}": ${reason}`);
		this.name = "InvalidDocIdError";
	}
};
function decodeSegment(segment, originalId) {
	try {
		return decodeURIComponent(segment);
	} catch {
		throw new InvalidDocIdError(originalId, `segment "${segment}" contains invalid encoding`);
	}
}
function assertSafeDocId(id) {
	if (typeof id !== "string") throw new InvalidDocIdError(String(id), "doc id must be a string");
	const trimmed = id.trim();
	if (!trimmed) throw new InvalidDocIdError(id, "doc id must not be empty");
	if (trimmed.startsWith("/")) throw new InvalidDocIdError(id, "leading slash is not allowed");
	const segments = trimmed.split("/");
	for (const segment of segments) {
		if (!segment.length) throw new InvalidDocIdError(id, "empty path segment is not allowed");
		const decoded = decodeSegment(segment, id);
		if (decoded === "." || decoded === "..") throw new InvalidDocIdError(id, "dot segments are not allowed");
		if (decoded.includes("/") || decoded.includes("\\")) throw new InvalidDocIdError(id, "slash characters in path segments are not allowed");
		if (decoded.includes("\0")) throw new InvalidDocIdError(id, "null bytes are not allowed");
	}
	return trimmed;
}
function resolveInsideRoot(root, relativePath) {
	const rootPath = path.resolve(root);
	const resolved = path.resolve(rootPath, relativePath);
	const relative = path.relative(rootPath, resolved);
	if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Resolved path escapes root: root="${rootPath}" relative="${relativePath}"`);
	return resolved;
}
//#endregion
//#region server/lib/load-compiled.ts
/**
* Runtime loading of pre-compiled documentation data.
*
* Expects build artifacts in distRoot:
* - doc-metadata.json (metadata for index + resource registry)
* - search-synonyms.json
* - version.json (optional)
* - docs/*.md
*/
var BuildArtifactError = class extends Error {
	constructor(missingFile, distRoot) {
		super(`Build artifact missing: ${missingFile}\nExpected at: ${path.join(distRoot, missingFile)}\n\nRun "npm run build:server" to compile documentation first.\nIf you installed this package via npm, this is a packaging error.`);
		this.name = "BuildArtifactError";
	}
};
function readRequiredJson(distRoot, filename) {
	const filePath = path.join(distRoot, filename);
	if (!existsSync(filePath)) throw new BuildArtifactError(filename, distRoot);
	try {
		return JSON.parse(readFileSync(filePath, "utf8"));
	} catch (error) {
		throw new Error(`Failed to parse ${filename}: ${error instanceof Error ? error.message : error}`);
	}
}
function loadCompiledData(distRoot) {
	const metadata = readRequiredJson(distRoot, "doc-metadata.json");
	const synonyms = readRequiredJson(distRoot, "search-synonyms.json");
	let version = null;
	const versionPath = path.join(distRoot, "version.json");
	if (existsSync(versionPath)) try {
		version = JSON.parse(readFileSync(versionPath, "utf8"));
	} catch {}
	return {
		metadata,
		synonyms,
		version
	};
}
function loadMarkdown(distRoot, id) {
	const safeId = assertSafeDocId(id);
	const fullPath = resolveInsideRoot(path.join(distRoot, "docs"), `${safeId}.md`);
	const mdPath = path.relative(distRoot, fullPath);
	if (!existsSync(fullPath)) throw new Error(`Markdown file not found: ${mdPath}\nExpected at: ${fullPath}\nThis may indicate an incomplete build. Run "npm run build:server" to regenerate.`);
	return readFileSync(fullPath, "utf8");
}
//#endregion
//#region server/lib/search-index.ts
/**
* Build search index from pre-compiled metadata entries.
* The boost field is already computed at build time.
*/
function buildSearchIndex(entries) {
	const index = new FlexSearch.Document({
		document: {
			id: "id",
			index: [
				{
					field: "title",
					tokenize: "forward"
				},
				{
					field: "lead",
					tokenize: "forward"
				},
				{
					field: "summary",
					tokenize: "forward"
				},
				{
					field: "searchText",
					tokenize: "forward"
				},
				{
					field: "boost",
					tokenize: "forward"
				}
			],
			store: [
				"id",
				"title",
				"summary",
				"category",
				"section"
			]
		},
		tokenize: "forward"
	});
	for (const entry of entries) index.add({
		id: entry.id,
		title: entry.indexTitle ?? entry.title,
		lead: entry.lead ?? "",
		summary: entry.summary,
		searchText: entry.searchText,
		boost: entry.boost,
		category: entry.category ?? "",
		section: entry.section ?? ""
	});
	const search = (query, options) => {
		return index.search(query, options).map((group) => ({
			field: group.field,
			result: (group.result ?? []).map((id) => String(id))
		}));
	};
	return { search };
}
//#endregion
//#region server/lib/search-docs.ts
function normaliseKey(value) {
	return value.toLowerCase().trim();
}
function slugifyHeading(value) {
	return normaliseKey(value).replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
function compactText(value) {
	return value.replace(/\s+/g, " ").trim();
}
function stripMarkdownForSearch(markdown) {
	return compactText(markdown.replace(/```[\s\S]*?```/g, " ").replace(/^#{1,6}\s+/gm, "").replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/__([^_]+)__|_([^_]+)_/g, (_, bold, italic) => bold ?? italic ?? "").replace(/<[^>]+>/g, " ").replace(/[[\]()*_>#|]/g, " "));
}
function excerpt(value, maxLength = 240) {
	const text = compactText(value);
	if (text.length <= maxLength) return text;
	return `${text.slice(0, maxLength - 1).trimEnd()}…`;
}
function extractSecondLevelMarkdownSections(markdown) {
	const sections = [];
	const lines = markdown.split(/\r?\n/);
	let current = null;
	for (const line of lines) {
		const match = /^##\s+(.+?)\s*$/.exec(line);
		if (match) {
			if (current) sections.push({
				heading: current.heading,
				body: current.bodyLines.join("\n").trim()
			});
			current = {
				heading: match[1].trim(),
				bodyLines: []
			};
			continue;
		}
		if (current) current.bodyLines.push(line);
	}
	if (current) sections.push({
		heading: current.heading,
		body: current.bodyLines.join("\n").trim()
	});
	return sections;
}
function expandDomainSynonyms(rawQuery, config) {
	const query = normaliseKey(rawQuery);
	const expansions = /* @__PURE__ */ new Set();
	const preferred = /* @__PURE__ */ new Set();
	const hints = /* @__PURE__ */ new Set();
	const synonyms = config.synonyms ?? {};
	const preferredDocIds = config.preferredDocIds ?? {};
	const allKeys = new Set([
		...Object.keys(synonyms),
		...Object.keys(preferredDocIds),
		...Object.keys(config.hints ?? {})
	]);
	for (const key of allKeys) {
		if (!query.includes(normaliseKey(key))) continue;
		for (const value of synonyms[key] ?? []) if (normaliseKey(value) !== query) expansions.add(value);
		for (const id of preferredDocIds[key] ?? []) preferred.add(id);
		if (config.hints?.[key]) hints.add(config.hints[key]);
	}
	return {
		expansions: Array.from(expansions),
		preferredDocIds: Array.from(preferred),
		hints: Array.from(hints)
	};
}
function buildFoundationSectionAliases(foundation, markdown) {
	if (!foundation || !markdown) return [];
	return extractSecondLevelMarkdownSections(markdown).map((section) => {
		const slug = slugifyHeading(section.heading);
		if (!slug) return null;
		const plainText = stripMarkdownForSearch(section.body);
		const guidance = `Matched section: "${section.heading}". After reading this resource, use the "## ${section.heading}" section; ignore unrelated Foundations sections unless they are needed for context.`;
		const summary = plainText ? `${guidance} Section excerpt: ${excerpt(plainText)}` : guidance;
		return {
			id: `foundations#${slug}`,
			resourceDocId: foundation.id,
			title: `Foundations: ${section.heading}`,
			indexTitle: section.heading,
			lead: null,
			summary,
			searchText: [section.heading, plainText].filter(Boolean).join("\n"),
			category: foundation.category,
			section: foundation.section,
			boost: [
				section.heading,
				`foundations ${section.heading}`,
				`#foundations/${slug}`
			].join(" "),
			matchedHeading: section.heading
		};
	}).filter((entry) => Boolean(entry));
}
function buildSearchEntries(metadata, options = {}) {
	const foundation = metadata.find((entry) => entry.id === "foundations");
	return [...metadata, ...buildFoundationSectionAliases(foundation, options.foundationMarkdown ?? null)];
}
function resolveSearchDocs(options) {
	const { query, limit, index, entriesById, synonymsConfig, resourceForDocId } = options;
	const searchQueue = [query];
	const domainSynonyms = expandDomainSynonyms(query, synonymsConfig);
	for (const extra of domainSynonyms.expansions) searchQueue.push(extra);
	const seen = /* @__PURE__ */ new Set();
	const results = [];
	const normalizedQuery = normaliseKey(query);
	function addResult(id) {
		if (seen.has(id)) return results.length >= limit;
		const entry = entriesById.get(id);
		if (!entry) return false;
		seen.add(id);
		const resourceDocId = entry.resourceDocId ?? entry.id;
		results.push({
			resource: resourceForDocId(resourceDocId),
			title: entry.title,
			summary: entry.summary,
			category: entry.category,
			section: entry.section,
			matchedHeading: entry.matchedHeading
		});
		return results.length >= limit;
	}
	if (/\bfoundations?\b/.test(normalizedQuery)) for (const entry of entriesById.values()) {
		if (!entry.matchedHeading) continue;
		if (!normalizedQuery.includes(normaliseKey(entry.matchedHeading))) continue;
		if (addResult(entry.id)) return {
			results,
			domainSynonyms
		};
	}
	for (const id of domainSynonyms.preferredDocIds) if (addResult(id)) return {
		results,
		domainSynonyms
	};
	for (const searchQuery of searchQueue) {
		const hits = index.search(searchQuery, {
			limit: Math.max(limit * 2, 10),
			suggest: true
		});
		for (const group of hits) for (const id of group.result ?? []) if (addResult(String(id))) return {
			results,
			domainSynonyms
		};
	}
	return {
		results,
		domainSynonyms
	};
}
//#endregion
//#region package.json
var version = "1.1.11";
//#endregion
//#region server/index.ts
const DIST_ROOT = import.meta.dirname;
const { metadata, synonyms, version: capturedVersionInfo } = loadCompiledData(DIST_ROOT);
const CAPTURED_UIKIT_VERSION = capturedVersionInfo?.version ?? null;
const server = new McpServer({
	name: "uikit-mcp-server",
	version
}, { instructions: `UI Kit MCP usage guidelines:
- Always discover documentation with search_docs; listResources is a static inventory and must not be used for guessing. Call readResource on the returned uikit-doc:// URI before using anything.
- Coding rules: import components from '@rio-cloud/rio-uikit', prefer styling via provided classes instead of custom CSS files, and wrap the app/content in ApplicationLayout.
- Follow documented props tables and examples; avoid inventing undocumented variants.
- Treat the captured metadata (Category, Section, Captured) as the source of truth.
- Captured UI Kit version: ${CAPTURED_UIKIT_VERSION ?? "unknown"}` });
function buildResourceUri(docId) {
	return new URL(`uikit-doc://${docId}`);
}
function registerDocResources(docs) {
	for (const doc of docs) {
		const safeId = assertSafeDocId(doc.id);
		const uri = buildResourceUri(safeId);
		server.registerResource(safeId, uri.href, {
			title: doc.title,
			description: doc.summary,
			mimeType: "text/markdown"
		}, async () => {
			const markdown = loadMarkdown(DIST_ROOT, safeId);
			const versionNote = CAPTURED_UIKIT_VERSION ? `\n\n> Captured UI Kit version: ${CAPTURED_UIKIT_VERSION}` : "";
			return { contents: [{
				uri: uri.href,
				mimeType: "text/markdown",
				text: markdown + versionNote
			}] };
		});
	}
	console.error(`Registered ${docs.length} documentation resources.`);
}
function registerSearchTool(index, entriesMap, synonymsConfig) {
	const inputSchema = {
		query: z.string().min(1, "Query is required"),
		limit: z.number().int().min(1).max(20).optional()
	};
	const outputSchema = { results: z.array(z.object({
		resource: z.string(),
		title: z.string(),
		summary: z.string().optional(),
		category: z.string().nullish(),
		section: z.string().nullish(),
		matchedHeading: z.string().optional()
	})) };
	server.registerTool("search_docs", {
		title: "Search UI Kit docs",
		description: "Full-text search across the captured UI Kit documentation. Always call this before readResource; it is the only supported way to discover resource IDs.",
		inputSchema,
		outputSchema,
		annotations: {
			readOnlyHint: true,
			idempotentHint: true,
			destructiveHint: false,
			openWorldHint: false
		}
	}, async ({ query, limit = 5 }) => {
		const { results: resolved, domainSynonyms } = resolveSearchDocs({
			query,
			limit,
			index,
			entriesById: entriesMap,
			synonymsConfig,
			resourceForDocId: (docId) => buildResourceUri(docId).href
		});
		let text = resolved.length > 0 ? resolved.map((entry, idx) => `${idx + 1}. ${entry.title} (${entry.resource})${entry.summary ? `\n   ${entry.summary}` : ""}`).join("\n\n").concat("\n\nAlways call readResource on one of the URIs above instead of guessing component routes.") : `No matches for "${query}".`;
		const uniqueHints = Array.from(new Set(domainSynonyms.hints.filter(Boolean)));
		if (uniqueHints.length) text += `\n\n${uniqueHints.join("\n")}`;
		if (CAPTURED_UIKIT_VERSION) text += `\n\nCaptured UI Kit version: ${CAPTURED_UIKIT_VERSION}. Read "How to use the UIKIT" → "Install the npm package" for installation details.`;
		return {
			structuredContent: { results: resolved },
			content: [{
				type: "text",
				text
			}]
		};
	});
}
async function main() {
	let foundationMarkdown = null;
	try {
		foundationMarkdown = loadMarkdown(DIST_ROOT, "foundations");
	} catch {
		foundationMarkdown = null;
	}
	const searchEntries = buildSearchEntries(metadata, { foundationMarkdown });
	const searchEntriesById = new Map(searchEntries.map((entry) => [entry.id, entry]));
	const index = buildSearchIndex(searchEntries);
	registerDocResources(metadata);
	registerSearchTool(index, searchEntriesById, synonyms);
	const docIds = new Set(metadata.map((m) => m.id));
	const missingPreferred = /* @__PURE__ */ new Set();
	for (const ids of Object.values(synonyms.preferredDocIds ?? {})) for (const id of ids ?? []) if (!docIds.has(id)) missingPreferred.add(id);
	if (missingPreferred.size) console.warn(`Warning: preferredDocIds not found in dataset: ${Array.from(missingPreferred).join(", ")}`);
	const transport = new StdioServerTransport();
	await server.connect(transport);
}
main().catch((error) => {
	console.error(error);
	process.exit(1);
});
//#endregion
export {};
