@rsc-kit/mcp 0.14.0 → 0.15.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.
Files changed (47) hide show
  1. package/dist/answers.d.ts +7 -0
  2. package/dist/answers.js +26 -0
  3. package/dist/answers.js.map +1 -1
  4. package/dist/bundleGuides.d.ts +22 -0
  5. package/dist/bundleGuides.js +130 -0
  6. package/dist/bundleGuides.js.map +1 -0
  7. package/dist/index.js +21 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/recipes.js +81 -6
  10. package/dist/recipes.js.map +1 -1
  11. package/dist/report.d.ts +11 -0
  12. package/dist/report.js +1 -1
  13. package/dist/report.js.map +1 -1
  14. package/guides/api-routes.md +168 -0
  15. package/guides/authorization.md +288 -0
  16. package/guides/caching.md +57 -0
  17. package/guides/connection.md +98 -0
  18. package/guides/edge-caching.md +159 -0
  19. package/guides/errors.md +109 -0
  20. package/guides/file-uploads.md +119 -0
  21. package/guides/fonts.md +117 -0
  22. package/guides/forms.md +528 -0
  23. package/guides/images.md +83 -0
  24. package/guides/index.json +162 -0
  25. package/guides/mcp.md +113 -0
  26. package/guides/metadata.md +289 -0
  27. package/guides/navigation.md +84 -0
  28. package/guides/no-javascript.md +39 -0
  29. package/guides/offline.md +215 -0
  30. package/guides/ppr.md +181 -0
  31. package/guides/pwa.md +260 -0
  32. package/guides/queries.md +340 -0
  33. package/guides/react-compiler.md +153 -0
  34. package/guides/redirects.md +143 -0
  35. package/guides/response-headers.md +66 -0
  36. package/guides/route-interception.md +206 -0
  37. package/guides/routing.md +458 -0
  38. package/guides/sections.md +74 -0
  39. package/guides/server-actions.md +444 -0
  40. package/guides/static-generation.md +347 -0
  41. package/guides/testing.md +158 -0
  42. package/guides/third-party-scripts.md +105 -0
  43. package/guides/typed-routes.md +139 -0
  44. package/guides/url-validation.md +143 -0
  45. package/guides/validation.md +175 -0
  46. package/guides/view-transitions.md +120 -0
  47. package/package.json +4 -3
