@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.
133 lines (99 loc) • 5.51 kB
Markdown
# Getting started
> Serve React Server Components from any JavaScript backend.
A host is a `Request` in and a `Response` out. You do not normally write the
server that calls it: [Nitro](https://nitro.build) builds one around your route
tree, and where it runs is a preset in `vite.config.ts`.
## Building
Three steps, and only the first is required:
```bash
npm run dev # vite — serves from source, no build step
npm run build # bundles, renders every route once, then Nitro assembles .output/
npm run start # runs .output/server/index.mjs
```
`dev` is Vite's own dev server, and edits reach the browser without a reload:
| you edit | what happens |
| --- | --- |
| a client component | Fast Refresh — the code updates and its state survives |
| a page or layout | the payload is re-fetched and the tree re-rendered |
| adding or deleting a page | the server restarts to pick up the new route table |
The browser never holds a server component, so Vite cannot hot-swap it. Instead
the client re-fetches the page — which remounts any client component below, so
their state resets.
Edit a client component directly and its state does survive; that is Fast
Refresh.
The restart on a new page is because the route tree is read when the server
starts: a page that appears later would otherwise 404 while sitting right there
on disk.
`build` ends by rendering every route once and storing what it can, which is
what turns a route into a file on disk instead of a render per request.
It is part of `build` rather than a separate command because forgetting it costs
you everything and looks like nothing — every page still works, each one just
renders again for every visitor.
A page the build machine cannot render says so with `await connection()`
and renders per request; there is no switch for the whole app. See [Static
generation](/guides/static-generation).
<Aside type="note" title="There is no NODE_ENV to set">
React picks its build from it, and getting it wrong gives you a page that
renders perfectly and never hydrates. The build stamps the mode it ran in
into the server bundles, so a server is production because it was built that
way — `npm run start` needs no environment at all.
</Aside>
## Compiling to a single binary
With the Bun preset the whole application ends up in one file:
```bash
npm run compile # builds, then bun build --compile --splitting
./dist/app
```
That works because the generated config sets `serveStatic: 'inline'`. Without
it the binary compiles, starts, serves pages, and 404s every asset — the static
path resolves into Bun's virtual filesystem, where the files on disk are not.
### What it costs
Measured on the example — eleven routes, 240 KB of assets, 164 KB of stored
pages:
| | compiled | run from `.output/` |
| --- | --- | --- |
| Cold start | 45 ms | 56–65 ms |
| Resident, idle | 18 MB | 19 MB |
| Resident, after 300 requests | 27 MB | 32 MB |
| On disk | 62 MB | 240 KB + a runtime |
Memory is flat between the two: the binary holds its assets as `Response`
objects built at boot, which for 240 KB is nothing, and both settle in the high
twenties once React has warmed up. A live render is what moves it — 40 MB after
200 of them — and that is the renderer, not the packaging.
<Aside type="note" title="What compiling is actually for">
Deployment shape, not speed. One file, no `node_modules`, no runtime to
install. Throughput between the two is inside the noise of any benchmark I
could run on one machine — the only differences I can defend are the ~15 ms
of cold start and the shape of what you ship.
</Aside>
<Aside type="caution" title="What embedding costs">
Every asset goes in whole. Three files is nothing; a media-heavy app is
hundreds of megabytes of executable, with no CDN in front and no streaming
from disk. Keep large or rarely-read files outside the binary and serve them
from wherever they already live.
Frozen pages are not embedded either. The build writes them to
`.output/server/rsc-static` and the server reads them from beside itself; a
binary has no filesystem to read, so it renders those pages live. Everything
still answers — what you lose is the stored render, not the page.
</Aside>
## If you work with an AI agent
The scaffold writes an `AGENTS.md` beside your `README.md`. Claude Code, Cursor
and the rest read it, and it covers the things an agent otherwise gets wrong
from React or Next habits — `"use client"` versus `"use server"`, where an
authorisation check belongs, which props are async, and that the build output is
worth reading rather than ignoring.
Beside it is `.mcp.json`, which connects the [MCP server](/guides/mcp): Claude
Code asks you to approve it on first use, and from then on an agent can read
what your last build actually did instead of guessing.
And there is a test to extend, `tests/app.test.ts`, which goes through the
real build: `bun run check` runs typecheck, lint and tests together, and is
the one command an agent — or you — runs before calling something done.
Edit it as your project grows. It is yours; nothing regenerates it.
There is also an MCP server, which answers from your actual build rather than
from memory:
```sh
claude mcp add rsc-kit -- npx -y @rsc-kit/mcp
```
It can say why a particular page is not static, what each route ships to the
browser, and how to build a form or an api route the way this framework
expects. See [Working with an AI agent](/guides/mcp/).