@rsc-kit/mcp 0.16.3 → 0.18.0

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/images.md CHANGED
@@ -62,12 +62,18 @@ build what resizing costs, once per image per width, and nothing at request
62
62
  time. It is opt-in for that reason: a hundred hero images at four widths is a
63
63
  noticeable build, and most of them belong on a CDN.
64
64
 
65
- Declare the query so TypeScript stops asking:
65
+ Declare the queries so TypeScript stops asking — and the build's typecheck
66
+ lets them through:
66
67
 
67
68
  ```ts title="src/images.d.ts"
68
- declare module '*?*' {
69
- const value: string;
70
- export default value;
69
+ // A pattern may hold one `*`, so each matches on the tail of the query.
70
+ declare module '*&as=srcset' {
71
+ const srcset: string;
72
+ export default srcset;
73
+ }
74
+ declare module '*&format=webp' {
75
+ const url: string;
76
+ export default url;
71
77
  }
72
78
  ```
73
79
 
package/guides/index.json CHANGED
@@ -9,6 +9,11 @@
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
+ },
12
17
  {
13
18
  "slug": "caching",
14
19
  "title": "Asking once per request",
@@ -24,11 +29,26 @@
24
29
  "title": "Rendering per request",
25
30
  "description": "Marking work that belongs to the visitor, not to the build."
26
31
  },
32
+ {
33
+ "slug": "deployment",
34
+ "title": "Deploying",
35
+ "description": "What to ship, and the two settings that fail quietly."
36
+ },
37
+ {
38
+ "slug": "domains",
39
+ "title": "Domains and subdomains",
40
+ "description": "A host as a route segment — admin.example.com reaches app/admin, a tenant's host binds [domain] — with nothing to rewrite."
41
+ },
27
42
  {
28
43
  "slug": "edge-caching",
29
44
  "title": "Serving shells from a CDN",
30
45
  "description": "Putting build-time shells on the edge, and what rsc-kit does not do."
31
46
  },
47
+ {
48
+ "slug": "emails",
49
+ "title": "Emails and other HTML",
50
+ "description": "Rendering React to HTML on the server — an email, a PDF, a feed — from a server action or a route, with \"use ssr\"."
51
+ },
32
52
  {
33
53
  "slug": "errors",
34
54
  "title": "Errors and 404s",
@@ -54,6 +74,11 @@
54
74
  "title": "Getting started",
55
75
  "description": "Serve React Server Components from any JavaScript backend."
56
76
  },
77
+ {
78
+ "slug": "go",
79
+ "title": "Go",
80
+ "description": "A Go process behind the renderer — functions, guards and actions in Go."
81
+ },
57
82
  {
58
83
  "slug": "images",
59
84
  "title": "Images",
@@ -64,11 +89,21 @@
64
89
  "title": "Installation",
65
90
  "description": "From an empty directory to a streaming RSC app."
66
91
  },
92
+ {
93
+ "slug": "instrumentation",
94
+ "title": "Startup",
95
+ "description": "instrumentation.ts runs once, before anything else."
96
+ },
67
97
  {
68
98
  "slug": "introduction",
69
99
  "title": "Introduction",
70
100
  "description": "React Server Components as a Vite plugin, deployed wherever you like."
71
101
  },
102
+ {
103
+ "slug": "laravel",
104
+ "title": "Laravel",
105
+ "description": "React Server Components in front, a Laravel application behind them."
106
+ },
72
107
  {
73
108
  "slug": "mcp",
74
109
  "title": "Working with an AI agent",
@@ -144,6 +179,11 @@
144
179
  "title": "Sections",
145
180
  "description": "Refreshing one region of a page without re-rendering the rest."
146
181
  },