@@ -0,0 +1,74 @@
1
+ # Sections
2
+
3
+ > Refreshing one region of a page without re-rendering the rest.
4
+
5
+ A page often has one part that changes and a lot that does not. A section names
6
+ that part, so an action can refresh it on its own.
7
+
8
+ ```tsx title="src/app/orders/orders.section.tsx"
9
+ import { section } from '@rsc-kit/core/section'
10
+
11
+ async function Orders() {
12
+ const orders = await db.orders()
13
+
14
+ return (
15
+ <ul>
16
+ {orders.map((o) => <li key={o.id}>{o.reference}</li>)}
17
+ </ul>
18
+ )
19
+ }
20
+
21
+ export default section('orders', Orders)
22
+ ```
23
+
24
+ Render it like any other component:
25
+
26
+ ```tsx title="src/app/orders/page.tsx"
27
+ import Orders from './orders.section'
28
+
29
+ export default function OrdersPage() {
30
+ return (
31
+ <>
32
+ <h1>Orders</h1>
33
+ <Orders />
34
+ </>
35
+ )
36
+ }
37
+ ```
38
+
39
+ ## Refreshing it
40
+
41
+ An action names what it changed, and only that region is rendered again:
42
+
43
+ ```tsx
44
+ 'use server'
45
+
46
+ import { revalidate } from '@rsc-kit/core/revalidate'
47
+
48
+ export async function placeOrder(form: FormData) {
49
+ await db.orders.create({ reference: String(form.get('reference')) })
50
+
51
+ revalidate('orders')
52
+ }
53
+ ```
54
+
55
+ The rest of the page is untouched — not re-rendered and not re-fetched. Whatever
56
+ state lives outside the section, including a half-filled form beside it, stays
57
+ exactly as it was.
58
+
59
+ ## The name is scoped to the module, not the app
60
+
61
+ Two pages may both call their section `orders`. The name is resolved through the
62
+ module the route declares, not through a table every section in the app writes
63
+ to.
64
+
65
+ That is a security property rather than a convenience. A name-keyed registry is
66
+ populated by every section at bundle load, so a lookup by name could reach any
67
+ page's region from any url — bounded only by whatever guard happened to sit on
68
+ the url that was asked for.
69
+
70
+ ## What a section is not
71
+
72
+ It is not a cache boundary and not a client component. It renders on the server
73
+ like everything else; what it adds is a seam the server can render *into* on its
74
+ own, without producing the whole page around it.
@@ -0,0 +1,444 @@
1
+ # Server actions
2
+
3
+ > Calling the server from a client component, as an ordinary function.
4
+
5
+ A server action is an async function a client component can call directly. Mark
6
+ the module `"use server"` and its exports become callable from the browser; the
7
+ body stays on the server.
8
+
9
+ ## Writing one
10
+
11
+ ```ts title="src/actions.ts"
12
+ 'use server'
13
+
14
+ import { revalidate } from '@rsc-kit/core/revalidate'
15
+ import { addOrder } from './orders'
16
+
17
+ let total = 0
18
+
19
+ // Called straight from a client component as an ordinary async function. The
20
+ // body never reaches the browser; the call becomes a POST to /_rsc/action.
21
+ export async function addToTotal(amount: number): Promise<number> {
22
+ total += amount
23
+
24
+ return total
25
+ }
26
+
27
+ // Marks the orders list stale. The re-rendered list travels back with this
28
+ // action's own answer, so the client never makes a second request for it.
29
+ export async function placeOrder(item: string): Promise<{ ok: true }> {
30
+ await addOrder(item)
31
+ revalidate('orders')
32
+
33
+ return { ok: true }
34
+ }
35
+ ```
36
+
37
+ Nothing is registered and nothing is generated. The build turns each export
38
+ into a reference, and the import in a client component resolves to a stub that
39
+ performs the call.
40
+
41
+ ## Calling one
42
+
43
+ ```tsx title="src/components/AddOrder.tsx"
44
+ 'use client'
45
+
46
+ import { useState } from 'react'
47
+ import { placeOrder } from '../actions'
48
+
49
+ export function AddOrder() {
50
+ const [item, setItem] = useState('')
51
+ const [note, setNote] = useState('')
52
+
53
+ return (
54
+ <div className="add-order">
55
+ <input value={item} onChange={(e) => setItem(e.target.value)} placeholder="Item" id="item" />
56
+ <button
57
+ id="place"
58
+ onClick={async () => {
59
+ await placeOrder(item)
60
+ setItem('')
61
+ }}
62
+ >
63
+ Place order
64
+ </button>
65
+ {/* Deliberately untouched by the action: proof the page was not replaced. */}
66
+ <input value={note} onChange={(e) => setNote(e.target.value)} placeholder="A note" id="note" />
67
+ </div>
68
+ )
69
+ }
70
+ ```
71
+
72
+ `import { placeOrder }` does not pull `actions.ts` into the browser bundle.
73
+ Anything that module imports — a database client, a secret — stays on the
74
+ server with it.
75
+
76
+ ## What happens on the wire
77
+
78
+ 1. The client component calls the function.
79
+ 2. React serialises the arguments in the Flight format.
80
+ 3. The browser sends one `POST /_rsc/action` carrying the action's id and the encoded arguments.
81
+ 4. The host decodes them and runs the real function.
82
+ 5. The return value is serialised as Flight and streamed back.
83
+
84
+ Files travel this way too, without any encoding of your own — see
85
+ [File uploads](/guides/file-uploads).
86
+
87
+ ## Returning UI
88
+
89
+ Step 5 is the same serialiser a page goes through, so an action can answer
90
+ with elements instead of data — server components, client components, Suspense
91
+ boundaries — and the caller receives real React elements to render:
92
+
93
+ ```tsx title="src/actions.tsx"
94
+ 'use server';
95
+
96
+ import { Suspense } from 'react';
97
+ import { Counter } from './Counter';
98
+
99
+ export async function renderCard(name: string) {
100
+ return (
101
+ <section>
102
+ <h2>{name}</h2>
103
+ <Counter />
104
+ <Suspense fallback={<p>loading…</p>}>
105
+ <Related to={name} />
106
+ </Suspense>
107
+ </section>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ```tsx title="src/CardButton.tsx"
113
+ 'use client';
114
+
115
+ const [ui, setUi] = useState<ReactNode>(null);
116
+
117
+ <button onClick={async () => setUi(await renderCard('ada'))}>card</button>
118
+ {ui}
119
+ ```
120
+
121
+ The answer streams. The promise resolves as soon as the root row arrives, so
122
+ the card mounts with its fallback showing, and `<Related>` fills the hole when
123
+ it is done — the action did not wait for it. The browser never receives
124
+ `<Related>`'s code: it ran on the server, and only its output crossed.
125
+
126
+ ### Streaming tokens
127
+
128
+ That is the primitive behind "generative UI". An async component that renders
129
+ one chunk and suspends on the next is a streamed answer, with nothing to
130
+ install:
131
+
132
+ ```tsx title="src/actions.tsx"
133
+ 'use server';
134
+
135
+ export async function ask(prompt: string) {
136
+ const tokens = model.stream(prompt); // an AsyncIterator<string>
137
+
138
+ return (
139
+ <Suspense fallback={<p>thinking…</p>}>
140
+ <Tokens from={tokens[Symbol.asyncIterator]()} />
141
+ </Suspense>
142
+ );
143
+ }
144
+
145
+ async function Tokens({ from }: { from: AsyncIterator<string> }) {
146
+ const { value, done } = await from.next();
147
+
148
+ if (done) return null;
149
+
150
+ return (
151
+ <>
152
+ {value}
153
+ <Suspense fallback={null}>
154
+ <Tokens from={from} />
155
+ </Suspense>
156
+ </>
157
+ );
158
+ }
159
+ ```
160
+
161
+ Each token is a row in the stream and each `<Suspense>` a hole the next one
162
+ fills. That is what `createStreamableUI` from Vercel's AI SDK does under the
163
+ hood, in Next as here — the same React serialiser.
164
+
165
+ Reach for it when the server knows what to render and the client should not:
166
+ a card, a chart, a widget. A chat wants data streamed to the client instead
167
+ (`streamText` and `useChat` in the AI SDK), because a half-streamed element
168
+ tree cannot be cancelled, resumed, retried or persisted, and this one is a
169
+ tree as deep as the token count.
170
+
171
+ ## Refreshing what the action changed
172
+
173
+ An action that changes data usually makes something on screen wrong. Mark it
174
+ stale with `revalidate`, and the re-rendered content travels back **with the
175
+ action's own answer**:
176
+
177
+ `placeOrder` in the module above does exactly that: it writes the order, then
178
+ calls `revalidate('orders')`.
179
+
180
+ The tag names a section — a region of the page registered under a name the
181
+ server can address on its own:
182
+
183
+ ```tsx title="src/app/orders/orders.section.tsx"
184
+ import { section } from '@rsc-kit/core/section'
185
+ import { listOrders } from '../../orders'
186
+
187
+ // A named region. section() registers it under a name the client can refresh
188
+ // and an action can mark, so this list re-renders without the page around it
189
+ // being touched.
190
+ async function Orders() {
191
+ const orders = await listOrders()
192
+
193
+ return (
194
+ <ul className="orders">
195
+ {orders.map((order) => (
196
+ <li key={order.id}>{order.item}</li>
197
+ ))}
198
+ </ul>
199
+ )
200
+ }
201
+
202
+ export default section('orders', Orders)
203
+ ```
204
+
205
+ One request, not two: the caller sees only what its action returned, and the
206
+ page updates around it. Nothing else is re-rendered — which is what the second input in
207
+ `AddOrder` is there to prove. Type into it, place an order, and
208
+ what you typed is still there.
209
+
210
+ `section()` is deliberately not a client module. The boundary it wraps things in
211
+ is one, but the thing it wraps is a server component that fetches its own data,
212
+ and a client reference cannot be async.
213
+
214
+ ## Building one with a schema and middleware
215
+
216
+ A bare `"use server"` function takes whatever it is given and defends itself by
217
+ hand. `createActionClient` gives you a shape where the checking, the context
218
+ and the types come from one declaration:
219
+
220
+ ```ts title="src/lib/action.ts"
221
+ import { createActionClient } from '@rsc-kit/core/action';
222
+
223
+ export const action = createActionClient({
224
+ // Everything reaching here is a bug or an outage, and its message may name a
225
+ // query, a path, a host. Say something fixed unless you raised it yourself.
226
+ onError: (error) => (error instanceof AppError ? error.message : 'Something went wrong.'),
227
+ });
228
+ ```
229
+
230
+ ```ts title="src/actions.ts"
231
+ 'use server'
232
+
233
+ import { z } from 'zod';
234
+ import { action } from './lib/action';
235
+
236
+ export const createPost = action
237
+ .use(async ({ next }) => next({ ctx: { user: await currentUser() } }))
238
+ .use(async ({ ctx, next }) => {
239
+ if (!ctx.user) throw new AppError('Sign in first');
240
+
241
+ return next({ ctx: { audit: `user:${ctx.user.id}` } });
242
+ })
243
+ .input(z.object({ title: z.string().min(3), body: z.string().min(10) }))
244
+ .handler(async ({ input, ctx }) => {
245
+ return savePost({ ...input, authorId: ctx.user.id });
246
+ });
247
+ ```
248
+
249
+ `input` is typed from the schema and `ctx` from every middleware that ran, so
250
+ the handler is checked against both without either being written twice. Add a
251
+ `.use()` and the handler's `ctx` grows; change the schema and the handler stops
252
+ compiling.
253
+
254
+ Middleware runs outermost first and wraps what follows, so a step can time or
255
+ clean up around the rest, not only check before it. Throw to refuse.
256
+
257
+ Return what `next()` gave you — it carries the value from everything inside. A
258
+ step that neither calls `next()` nor throws is reported as a mistake, because a
259
+ forgotten `next()` would otherwise look exactly like a check that passed.
260
+
261
+ <Aside type="note" title="A different thing from middleware.ts">
262
+ `middleware.ts` in a route directory decides whether a page may render. This
263
+ wraps one action. They are separate because an action is reachable without
264
+ any page — which is why it defends itself.
265
+ </Aside>
266
+
267
+ ### Failures come back, they are not thrown
268
+
269
+ ```ts
270
+ const result = await createPost({ title: 'x', body: 'y' });
271
+
272
+ result.data // what the handler returned
273
+ result.validationErrors // { title: ['Too short'] }
274
+ result.serverError // 'Something went wrong.'
275
+ ```
276
+
277
+ Returned rather than thrown, and that is not a style choice: React serialises a
278
+ rejected server action opaquely — production strips the message and leaves a
279
+ digest — so a thrown validation error reaches the browser as "an error
280
+ occurred" with the fields it named gone. A returned object crosses intact.
281
+
282
+ `<Form>` reads that shape directly, so there is nothing to wire:
283
+
284
+ ```tsx
285
+ <Form action={createPost} schema={schema}>
286
+ ```
287
+
288
+ The schema then runs twice, in the two places it has to: in the browser so a
289
+ mistake costs no round trip, and in the action because the action is a public
290
+ endpoint reachable without the form.
291
+
292
+ ### Failing on something a schema cannot know
293
+
294
+ ```ts
295
+ .handler(async ({ input, fieldErrors }) => {
296
+ if (await slugTaken(input.slug)) return fieldErrors({ slug: 'Already taken' });
297
+
298
+ return save(input);
299
+ })
300
+ ```
301
+
302
+ It arrives as `validationErrors`, on the field you named, exactly like a schema
303
+ failure — so the form renders it in the same place with no extra handling.
304
+
305
+ `fieldErrors` comes in with the handler's arguments rather than being imported,
306
+ and that is what makes it typed: the field names are the schema's, so
307
+ `fieldErrors({ slgu: … })` does not compile. There is nothing to pass — the
308
+ handler already knows its input.
309
+
310
+ **Write `return fieldErrors(…)`.** It throws either way, so nothing after it
311
+ runs — but TypeScript cannot see that from a destructured argument. Without the
312
+ `return`, a `user` you checked is still `possibly undefined` on the next line.
313
+ With it, the type narrows and the code reads as what it is: this branch is
314
+ over. A forgotten `return` is not a runtime bug; it is a type error that tells
315
+ you to add one.
316
+
317
+ A string is one message; an array is several. The empty key is the whole
318
+ submission, for a refusal that is about no field in particular:
319
+
320
+ ```ts
321
+ return fieldErrors({ '': 'Sign-ups are closed for the weekend' });
322
+ ```
323
+
324
+ :::note[Porting from next-safe-action]
325
+ `return returnValidationErrors(schema, { email: { _errors: ['Account not found'] } })`
326
+ becomes `return fieldErrors({ email: 'Account not found' })`. No schema
327
+ argument, because the typing comes from the handler, and no `_errors` nesting.
328
+ :::
329
+
330
+ ## Setting a cookie
331
+
332
+ Signing someone in is a mutation whose entire result is a cookie, so an action
333
+ can write one:
334
+
335
+ ```ts
336
+ 'use server';
337
+
338
+ import { cookies } from '@rsc-kit/core/request';
339
+
340
+ export async function login(formData: FormData) {
341
+ const session = await authenticate(formData);
342
+
343
+ (await cookies()).set('session', session.token, {
344
+ httpOnly: true,
345
+ secure: true,
346
+ sameSite: 'lax',
347
+ maxAge: 60 * 60 * 24 * 7,
348
+ });
349
+ }
350
+ ```
351
+
352
+ It lands on the action's own response, so the next navigation already carries
353
+ it. `delete` expires one, which is what signing out is.
354
+
355
+ That `get`/`set`/`delete` trio is the whole surface an auth library needs, so
356
+ one can be wired in without this package having an opinion about which. See
357
+ [Writing to the response](/guides/authorization#writing-to-the-response) for
358
+ where else it works, and where it does not.
359
+
360
+ ## Two at once
361
+
362
+ Nothing queues them. Each call is an ordinary `fetch`, so two submits fired
363
+ together really do overlap — they are not serialised behind one another.
364
+
365
+ That is a difference from Next, which runs one server action at a time per
366
+ client — and it is worth being deliberate about, because Next's queue was
367
+ doing something for you that you may not have noticed.
368
+
369
+ A double-clicked "add to cart" sends two calls either way. Next runs them one
370
+ after the other; here they run together.
371
+
372
+ So if a handler reads a value, changes it and writes it back, two of them can
373
+ lose an update. That is ordinary database concurrency, which a per-client queue
374
+ was quietly absorbing for you. Fix it where you would fix it anywhere else:
375
+
376
+ - do the read and the write in one transaction
377
+ - use an atomic update instead of read-modify-write
378
+ - accept an idempotency key from the form
379
+
380
+ None of that is new advice. It just stops being optional.
381
+
382
+ The engine itself is safe under concurrency, and that part is tested rather
383
+ than assumed: two actions running at the same moment each get their own
384
+ request scope, so their cookies, headers and revalidation marks never reach
385
+ each other's response.
386
+
387
+ An action's answer can carry a re-rendered region with it, and applying two of
388
+ those is last-response-wins. Two actions that both `revalidate('orders')` will
389
+ leave whichever response *arrived* last on screen, which is usually the one
390
+ that committed last — but not necessarily, and nothing detects the difference.
391
+
392
+ <Aside type="note" title="Why not just order them">
393
+ Tagging each call with a sequence number and ignoring the older one sounds
394
+ like the fix and is worse than the problem. The order they were *sent* in is
395
+ not the order they *committed* in: a slow first action can commit after a
396
+ fast second one, and its tree is then the newer state. Rejecting it because
397
+ it was sent first shows older data. Only the server knows which write won,
398
+ so ordering has to come from there or not at all.
399
+ </Aside>
400
+
401
+ Fire as many as you like when they touch different things. When several touch
402
+ the same region, either let the last answer win or serialise them yourself.
403
+
404
+ `<Form>` gives you `pending` for that. It does not guard itself, so a
405
+ double-click sends two requests unless you disable the button.
406
+
407
+ ## Errors
408
+
409
+ An action that fails does not answer with a Flight stream, so the client turns
410
+ the response into an error before the decoder ever sees it:
411
+
412
+ | Response | What the client throws | What you do |
413
+ | --- | --- | --- |
414
+ | `X-RSC-Redirect` header | `ServerRedirectError` | Nothing — the browser is sent to the location. |
415
+ | `422` | `ServerValidationError`, carrying `errors` | Show the messages. `<Form>` does it for you. |
416
+ | Any other failure | `Error`, naming the status | Whatever the app needs. |
417
+
418
+ ```tsx
419
+ "use client";
420
+
421
+ import { ServerValidationError } from '@rsc-kit/core/errors';
422
+ import { createPost } from '../actions';
423
+
424
+ try {
425
+ await createPost(title, body);
426
+ } catch (error) {
427
+ if (error instanceof ServerValidationError) {
428
+ // error.errors — { title: ['Too short'], body: ['Required'] }
429
+ }
430
+ }
431
+ ```
432
+
433
+ A redirect is treated as an instruction rather than something to display: an
434
+ expired session answers that way, and the right response is the login page, not
435
+ a message about one. See [Validation](/guides/validation) for the form-shaped
436
+ version of the same flow, which needs no `try`/`catch` at all.
437
+
438
+ <Aside type="danger" title="An action is a public endpoint">
439
+ The id is not a secret and the endpoint takes no session: anything exported
440
+ from a `"use server"` module can be invoked by anyone who can reach your
441
+ server. Check the caller **inside the action** — rendering the button
442
+ conditionally is a UI decision, not a control. See
443
+ [Authorization](/guides/authorization).
444
+ </Aside>