import { existsSync, readFileSync, readdirSync } from "node:fs"; import { extname, join, relative, resolve } from "node:path"; export interface McpTool { name: string; description: string; inputSchema: { type: "object"; properties?: Record; additionalProperties?: boolean; }; } export interface McpServerOptions { maxFiles?: number; maxFileBytes?: number; fetch?: typeof fetch; devServerUrl?: string; runtimeErrors?: () => unknown[] | Promise; } export interface McpServer { tools(): McpTool[]; call(name: string, args?: Record): Promise; handle(message: unknown): Promise | null>; } const TOOLS: McpTool[] = [ { name: "wrnexus_routes", description: "List current application routes and source files.", inputSchema: { type: "object", additionalProperties: false }, }, { name: "wrnexus_components", description: "List components with declared props and public events.", inputSchema: { type: "object", additionalProperties: false }, }, { name: "wrnexus_database_schema", description: "Read bounded database schema and migration sources.", inputSchema: { type: "object", additionalProperties: false }, }, { name: "wrnexus_diagnostics", description: "Read current compiler diagnostics and build report.", inputSchema: { type: "object", additionalProperties: false }, }, { name: "wrnexus_runtime_errors", description: "Read captured runtime errors without secrets.", inputSchema: { type: "object", additionalProperties: false }, }, { name: "wrnexus_dev_status", description: "Check development server liveness and readiness.", inputSchema: { type: "object", additionalProperties: false }, }, { name: "wrnexus_docs", description: "List framework documentation or read one documentation file.", inputSchema: { type: "object", properties: { file: { type: "string" } }, additionalProperties: false, }, }, { name: "wrnexus_packages", description: "List installed WRNexus packages and versions.", inputSchema: { type: "object", additionalProperties: false }, }, ]; function inside(root: string, path: string): boolean { const rel = relative(root, path); return rel === "" || (!rel.startsWith("..") && !resolve(rel).startsWith("..")); } function walk( root: string, directories: string[], options: Required>, extensions?: string[], ) { const output: Array<{ file: string; content: string }> = []; const visit = (directory: string) => { if (!existsSync(directory) || output.length >= options.maxFiles) return; for (const entry of readdirSync(directory, { withFileTypes: true })) { if ( output.length >= options.maxFiles || entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" ) continue; const path = join(directory, entry.name); if (entry.isDirectory()) visit(path); else if (!extensions || extensions.includes(extname(entry.name))) output.push({ file: relative(root, path).replace(/\\/g, "/"), content: readFileSync(path, "utf8").slice(0, options.maxFileBytes), }); } }; for (const directory of directories) visit(resolve(root, directory)); return output; } function routePath(file: string) { const value = file .replace(/^app\/(pages|api)\//, "") .replace(/\.(wrn|tsx?|jsx?)$/, "") .replace(/\/index$/, "") .replace(/\[\.\.\.(\w+)\]/g, "*$1") .replace(/\[(\w+)\]/g, ":$1"); if (value === "index") return "/"; return `/${value}`.replace(/\/$/, "") || "/"; } function componentContract(content: string) { return { props: [...content.matchAll(/^\s*prop\s+([A-Za-z_$][\w$]*)/gm)].map((match) => match[1]), events: [...content.matchAll(/^\s*(?:event|emit)\s+([A-Za-z_$][\w$.-]*)/gm)].map( (match) => match[1], ), }; } function redact(value: unknown): unknown { if (Array.isArray(value)) return value.map(redact); if (value && typeof value === "object") return Object.fromEntries( Object.entries(value).map(([key, item]) => [ /token|secret|password|authorization|cookie/i.test(key) ? key : key, /token|secret|password|authorization|cookie/i.test(key) ? "[REDACTED]" : redact(item), ]), ); return typeof value === "string" ? value.slice(0, 4_000) : value; } export function createFrameworkMcpServer( appRoot: string, options: McpServerOptions = {}, ): McpServer { const root = resolve(appRoot); const limits = { maxFiles: options.maxFiles ?? 200, maxFileBytes: options.maxFileBytes ?? 128 * 1024, }; const files = (directories: string[], extensions?: string[]) => walk(root, directories, limits, extensions); const call = async (name: string, args: Record = {}): Promise => { if (name === "wrnexus_routes") return files(["app/pages", "app/api"], [".wrn", ".ts", ".tsx", ".js", ".jsx"]) .map(({ file }) => ({ path: routePath(file), kind: file.startsWith("app/api/") ? "api" : "page", file, })) .sort((left, right) => left.path.localeCompare(right.path)); if (name === "wrnexus_components") return files(["app/components"], [".wrn"]).map(({ file, content }) => ({ name: file .split("/") .at(-1)! .replace(/\.wrn$/, ""), file, ...componentContract(content), })); if (name === "wrnexus_database_schema") return files(["app/db"], [".sql", ".ts", ".json"]); if (name === "wrnexus_diagnostics") return files([".wrnexus", "dist"], [".json"]) .filter((entry) => /diagnostic|report|contract/i.test(entry.file)) .map((entry) => ({ file: entry.file, data: (() => { try { return redact(JSON.parse(entry.content)); } catch { return entry.content; } })(), })); if (name === "wrnexus_runtime_errors") return redact((await options.runtimeErrors?.()) ?? []); if (name === "wrnexus_dev_status") { const base = (options.devServerUrl ?? "http://localhost:3000").replace(/\/$/, ""); const check = async (path: string) => { try { const response = await (options.fetch ?? fetch)(`${base}${path}`); return { status: response.status, ok: response.ok }; } catch (error) { return { status: 0, ok: false, error: error instanceof Error ? error.message : "Unavailable", }; } }; return { url: base, liveness: await check("/healthz"), readiness: await check("/readyz") }; } if (name === "wrnexus_docs") { const docs = files(["docs"], [".md"]); if (typeof args.file !== "string") return docs.map(({ file }) => file); const path = resolve(root, args.file); if (!inside(root, path) || !path.startsWith(resolve(root, "docs")) || !existsSync(path)) throw new Error( "WRN-MCP-DOC-PATH: documentation file is outside the application docs directory.", ); return { file: relative(root, path).replace(/\\/g, "/"), content: readFileSync(path, "utf8").slice(0, limits.maxFileBytes), }; } if (name === "wrnexus_packages") { const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { dependencies?: Record; devDependencies?: Record; }; return Object.entries({ ...manifest.dependencies, ...manifest.devDependencies }) .filter(([packageName]) => packageName.startsWith("@wrnexus/")) .map(([packageName, version]) => ({ name: packageName, version })) .sort((a, b) => a.name.localeCompare(b.name)); } throw new Error(`WRN-MCP-TOOL: unknown tool '${name}'.`); }; return { tools: () => TOOLS.map((tool) => ({ ...tool })), call, async handle(message) { if (!message || typeof message !== "object") return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request" } }; const request = message as { id?: unknown; method?: unknown; params?: any }; if (request.method === "notifications/initialized") return null; if (request.method === "initialize") return { jsonrpc: "2.0", id: request.id ?? null, result: { protocolVersion: "2025-03-26", capabilities: { tools: { listChanged: false } }, serverInfo: { name: "wrnexus", version: "0.8.0" }, }, }; if (request.method === "tools/list") return { jsonrpc: "2.0", id: request.id ?? null, result: { tools: TOOLS } }; if (request.method === "tools/call") { try { const value = await call( String(request.params?.name ?? ""), request.params?.arguments ?? {}, ); return { jsonrpc: "2.0", id: request.id ?? null, result: { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: { value }, }, }; } catch (error) { return { jsonrpc: "2.0", id: request.id ?? null, result: { isError: true, content: [ { type: "text", text: error instanceof Error ? error.message : "Tool failed" }, ], }, }; } } return { jsonrpc: "2.0", id: request.id ?? null, error: { code: -32601, message: "Method not found" }, }; }, }; }