release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+11
View File
@@ -0,0 +1,11 @@
# @wrnexus/mcp
Editor-neutral Model Context Protocol server for AI development tools.
```bash
bunx wrnexus-mcp --root=.
```
It exposes current routes, components with props/events, database schema files, compiler diagnostics,
runtime errors, dev-server health, framework documentation and installed packages. Files are resolved
inside the configured application root and returned as bounded structured JSON.
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@wrnexus/mcp",
"version": "0.8.0",
"type": "module",
"description": "Model Context Protocol server exposing WRNexus application and framework context.",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./stdio": "./src/stdio.ts"
},
"bin": {
"wrnexus-mcp": "src/stdio.ts"
}
}
+275
View File
@@ -0,0 +1,275 @@
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" },
};
},
};
}
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bun
import { createInterface } from "node:readline";
import { createFrameworkMcpServer } from "./index.ts";
export async function runMcpStdio(root = process.cwd()): Promise<void> {
const server = createFrameworkMcpServer(root);
const lines = createInterface({ input: process.stdin, terminal: false });
for await (const line of lines) {
if (!line.trim()) continue;
let message: unknown;
try {
message = JSON.parse(line);
} catch {
process.stdout.write(
`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } })}\n`,
);
continue;
}
const response = await server.handle(message);
if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
}
}
if (import.meta.main) {
const root = process.argv.find((value) => value.startsWith("--root="))?.slice(7) ?? process.cwd();
await runMcpStdio(root);
}
+80
View File
@@ -0,0 +1,80 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createFrameworkMcpServer } from "../src/index.ts";
function fixture() {
const root = mkdtempSync(join(tmpdir(), "wrnexus-mcp-"));
for (const dir of ["app/pages", "app/api", "app/components", "app/db/migrations", "docs"])
mkdirSync(join(root, dir), { recursive: true });
writeFileSync(join(root, "app/pages/index.wrn"), "<h1>Home</h1>");
writeFileSync(join(root, "app/api/users.ts"), "export const GET = () => Response.json([])");
writeFileSync(
join(root, "app/components/Button.wrn"),
"prop label: string\nevent click\n<button>{label}</button>",
);
writeFileSync(join(root, "app/db/migrations/001.sql"), "CREATE TABLE users(id TEXT);");
writeFileSync(join(root, "docs/GUIDE.md"), "# Guide");
writeFileSync(
join(root, "package.json"),
JSON.stringify({ dependencies: { "@wrnexus/core": "0.8.0", other: "1.0.0" } }),
);
return root;
}
describe("WRNexus MCP", () => {
test("exposes application framework context through bounded tools", async () => {
const server = createFrameworkMcpServer(fixture(), {
fetch: (async (input: URL | RequestInfo) =>
new Response(null, {
status: String(input).endsWith("readyz") ? 503 : 200,
})) as unknown as typeof fetch,
runtimeErrors: () => [{ message: "bad", token: "secret" }],
});
expect(((await server.call("wrnexus_routes")) as any[]).map((route) => route.path)).toEqual([
"/",
"/users",
]);
expect(await server.call("wrnexus_components")).toEqual([
{ name: "Button", file: "app/components/Button.wrn", props: ["label"], events: ["click"] },
]);
expect(((await server.call("wrnexus_database_schema")) as any[])[0].content).toContain(
"CREATE TABLE",
);
expect(await server.call("wrnexus_packages")).toEqual([
{ name: "@wrnexus/core", version: "0.8.0" },
]);
expect(await server.call("wrnexus_runtime_errors")).toEqual([
{ message: "bad", token: "[REDACTED]" },
]);
expect(await server.call("wrnexus_dev_status")).toMatchObject({
liveness: { ok: true },
readiness: { ok: false },
});
});
test("speaks MCP JSON-RPC initialize, list and call", async () => {
const server = createFrameworkMcpServer(fixture());
expect(await server.handle({ jsonrpc: "2.0", id: 1, method: "initialize" })).toHaveProperty(
"result.serverInfo.name",
"wrnexus",
);
expect(await server.handle({ jsonrpc: "2.0", id: 2, method: "tools/list" })).toHaveProperty(
"result.tools.0.name",
"wrnexus_routes",
);
const result = await server.handle({
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: { name: "wrnexus_docs", arguments: {} },
});
expect(result).toHaveProperty("result.structuredContent.value.0", "docs/GUIDE.md");
});
test("rejects documentation traversal", async () => {
const server = createFrameworkMcpServer(fixture());
await expect(server.call("wrnexus_docs", { file: "../secret" })).rejects.toThrow(
"WRN-MCP-DOC-PATH",
);
});
});