@rsc-kit/mcp 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/guides/go.md ADDED
@@ -0,0 +1,194 @@
1
+ # Go
2
+
3
+ > A Go process behind the renderer — functions, guards and actions in Go.
4
+
5
+ Go owns the request: sessions, auth, the database. The renderer owns
6
+ rendering, because that half is React. A server component reaches Go by
7
+ calling `rpc()`, which leaves the renderer as an ordinary POST; a `middleware.ts`
8
+ names guards Go runs before a page renders; a server action the browser calls
9
+ is a Go function the build wrote a stub for.
10
+
11
+ A [BAP](/hosts/backend-answered-pages) with Go behind it. There is no
12
+ JavaScript to write for Go: the app is what the scaffold writes, and the Go
13
+ side is one package answering
14
+ [the endpoint every backend answers](/hosts/your-own-backend).
15
+
16
+ ```sh
17
+ go get github.com/rsc-kit/go
18
+ ```
19
+
20
+ ## Wiring
21
+
22
+ In a Go module, `init` sees `go.mod` and does the JavaScript half — the route
23
+ tree, `vite.config.ts`, `.env` with `RSC_BACKEND` and a generated secret, the
24
+ scripts — and prints the Go half for you to add:
25
+
26
+ ```sh
27
+ bunx rsc-kit@latest init # in the directory with go.mod
28
+ bun create rsc-kit@latest my-app --backend=http://127.0.0.1:8080 # or a new app
29
+ ```
30
+
31
+ What it wrote is two lines, read by `vite` in development and by the built
32
+ server in production:
33
+
34
+ ```ini title=".env"
35
+ RSC_BACKEND=http://127.0.0.1:8080
36
+ RSC_HOST_CALL_SECRET=a-long-random-string
37
+ ```
38
+
39
+ And the endpoint, on whatever mux you already have:
40
+
41
+ ```go title="main.go"
42
+ reg := rsckit.NewRegistry()
43
+ // … Register, Middleware, RegisterAction below …
44
+
45
+ callback, err := rsckit.NewCallbackHandler(reg, os.Getenv("RSC_HOST_CALL_SECRET"))
46
+ if err != nil {
47
+ log.Fatal(err) // refuses to exist without a secret
48
+ }
49
+
50
+ mux := http.NewServeMux()
51
+ mux.Handle("POST /__rsc/host-call", callback)
52
+ http.ListenAndServe("127.0.0.1:8080", mux)
53
+ ```
54
+
55
+ The renderer forwards any url its route tree does not own to `RSC_BACKEND`,
56
+ so a Go route on that mux — `/login`, a webhook, an upload — is reachable at
57
+ the renderer's origin too.
58
+
59
+ ## Reading from Go
60
+
61
+ ```go
62
+ reg.Register("Orders.recent", func(ctx context.Context, args rsckit.Args) (any, error) {
63
+ var limit int
64
+ if err := args.Bind(&limit); err != nil {
65
+ return nil, err
66
+ }
67
+
68
+ // The visitor's cookie, forwarded from the page request: this runs as
69
+ // them, not as nobody.
70
+ session := rsckit.HeadersFrom(ctx).Get("Cookie")
71
+
72
+ return db.RecentOrders(ctx, session, limit)
73
+ })
74
+ ```
75
+
76
+ ```tsx title="src/app/orders/page.tsx"
77
+ export default async function Orders() {
78
+ const orders = await rpc<Order[]>('Orders.recent', 5)
79
+
80
+ return <ul>{orders.map((o) => <li key={o.id}>{o.number}</li>)}</ul>
81
+ }
82
+ ```
83
+
84
+ `args.Bind` decodes the positional arguments `rpc()` was given; too few is an
85
+ error, extra ones are ignored. Whatever the function returns is encoded as
86
+ JSON and decoded on the other side as whatever `rpc<T>()` was told.
87
+
88
+ ### Refusing
89
+
90
+ Return a value and it is the result. Return one of these and the render is
91
+ told what happened, rather than handed a 500:
92
+
93
+ | return | the render gets |
94
+ | --- | --- |
95
+ | `rsckit.InvalidField("name", "…")`, `rsckit.Invalid(map)` | 422, each message under its input on the form that submitted |
96
+ | `rsckit.Unauthenticated()` | 401, the engine's own authentication error |
97
+ | `rsckit.Unauthorized("…")` | 403 |
98
+ | `rsckit.Redirect("/login")` | the browser goes there |
99
+ | `rsckit.Refuse(429, "slow down")` | that status, kept |
100
+ | any other `error` | 500, with the message |
101
+
102
+ Wrapped errors still answer as what they are — `errors.As` finds the refusal
103
+ inside `fmt.Errorf("…: %w", err)` — and a panic becomes an error for that one
104
+ call rather than taking the server down.
105
+
106
+ ## Route middleware
107
+
108
+ ```ts title="src/app/admin/middleware.ts"
109
+ export const middleware = ['auth', 'can:manage-orders', 'throttle:60,1']
110
+ ```
111
+
112
+ ```go
113
+ reg.Middleware("auth", func(ctx context.Context, _ string) error {
114
+ if !signedIn(rsckit.HeadersFrom(ctx)) {
115
+ return rsckit.Redirect("/login")
116
+ }
117
+ return nil
118
+ })
119
+
120
+ reg.Middleware("can", func(ctx context.Context, ability string) error {
121
+ if !allowed(ctx, ability) {
122
+ return rsckit.Unauthorized("you may not " + ability)
123
+ }
124
+ return nil
125
+ })
126
+ ```
127
+
128
+ The renderer sends the list before anything at or below `admin/` renders —
129
+ including a page it froze at build time, before the file is served. A guard
130
+ receives what follows the colon in its name (`"manage-orders"`, `"60,1"`,
131
+ `""`); guards run in order and stop at the first refusal. A name nothing is
132
+ registered for refuses rather than passes: a declared check that silently
133
+ does not happen is the failure this exists to prevent.
134
+
135
+ ## Server actions
136
+
137
+ ```go
138
+ reg.RegisterAction("ordersCancel", "Orders.cancel", func(ctx context.Context, args rsckit.Args) (any, error) {
139
+ var id int
140
+ if err := args.Bind(&id); err != nil {
141
+ return nil, err
142
+ }
143
+
144
+ if err := orders.Cancel(ctx, id); err != nil {
145
+ return nil, err
146
+ }
147
+
148
+ rsckit.Revalidate(ctx, "orders")
149
+
150
+ return nil, nil
151
+ })
152
+
153
+ // Before each build. The build reads it and writes the stubs.
154
+ reg.WriteActionManifest("rsc-host-actions.json")
155
+ ```
156
+
157
+ ```tsx title="src/app/orders/CancelButton.tsx"
158
+ 'use client'
159
+ import { ordersCancel } from '../../server-actions.generated'
160
+
161
+ export function CancelButton({ id }: { id: number }) {
162
+ return <button onClick={() => ordersCancel(id)}>Cancel</button>
163
+ }
164
+ ```
165
+
166
+ `Revalidate` names a [section](/guides/sections) or `page`, and the answer to
167
+ the action carries it re-rendered rather than the browser being told to ask
168
+ again. Write the manifest as part of the build — the example's `build`
169
+ script runs the Go program with `-manifest-only` first — because a stale map
170
+ names a function that has since been renamed, and nothing fails until the
171
+ browser calls it.
172
+
173
+ ## Production
174
+
175
+ `bun run build` and `bun run start` with the same two variables in the
176
+ renderer's environment. Put the renderer in front and restrict
177
+ `/__rsc/host-call` at the web server, as [Laravel's page](/hosts/laravel/#which-process-faces-the-internet)
178
+ shows; a Go worker is held for the length of a host call, never a render.
179
+
180
+ Or put Go in front: `rsckit.NewRenderer(url)` is a streaming reverse proxy
181
+ to the renderer, and `rsckit.NewHandler(renderer, callback, path)` routes
182
+ the callback path to the endpoint and everything else through it. Both sides
183
+ mark what they forward, so a url neither owns is a 404 rather than a loop.
184
+
185
+ On a Worker, the two variables are a var and a secret in `wrangler.json`,
186
+ and the Go process has to be reachable from Cloudflare's network — an
187
+ `https` origin behind the secret and, ideally, an allow-list, since loopback
188
+ is not an option there.
189
+
190
+ ---
191
+
192
+ The runnable version is
193
+ [`examples/go-backend`](https://github.com/rsc-kit/rsc-kit/tree/main/examples/go-backend)
194
+ in the repository: `bun run backend`, `bun run dev`, open the renderer.
package/guides/index.json CHANGED
@@ -9,6 +9,16 @@
9
9
  "title": "Authorization",
10
10
  "description": "Protecting pages, server actions and API routes."
11
11
  },
12
+ {
13
+ "slug": "backend-answered-pages",
14
+ "title": "Backend-Answered Pages",
15
+ "description": "BAP — the model for an rsc-kit app with a backend behind it, and how to build for it."
16
+ },
17
+ {
18
+ "slug": "bun",
19
+ "title": "Running on Bun",
20
+ "description": "What is different when the runtime is Bun — and what only looks like it is."
21
+ },
12
22
  {
13
23
  "slug": "caching",
14
24
  "title": "Asking once per request",
@@ -24,6 +34,16 @@
24
34
  "title": "Rendering per request",
25
35
  "description": "Marking work that belongs to the visitor, not to the build."
26
36
  },
37
+ {
38
+ "slug": "deployment",
39
+ "title": "Deploying",
40
+ "description": "What to ship, and the two settings that fail quietly."
41
+ },
42
+ {
43
+ "slug": "domains",
44
+ "title": "Domains and subdomains",
45
+ "description": "A host as a route segment — admin.example.com reaches app/admin, a tenant's host binds [domain] — with nothing to rewrite."
46
+ },
27
47
  {
28
48
  "slug": "edge-caching",
29
49
  "title": "Serving shells from a CDN",
@@ -59,6 +79,11 @@
59
79
  "title": "Getting started",
60
80
  "description": "Serve React Server Components from any JavaScript backend."
61
81
  },
82
+ {
83
+ "slug": "go",
84
+ "title": "Go",
85
+ "description": "A Go process behind the renderer — functions, guards and actions in Go."
86
+ },
62
87
  {
63
88
  "slug": "images",
64
89
  "title": "Images",
@@ -69,11 +94,21 @@
69
94
  "title": "Installation",
70
95
  "description": "From an empty directory to a streaming RSC app."
71
96
  },
97
+ {
98
+ "slug": "instrumentation",
99
+ "title": "Startup",
100
+ "description": "instrumentation.ts runs once, before anything else."
101
+ },
72
102
  {
73
103
  "slug": "introduction",
74
104
  "title": "Introduction",
75
105
  "description": "React Server Components as a Vite plugin, deployed wherever you like."
76
106
  },
107
+ {
108
+ "slug": "laravel",
109
+ "title": "Laravel",
110
+ "description": "React Server Components in front, a Laravel application behind them."
111
+ },
77
112
  {
78
113
  "slug": "mcp",
79
114
  "title": "Working with an AI agent",
@@ -193,5 +228,15 @@
193
228
  "slug": "view-transitions",
194
229
  "title": "View transitions",
195
230
  "description": "What React's ViewTransition animates in an app built with this, and what it does not."
231
+ },
232
+ {
233
+ "slug": "where-it-runs",
234
+ "title": "Where it runs",
235
+ "description": "A host is a Nitro preset, not a server you write."
236
+ },
237
+ {
238
+ "slug": "your-own-backend",
239
+ "title": "Your own backend",
240
+ "description": "The one endpoint a backend in any language answers."
196
241
  }
197
242
  ]