182
+ {
183
+ "slug": "seo-files",
184
+ "title": "robots, sitemap and llms.txt",
185
+ "description": "The files a site describes itself with, from a file beside the root layout — written the way Next writes them, stored at build when they can be."
186
+ },
147
187
  {
148
188
  "slug": "server-actions",
149
189
  "title": "Server actions",
@@ -183,5 +223,15 @@
183
223
  "slug": "view-transitions",
184
224
  "title": "View transitions",
185
225
  "description": "What React's ViewTransition animates in an app built with this, and what it does not."
226
+ },
227
+ {
228
+ "slug": "where-it-runs",
229
+ "title": "Where it runs",
230
+ "description": "A host is a Nitro preset, not a server you write."
231
+ },
232
+ {
233
+ "slug": "your-own-backend",
234
+ "title": "Your own backend",
235
+ "description": "The one endpoint a backend in any language answers."
186
236
  }
187
237
  ]
@@ -43,10 +43,11 @@ needs, not ones this package pins for you.
43
43
  ## Configure Vite
44
44
 
45
45
  ```ts title="vite.config.ts"
46
- import { defineConfig } from 'vite'
47
- import react from '@vitejs/plugin-react'
48
- import { nitro } from 'nitro/vite'
49
- import { rscKit } from '@rsc-kit/core/vite'
46
+ import { defineConfig } from "vite";
47
+ import react from "@vitejs/plugin-react";
48
+ import { nitro } from "nitro/vite";
49
+ import { rscKit } from "@rsc-kit/core/vite";
50
+ import { fileURLToPath } from "node:url";
50
51
 
51
52
  /**
52
53
  * The full route tree, built the way every scaffolded app is built.
@@ -56,16 +57,22 @@ import { rscKit } from '@rsc-kit/core/vite'
56
57
  */
57
58
  export default defineConfig({
58
59
  plugins: [
59
- nitro({ preset: 'bun', serveStatic: 'inline' }),
60
+ nitro({ preset: "bun", serveStatic: "inline" }),
60
61
  rscKit({
61
- sourceDir: 'src',
62
- outDir: 'build',
63
- viewTransitions: true,
62
+ sourceDir: "src",
63
+ outDir: "build",
64
64
  offline: true,
65
65
  }),
66
66
  react(),
67
67
  ],
68
- })
68
+ // `@/thing` for `src/thing`, the same alias tsconfig.json declares in
69
+ // `paths`: both, or the import type-checks and then fails to resolve.
70
+ resolve: {
71
+ alias: {
72
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
73
+ },
74
+ },
75
+ });
69
76
  ```
70
77
 
71
78
  `sourceDir` is where your route tree lives. The defaults are plain Vite ones —
@@ -211,15 +218,18 @@ not match its `[param]` segment is a compile error rather than a blank page.
211
218
  "target": "ESNext",
212
219
  "module": "ESNext",
213
220
  "moduleResolution": "bundler",
221
+ "paths": {
222
+ "@/*": ["./src/*"]
223
+ },
214
224
  "jsx": "react-jsx",
215
225
  "strict": true,
216
226
  "noEmit": true,
227
+ "isolatedModules": true,
228
+ "moduleDetection": "force",
229
+ "verbatimModuleSyntax": true,
217
230
  "skipLibCheck": true,
218
231
  "resolveJsonModule": true,
219
- "types": [
220
- "@types/bun",
221
- "vite/client"
222
- ]
232
+ "types": ["@types/bun", "vite/client"]
223
233
  },
224
234
  "include": ["src/**/*", "server/**/*", ".rsc-kit/**/*", "vite.config.ts"]
225
235
  }
@@ -229,6 +239,14 @@ not match its `[param]` segment is a compile error rather than a blank page.
229
239
  of a stylesheet (`import './styles.css'` in the root layout) is an error, and
230
240
  `import.meta.env` is untyped.
231
241
 
242
+ Three of those are worth keeping even if you trim the rest.
243
+ `verbatimModuleSyntax` makes a type-only import say so, which is what keeps a
244
+ `type` import of a client component from pulling the component into the server
245
+ bundle. `isolatedModules` and `moduleDetection` are what a per-file transpiler
246
+ like Vite's assumes. And `paths` pairs with `resolve.alias` in `vite.config.ts`
247
+ — `@/thing` for `src/thing` — which some tools (shadcn's `init`, for one) check
248
+ for before they will run.
249
+
232
250
  ### Environment variables
233
251
 
234
252
  There is nothing to install and nothing this package adds — Vite already owns
@@ -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">