@rsc-kit/mcp
Version:
An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.
56 lines • 2.63 kB
JavaScript
// Reading what the build wrote down.
//
// Everything this server answers comes from one file: `build-report.json`, in
// the build's own output directory. Nothing here runs a build, imports the
// app, or re-derives the route tree — the build already decided all of this,
// and a second implementation would be a second thing that can be wrong.
//
// The consequence to be honest about: an answer is only as fresh as the last
// build. So every answer says when it was built, and a missing report says
// "run a build" rather than "there are no routes".
import { existsSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
/** Where a build leaves its report, in the order worth looking. */
const LIKELY = [".rsc-kit", "build", ".rsc", "dist", ".output"];
export class NoReport extends Error {
constructor(root) {
super(`No build report under ${root}. This server answers from what the last build ` +
"decided, so there has to have been one — run the build and ask again.");
this.name = "NoReport";
}
}
/** The report, and how old it is. */
export function loadReport(root) {
const base = resolve(root);
for (const dir of LIKELY) {
const file = join(base, dir, "build-report.json");
if (!existsSync(file))
continue;
return {
report: JSON.parse(readFileSync(file, "utf-8")),
// The file's own mtime rather than a timestamp inside it: a stamp written
// into the file changes the file on every build even when nothing else
// did, which defeats every cache keyed on its contents.
builtAt: statSync(file).mtime,
from: file,
};
}
throw new NoReport(base);
}
/** What each classification means, in one line, for an answer that has to stand alone. */
export const MEANING = {
frozen: "stored whole at build time and served as a file",
shell: "a stored shell, with the rest rendered per request",
blocked: "REFUSED — nothing could paint before it read the request, so the build did not finish",
dynamic: "answered per request",
error: "failed to render",
};
/** A route by url, tolerating a trailing slash either way. */
export function routeFor(report, url) {
const wanted = (url.split("?")[0].replace(/\/+$/, "") || "/").toLowerCase();
const matches = (candidate) => (candidate.replace(/\/+$/, "") || "/").toLowerCase() === wanted;
return (report.routes.find((r) => matches(r.url)) ??
report.apis.find((a) => matches(a.url)) ??
null);
}
//# sourceMappingURL=report.js.map