@@ -32,8 +32,9 @@ The rest of this page is for adding rsc-kit to an app you already have.
32
32
  <PackageManagers pkg="vite @vitejs/plugin-rsc @vitejs/plugin-react @types/react @types/react-dom" dev />
33
33
 
34
34
  `@rsc-kit/core` brings the Vite plugin, the render engine and the client
35
- runtime. React 19 and Vite 8 are peer dependencies — the versions the RSC build
36
- needs, not ones this package pins for you.
35
+ runtime. React 19.2 or later and Vite 8 are peer dependencies — the versions
36
+ the RSC build needs, not ones this package pins for you. 19.2 is where
37
+ `useEffectEvent` arrived, which the live-data hooks are built on.
37
38
 
38
39
  <Aside type="note" title="What @vitejs/plugin-react is for">
39
40
  Fast Refresh: edit a client component and React keeps its state, instead of
@@ -47,6 +48,7 @@ import { defineConfig } from "vite";
47
48
  import react from "@vitejs/plugin-react";
48
49
  import { nitro } from "nitro/vite";
49
50
  import { rscKit } from "@rsc-kit/core/vite";
51
+ import { fileURLToPath } from "node:url";
50
52
 
51
53
  /**
52
54
  * The full route tree, built the way every scaffolded app is built.
@@ -64,6 +66,13 @@ export default defineConfig({
64
66
  }),
65
67
  react(),
66
68
  ],
69
+ // `@/thing` for `src/thing`, the same alias tsconfig.json declares in
70
+ // `paths`: both, or the import type-checks and then fails to resolve.
71
+ resolve: {
72
+ alias: {
73
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
74
+ },
75
+ },
67
76
  });
68
77
  ```
69
78
 
@@ -159,14 +168,20 @@ runs](/hosts/where-it-runs).
159
168
  {
160
169
  "type": "module",
161
170
  "scripts": {
162
- "dev": "vite",
163
- "build": "vite build",
171
+ "dev": "bun --bun vite",
172
+ "build": "bun --bun vite build",
164
173
  "start": "bun .output/server/index.mjs",
165
- "compile": "bun build --compile .output/server/index.mjs --outfile my-app"
174
+ "compile": "bun --bun vite build && bun build --compile .output/server/index.mjs --outfile dist/app"
166
175
  }
167
176
  }
168
177
  ```
169
178
 
179
+ `bun --bun` runs Vite on Bun's runtime. The `vite` bin has a Node shebang, so
180
+ `bun run dev` alone would start the dev server, the build and the prerender
181
+ under Node — and a project importing `bun` or `bun:sqlite` fails at the first
182
+ render. On Node the scripts are plain `vite` and `vite build`;
183
+ [Running on Bun](/hosts/bun) has the rest.
184
+
170
185
  `build` freezes every page it can, so there is no separate prerender step and
171
186
  no command for one — freezing runs the app, which needs the bundle the build
172
187
  just wrote, and only the build knows where that is. A page that must render
@@ -210,15 +225,18 @@ not match its `[param]` segment is a compile error rather than a blank page.
210
225
  "target": "ESNext",
211
226
  "module": "ESNext",
212
227
  "moduleResolution": "bundler",
228
+ "paths": {
229
+ "@/*": ["./src/*"]
230
+ },
213
231
  "jsx": "react-jsx",
214
232
  "strict": true,
215
233
  "noEmit": true,
234
+ "isolatedModules": true,
235
+ "moduleDetection": "force",
236
+ "verbatimModuleSyntax": true,
216
237
  "skipLibCheck": true,
217
238
  "resolveJsonModule": true,
218
- "types": [
219
- "@types/bun",
220
- "vite/client"
221
- ]
239
+ "types": ["@types/bun", "vite/client"]
222
240
  },
223
241
  "include": ["src/**/*", "server/**/*", ".rsc-kit/**/*", "vite.config.ts"]
