Files
WRNexusJS/packages/mcp/src/index.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

276 lines
9.6 KiB
TypeScript

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<string, unknown>;
additionalProperties?: boolean;
};
}
export interface McpServerOptions {
maxFiles?: number;
maxFileBytes?: number;
fetch?: typeof fetch;
devServerUrl?: string;
runtimeErrors?: () => unknown[] | Promise<unknown[]>;
}
export interface McpServer {
tools(): McpTool[];
call(name: string, args?: Record<string, unknown>): Promise<unknown>;
handle(message: unknown): Promise<Record<string, unknown> | 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<Pick<McpServerOptions, "maxFiles" | "maxFileBytes">>,
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<string, unknown> = {}): Promise<unknown> => {
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<string, string>;
devDependencies?: Record<string, string>;
};
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" },
};
},
};
}