224
242
  }
@@ -228,6 +246,14 @@ not match its `[param]` segment is a compile error rather than a blank page.
228
246
  of a stylesheet (`import './styles.css'` in the root layout) is an error, and
229
247
  `import.meta.env` is untyped.
230
248
 
249
+ Three of those are worth keeping even if you trim the rest.
250
+ `verbatimModuleSyntax` makes a type-only import say so, which is what keeps a
251
+ `type` import of a client component from pulling the component into the server
252
+ bundle. `isolatedModules` and `moduleDetection` are what a per-file transpiler
253
+ like Vite's assumes. And `paths` pairs with `resolve.alias` in `vite.config.ts`
254
+ — `@/thing` for `src/thing` — which some tools (shadcn's `init`, for one) check
255
+ for before they will run.
256
+
231
257
  ### Environment variables
232
258
 
233
259
  There is nothing to install and nothing this package adds — Vite already owns
@@ -240,6 +266,18 @@ server, read through `process.env`.
240
266
  The prefix is the whole boundary, so never put a secret behind it.
241
267
  `VITE_STRIPE_KEY` is a published key.
242
268
 
269
+ And one line never to write into a `.env`: `NODE_ENV`. Vite sets it itself —
270
+ `development` under `vite`, `production` under `vite build` — and honours a
271
+ `.env` that sets it, so `NODE_ENV=development` in `.env` turns `vite build`
272
+ into a development build: the pages compile against React's development JSX
273
+ runtime, the server bundles carry React's production build, and every route
274
+ fails to render with React's opaque "message omitted in production builds".
275
+ The build refuses that up front and names the file and line, because a
276
+ plugin cannot override it — Vite applies the `.env` value after plugins have
277
+ run. If another tool in the repository wants the line, keep it in that
278
+ tool's own `.env`, not the app's; the env schema still validates
279
+ `NODE_ENV` from what the runtime sets.
280
+
243
281
  Declare the ones you use, and Vite types them:
244
282
 
245
283
  ```ts title="src/env.d.ts"
@@ -0,0 +1,91 @@
1
+ # Startup
2
+
3
+ > instrumentation.ts runs once, before anything else.
4
+
5
+ Some setup belongs to the process, not to a page: validating the
6
+ environment, configuring a shared package, warming a connection. Put it in
7
+ `src/instrumentation.ts` and it runs once — before any page module
8
+ evaluates, and before the first request.
9
+
10
+ ```ts title="src/instrumentation.ts"
11
+ // Runs at import, before any page: a bad variable stops the server here.
12
+ import './env'
13
+
14
+ // Anything asynchronous the app needs before it serves. The first render
15
+ // waits for it.
16
+ export async function register() {
17
+ await db.connect()
18
+ }
19
+ ```
20
+
21
+ The name is Next's, so a reader arriving from there knows what it is. Both
22
+ halves are optional: a file with only imports is a bootstrap, a file with
23
+ only `register()` is a hook.
24
+
25
+ ## Why it is a framework file
26
+
27
+ Two things make this a file the framework has to know about rather than a
28
+ module the app imports.
29
+
30
+ **Import order.** A page that configures a shared package at import time —
31
+ a logger, an ORM, a metrics client — runs before any module the app could
32
+ put in front of it, because the generated entry imports the pages. The entry
33
+ imports `instrumentation.ts` *first*, so whatever it sets up is set up when
34
+ the first page module evaluates. Importing a bootstrap module from every
35
+ file that needs it works until someone forgets, and the failure is a page
36
+ that throws "not configured" for the one visitor who reached it first.
37
+
38
+ **Before the first request.** `register()` is awaited by every entry point
39
+ a render can come through — the built server, the dev server, the
40
+ prerender, a route's middleware check, a server action, an api route — so
41
+ "before the first render" holds without the app knowing the list.
42
+
43
+ ## When it runs
44
+
45
+ | where | when |
46
+ | --- | --- |
47
+ | a server (`bun`, `node`, any long-lived preset) | at startup, before the server reports itself up |
48
+ | a Worker | at the isolate's first request — there is no startup, and a binding is only readable once a request has arrived |
49
+ | `vite dev` | when the dev server first evaluates the app, and again when the file or anything it imports changes |
50
+ | `vite build` | before the first page is prerendered |
51
+
52
+ Once per process. A `register()` that resolved is not called again; one
53
+ that rejected is retried by the next request, so a database that was not up
54
+ yet is asked again rather than leaving the process permanently refusing.
55
+
56
+ On a server, a failure at startup — the file throwing at import, or
57
+ `register()` rejecting — is reported and the process exits. A server that
58
+ could not bootstrap has nothing correct to serve, and a health check that
59
+ passed on a server about to fail its first visitor is worse than one that
60
+ never passed. The dev server stays up and reports the error on the page.
61
+
62
+ ## Environment validation
63
+
64
+ The scaffold writes this file when the project validates its environment,
65
+ and the only line it needs is the import: `src/env.ts` refuses at import, and
66
+ importing it here is what makes a missing variable the server's failure at
67
+ startup rather than a visitor's three calls later.
68
+
69
+ The build prerenders pages, so it runs the bootstrap too. A build machine
70
+ without the production variables sets `SKIP_ENV_VALIDATION=1`, which the
71
+ generated schema honours; the server that runs the build validates at
72
+ startup regardless.
73
+
74
+ ## On a Worker
75
+
76
+ Read bindings inside `register()`, not at the top of the module. A Worker
77
+ evaluates the module before any request exists, and `process.env` is empty
78
+ until Nitro maps the first request's bindings onto it — so a top-level
79
+ `process.env.DATABASE_URL` is `undefined` at exactly the moment it looks
80
+ like it should not be. Inside `register()`, which runs at the first request,
81
+ it is there.
82
+
83
+ ```ts
84
+ export async function register() {
85
+ const url = process.env.DATABASE_URL // readable here on every host
86
+ await db.connect(url)
87
+ }
88
+ ```
89
+
90
+ The same file works unchanged on a server, where both moments have the
91
+ environment.
@@ -96,6 +96,10 @@ Swap the preset for `node`, `cloudflare_module`, `vercel`, `netlify` or `deno`
96
96
  and the same app deploys there instead. The build produces a `.output`
97
97
  directory; nothing else changes.
98
98
 
99
+ With a backend behind it — Laravel, Go, anything answering one endpoint — the
100
+ app is a **BAP**, [Backend-Answered Pages](/hosts/backend-answered-pages):
101
+ the page is rendered in front of the backend rather than by it.
102
+
99
103
  ## What it is not
100
104
 
101
105
  <Aside type="note" title="No data layer, no CSS pipeline, no dev server of its own">