@rsc-kit/mcp 0.14.0 → 0.16.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 (52) hide show
  1. package/dist/answers.d.ts +7 -0
  2. package/dist/answers.js +28 -0
  3. package/dist/answers.js.map +1 -1
  4. package/dist/bundleGuides.d.ts +27 -0
  5. package/dist/bundleGuides.js +138 -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 +138 -17
  10. package/dist/recipes.js.map +1 -1
  11. package/dist/report.d.ts +13 -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/coming-from-next.md +151 -0
  18. package/guides/connection.md +98 -0
  19. package/guides/edge-caching.md +159 -0
  20. package/guides/errors.md +109 -0
  21. package/guides/file-uploads.md +119 -0
  22. package/guides/fonts.md +117 -0
  23. package/guides/forms.md +528 -0
  24. package/guides/getting-started.md +132 -0
  25. package/guides/images.md +83 -0
  26. package/guides/index.json +187 -0
  27. package/guides/installation.md +338 -0
  28. package/guides/introduction.md +119 -0
  29. package/guides/mcp.md +113 -0
  30. package/guides/metadata.md +289 -0
  31. package/guides/navigation.md +84 -0
  32. package/guides/no-javascript.md +76 -0
  33. package/guides/offline.md +215 -0
  34. package/guides/ppr.md +181 -0
  35. package/guides/pwa.md +260 -0
  36. package/guides/queries.md +340 -0
  37. package/guides/quick-start.md +99 -0
  38. package/guides/react-compiler.md +153 -0
  39. package/guides/redirects.md +143 -0
  40. package/guides/response-headers.md +66 -0
  41. package/guides/route-interception.md +206 -0
  42. package/guides/routing.md +458 -0
  43. package/guides/sections.md +74 -0
  44. package/guides/server-actions.md +444 -0
  45. package/guides/static-generation.md +347 -0
  46. package/guides/testing.md +158 -0
  47. package/guides/third-party-scripts.md +105 -0
  48. package/guides/typed-routes.md +139 -0
  49. package/guides/url-validation.md +143 -0
  50. package/guides/validation.md +175 -0
  51. package/guides/view-transitions.md +120 -0
  52. package/package.json +4 -3
@@ -0,0 +1,139 @@
1
+ # Typed routes
2
+
3
+ > Links that fail the typecheck instead of the browser.
4
+
5
+ Every build writes the urls it found to `.rsc-kit/rsc-routes.d.ts`.
6
+ `Link`, `visit`, `prefetch` and `Form` accept only those.
7
+
8
+ ```tsx
9
+ import Link from '@rsc-kit/core/Link'
10
+
11
+ <Link href="/about">About</Link> // ✅
12
+ <Link href="/abuot">About</Link> // ❌ typecheck fails
13
+ ```
14
+
15
+ Dynamic segments work through ordinary template literals:
16
+
17
+ ```tsx
18
+ <Link href={`/posts/${post.slug}`}>{post.title}</Link> // ✅
19
+ <Link href={`/postz/${post.slug}`}>{post.title}</Link> // ❌
20
+ ```
21
+
22
+ ## There is no `route()` helper
23
+
24
+ Deliberately. A template literal is already checked the same way a builder
25
+ would check it, so a builder would only wrap what the language does for free.
26
+ One existed and was removed.
27
+
28
+ The one thing to watch is that a value you interpolate is url-safe.
29
+ `` `/posts/${'a / b'}` `` type-checks and means three path segments — use
30
+ `encodeURIComponent` when the value is not yours:
31
+
32
+ ```tsx
33
+ <Link href={`/posts/${encodeURIComponent(slug)}`}>…</Link>
34
+ ```
35
+
36
+ ## Search params, typed by the page
37
+
38
+ A page that [exports a `searchParams` schema](/guides/url-validation) has said
39
+ what its query string means. The same schema types every link to it:
40
+
41
+ ```tsx title="src/app/search/page.tsx"
42
+ export const searchParams = z.object({
43
+ q: z.string().default(''),
44
+ page: z.coerce.number().int().min(1).default(1),
45
+ });
46
+ ```
47
+
48
+ ```tsx
49
+ <Link href="/search" search={{ q: 'shoes', page: 2 }}>Page 2</Link>
50
+
51
+ <Link href="/search" search={{ sort: 'asc' }}>…</Link> // does not compile: the page never reads sort
52
+ <Link href="/search" search={{ page: '2' }}>…</Link> // does not compile: page is a number
53
+ ```
54
+
55
+ A key the page requires is required on the link — a `q: z.string()` with no
56
+ default makes `search` itself required, so the page's error boundary is not
57
+ where a missing `q` is found. A page with no schema takes any scalars, and so
58
+ does an href that is not one route (`path as Href`), because there is nothing
59
+ to check it against.
60
+
61
+ Values are typed by what the page will **see**, not what the schema accepts:
62
+ `z.coerce.number()` takes anything in, and a link typed by that would take
63
+ `page: 'two'`. Required-ness comes from the schema's input, the value from its
64
+ output. Arrays repeat the key — `tags: ['a', 'b']` is `?tags=a&tags=b`, which
65
+ is what `z.array()` parses back.
66
+
67
+ For `visit()`, `prefetch()` and anything else that wants the string,
68
+ `href()` runs the same check:
69
+
70
+ ```ts
71
+ import { href } from '@rsc-kit/core/routes';
72
+
73
+ visit(href('/search', { q: 'shoes', page: 2 }));
74
+ ```
75
+
76
+ The build writes one line per route into `rsc-routes.d.ts` that reads the
77
+ page module's `searchParams` export as a type. Nothing runs; a page without
78
+ the export costs nothing.
79
+
80
+ ## Two limits
81
+
82
+ **A dynamic segment widens.** `/posts/[slug]` becomes `` `/posts/${string}` ``,
83
+ so `/posts/a/b` type-checks even though it does not match at runtime.
84
+
85
+ **A list widens to `string`** unless you say what it is:
86
+
87
+ ```tsx
88
+ const nav = [
89
+ { href: '/', label: 'Home' },
90
+ { href: '/about', label: 'About' },
91
+ ] satisfies { href: Href; label: string }[]
92
+ ```
93
+
94
+ Without `satisfies`, TypeScript infers `string` for `href` and you lose the
95
+ check.
96
+
97
+ ## If you never run the generator
98
+
99
+ `.rsc-kit/rsc-routes.d.ts` is written by the build. Without it — or with a
100
+ tsconfig whose `include` does not cover `.rsc-kit` — nothing is registered,
101
+ every url-taking prop stays exactly as permissive as a plain `string`, and
102
+ nothing breaks. There is no flag to turn this on.
103
+
104
+ ## `redirect()` is not typed
105
+
106
+ Its destination is usually computed — read from a cookie, handed over by
107
+ middleware — so typing it would make the common case a cast.
108
+
109
+ ## Api routes
110
+
111
+ Every build writes the `route.ts` files it found as well, in their own union —
112
+ so a `fetch` to an endpoint that no longer exists stops compiling:
113
+
114
+ ```ts
115
+ import { apiUrl } from '@rsc-kit/core/routes'
116
+
117
+ await fetch(apiUrl(`/api/orders/${id}`))
118
+ await fetch(apiUrl('/api/ordrs')) // does not compile
119
+ ```
120
+
121
+ `apiUrl` returns what it was given. It exists because `fetch` takes any
122
+ `string`, so without somewhere to put the type there is nothing to check
123
+ against — the function is the place.
124
+
125
+ **Pages and api routes are separate unions on purpose.** `<Link href="/api/health">`
126
+ does not compile, because linking to an api route navigates the browser away to
127
+ a json document; and `apiUrl('/orders')` does not compile either, because
128
+ fetching a page gets html where json was expected. Each refuses the other's
129
+ urls, which is the pair of mistakes worth catching.
130
+
131
+ :::note[Paths, not response types]
132
+ This checks the **url**. It does not infer what the endpoint returns — that
133
+ would mean a typed `json()` helper of our own in place of `Response.json()`, and
134
+ api routes are deliberately web standards with nothing of ours required in them.
135
+
136
+ For end-to-end types without a fetch at all, a [server action or
137
+ query](/guides/queries/) is already typed across the boundary: the return type
138
+ is the function's, because it is the same function.
139
+ :::
@@ -0,0 +1,143 @@
1
+ # Typed URLs
2
+
3
+ > Checking and typing params, query strings and request bodies with a schema you already write.
4
+
5
+ A url is input from a stranger. `params` arrive as strings, `searchParams` as
6
+ more strings, and `?page=banana` reaches your database as `NaN` unless
7
+ something stopped it.
8
+
9
+ Export a schema beside the page and it stops there instead:
10
+
11
+ ```tsx title="src/app/posts/[slug]/page.tsx"
12
+ import { z } from 'zod'
13
+ import type { PageProps } from '@rsc-kit/core/route-schema'
14
+
15
+ export const params = z.object({ slug: z.string().min(1) })
16
+
17
+ export const searchParams = z.object({
18
+ page: z.coerce.number().int().min(1).default(1),
19
+ tag: z.array(z.string()).default([]),
20
+ })
21
+
22
+ export default async function PostPage({ params, searchParams }: PageProps<
23
+ typeof params,
24
+ typeof searchParams
25
+ >) {
26
+ const { slug } = await params // string
27
+ const { page, tag } = await searchParams // number, string[]
28
+ }
29
+ ```
30
+
31
+ Both are ordinary exports, read by the build the same way `metadata` and
32
+ `generateStaticParams` are. Any [Standard Schema](https://standardschema.dev)
33
+ works — Zod, Valibot, ArkType — because the schema is asked to validate itself
34
+ and nothing here imports one.
35
+
36
+ Three things you get that types alone cannot give you:
37
+
38
+ - **`?page=3` arrives as `3`**, the number, not `"3"`.
39
+ - **A missing value arrives as its default**, so there is no `undefined` branch
40
+ to write.
41
+ - **A bad value is refused once**, at the edge, instead of surviving as `NaN`
42
+ into whatever the page does next.
43
+
44
+ Export neither and nothing changes: `params` is the record of strings it always
45
+ was, and `searchParams` is a `URLSearchParams`.
46
+
47
+ ## They fail differently, on purpose
48
+
49
+ This is the part worth reading twice.
50
+
51
+ | what was wrong | answer | why |
52
+ | --- | --- | --- |
53
+ | `params` | **404**, `not-found.tsx` | the url does not describe a page |
54
+ | `searchParams` | nearest `error.tsx` | the page exists, the query was wrong |
55
+
56
+ `/posts/` with a slug your schema refuses is not a broken page, it is an
57
+ absent one — and a `500` tells a crawler to come back later while a `404` tells
58
+ it the thing is gone. Meanwhile refusing a bad `?page=` as a 404 would let one
59
+ bad link make a real page look deleted.
60
+
61
+ The error carries the fields, so a boundary can say which one:
62
+
63
+ ```tsx title="src/app/posts/[slug]/error.tsx"
64
+ 'use client'
65
+
66
+ import { isSearchParamsError } from '@rsc-kit/core/route-schema'
67
+
68
+ export default function Error({ error }) {
69
+ if (isSearchParamsError(error)) {
70
+ return <p>That link is not quite right: {Object.keys(error.errors).join(', ')}</p>
71
+ }
72
+
73
+ return <p>Something went wrong.</p>
74
+ }
75
+ ```
76
+
77
+ ## Repeated keys
78
+
79
+ `?tag=red&tag=blue` arrives as an array and `?q=shoes` as a string, without the
80
+ schema having to know which shape the url happened to take. So
81
+ `z.array(z.string())` and `z.string()` both work on the key you would expect.
82
+
83
+ ## API routes take the same three
84
+
85
+ Plus the body, which is the one that matters for a `POST`:
86
+
87
+ ```ts title="src/app/api/posts/[id]/route.ts"
88
+ import { z } from 'zod'
89
+
90
+ export const params = z.object({ id: z.coerce.number().int() })
91
+ export const searchParams = z.object({ fields: z.string().optional() })
92
+ export const body = z.object({ title: z.string().min(1), draft: z.boolean().default(false) })
93
+
94
+ export async function POST(request: Request, { params, body }) {
95
+ const { id } = await params // number
96
+ const { title } = await body // non-empty string
97
+
98
+ return Response.json(await createPost(id, title), { status: 201 })
99
+ }
100
+ ```
101
+
102
+ Awaited, the same way a page awaits its props. That is not only symmetry: a
103
+ route that never awaits `searchParams` provably does not vary by it, so the
104
+ build can store one answer and serve it for `?utm_source=anything`. Resolved
105
+ eagerly, that fact is unknowable and every tracking link misses the stored
106
+ answer.
107
+
108
+ The handler still takes a real `Request` and still returns a real `Response` —
109
+ the schemas add a second argument and take nothing away. A route that exports
110
+ none behaves exactly as it did before any of this existed.
111
+
112
+ Statuses follow the same reasoning as a page, plus one:
113
+
114
+ | what was wrong | answer |
115
+ | --- | --- |
116
+ | `params` | `404` |
117
+ | `searchParams` | `400` with `{ message, errors }` |
118
+ | `body` | `422` with `{ message, errors }` |
119
+
120
+ `422` because that is what a [server action](/guides/validation/) already
121
+ returns for a refused field, so a client has one shape to handle rather than
122
+ two.
123
+
124
+ JSON and form encodings both parse, since a url is posted to by `fetch` and by
125
+ `<form>` alike. A body that is not valid JSON is a `422` about the body as a
126
+ whole rather than a crash.
127
+
128
+ :::caution[Exporting `body` reads the request]
129
+ The schema consumes the stream, so `await request.json()` inside the handler
130
+ will find it already read. Use the parsed value — that is the point of it.
131
+
132
+ Only when you export a `body` schema, and only for `POST`, `PUT`, `PATCH` and
133
+ `DELETE`. A `GET` handler is never touched.
134
+ :::
135
+
136
+ ## What this is not
137
+
138
+ A schema here checks the **shape** of a request, not whether the person making
139
+ it may. `params.id` being a number says nothing about whose row it is — the
140
+ caller chooses the id, and trusting it is the whole of an IDOR.
141
+
142
+ Authorise on identity, in the handler, after the shape is known good. See
143
+ [Authorization](/guides/authorization/).
@@ -0,0 +1,175 @@
1
+ # Validation
2
+
3
+ > Surfacing server-side validation errors in a form.
4
+
5
+ Validation errors have one shape everywhere: a field name mapped to the
6
+ messages for it.
7
+
8
+ ```ts
9
+ { title: ['The title must be at least 3 characters.'], body: ['The body is required.'] }
10
+ ```
11
+
12
+ `ServerValidationError` carries that shape, and `<Form>`
13
+ catch it and put it in `errors`. Nothing in a component has to know where the
14
+ validation ran.
15
+
16
+ ## In a form
17
+
18
+ ```tsx title="src/components/PostForm.tsx"
19
+ "use client";
20
+
21
+ import { Form } from '@rsc-kit/core/Form';
22
+ import { createPost } from '../actions';
23
+
24
+ export function PostForm() {
25
+ return (
26
+ <Form action={createPost}>
27
+ {({ errors, pending }) => (
28
+ <>
29
+ <input name="title" />
30
+ {errors.title?.map((message) => <p key={message} className="error">{message}</p>)}
31
+
32
+ <textarea name="body" />
33
+ {errors.body?.map((message) => <p key={message} className="error">{message}</p>)}
34
+
35
+ <button disabled={pending}>Create</button>
36
+ </>
37
+ )}
38
+ </Form>
39
+ );
40
+ }
41
+ ```
42
+
43
+ No `try`/`catch`, and no state of your own. A successful submit clears
44
+ `errors`; a failed one fills them.
45
+
46
+ ## Outside a form
47
+
48
+ Catch it yourself, wherever the call rejects with one:
49
+
50
+ ```tsx
51
+ "use client";
52
+
53
+ import { ServerValidationError } from '@rsc-kit/core/errors';
54
+ import { createPost } from '../actions';
55
+
56
+ try {
57
+ await createPost(title, body);
58
+ } catch (error) {
59
+ if (error instanceof ServerValidationError) {
60
+ setErrors(error.errors);
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Producing one
66
+
67
+ <Aside type="caution" title="The server half is the host's">
68
+ There is no validation library here, and no rule syntax to learn. What the
69
+ engine defines is the error the client understands — deciding that input is
70
+ invalid is your code's job, or your host's.
71
+ </Aside>
72
+
73
+ ### Before it is submitted
74
+
75
+ `<Form>` takes any [Standard Schema](https://standardschema.dev) —
76
+ Zod, Valibot, ArkType, or anything else implementing it. There is no adapter
77
+ per library and no import of one: the schema is asked to validate itself.
78
+
79
+ ```tsx title="src/components/PostForm.tsx"
80
+ "use client";
81
+
82
+ import { z } from 'zod';
83
+ import { Form } from '@rsc-kit/core/Form';
84
+ import { createPost } from '../actions';
85
+
86
+ const schema = z.object({
87
+ title: z.string().min(3, 'Too short'),
88
+ body: z.string().min(10, 'Too short'),
89
+ });
90
+
91
+ export function PostForm() {
92
+ return (
93
+ <Form action={createPost} schema={schema}>
94
+ {({ errors, pending }) => (
95
+ <>
96
+ <input name="title" />
97
+ {errors.title?.map((m) => <p key={m} className="error">{m}</p>)}
98
+
99
+ <textarea name="body" />
100
+ {errors.body?.map((m) => <p key={m} className="error">{m}</p>)}
101
+
102
+ <button disabled={pending}>Create</button>
103
+ </>
104
+ )}
105
+ </Form>
106
+ );
107
+ }
108
+ ```
109
+
110
+ A failure fills the same `errors` you were already rendering and the action is
111
+ never called — so a mistake costs no round trip, and an optimistic update never
112
+ shows a row the server was going to refuse. Async schemas are awaited, so a
113
+ uniqueness check works the same way.
114
+
115
+ Field names follow the schema's paths: a nested field is `address.city`, and an
116
+ issue belonging to no field — a cross-field rule like "these must match" — is
117
+ under the empty string, so it has somewhere to be shown rather than being
118
+ dropped.
119
+
120
+ <Aside type="danger" title="A schema here is a courtesy, not a control">
121
+ It runs in the browser. The action is a public endpoint reachable without
122
+ this form — with `curl`, with the wrong values, in any order — so the server
123
+ still has to check. See [Authorization](/guides/authorization).
124
+
125
+ Use the same schema on both sides if you like; the point is that the one
126
+ running on the server is the one that decides.
127
+ </Aside>
128
+
129
+ ### In a JavaScript action
130
+
131
+ The same schema, on the side that decides. An action **returns** its failure
132
+ rather than throwing one, so one line turns that into the error the form
133
+ already understands:
134
+
135
+ ```ts title="src/actions.ts"
136
+ 'use server'
137
+
138
+ import { schema } from './schema';
139
+
140
+ export async function createPost(title: string, body: string) {
141
+ const parsed = await schema['~standard'].validate({ title, body });
142
+
143
+ if (parsed.issues) {
144
+ return { ok: false as const, issues: parsed.issues };
145
+ }
146
+
147
+ return { ok: true as const, post: await savePost(parsed.value) };
148
+ }
149
+ ```
150
+
151
+ ```tsx title="src/components/PostForm.tsx"
152
+ import { ServerValidationError } from '@rsc-kit/core/errors';
153
+ import { issuesToErrors } from '@rsc-kit/core/standardSchema';
154
+
155
+ async function submit(formData: FormData) {
156
+ const result = await createPost(
157
+ formData.get('title') as string,
158
+ formData.get('body') as string,
159
+ );
160
+
161
+ if (!result.ok) throw new ServerValidationError('Validation failed', issuesToErrors(result.issues));
162
+
163
+ return result.post;
164
+ }
165
+ ```
166
+
167
+ `issuesToErrors` is the same mapping the client-side check uses, so the field
168
+ names match whichever side produced them.
169
+
170
+ ## ServerValidationError
171
+
172
+ | Member | Type | What it holds |
173
+ | --- | --- | --- |
174
+ | `errors` | `Record<string, string[]>` | Field name to messages. |
175
+ | `message` | `string` | A summary, for when there is nowhere to put field errors. |
@@ -0,0 +1,120 @@
1
+ # View transitions
2
+
3
+ > What React's ViewTransition animates in an app built with this, and what it does not.
4
+
5
+ React 19.3 ships [`<ViewTransition>`](https://react.dev/reference/react/ViewTransition),
6
+ which animates a change by handing it to the browser's View Transition API.
7
+ You can use it here. There is nothing to turn on, and no wrapper this package
8
+ provides — it is React's component, used directly.
9
+
10
+ It has one boundary worth knowing before you reach for it, because the failure
11
+ is silence: a transition that does not run looks exactly like one you styled
12
+ badly.
13
+
14
+ | the change comes from | animates |
15
+ | --- | --- |
16
+ | your own `useState`, inside `startTransition` | yes |
17
+ | a server action's result, put into state | yes |
18
+ | `<Form>` — errors, success, optimistic updates | yes |
19
+ | a streamed `<Suspense>` boundary arriving | yes, React does this on its own |
20
+ | navigating to another page | yes, behind a flag |
21
+
22
+ ## What works
23
+
24
+ Ordinary React. Give the changing element a `key` so React sees a replacement
25
+ rather than an edit, and commit inside `startTransition`:
26
+
27
+ ```tsx
28
+ 'use client';
29
+
30
+ import { useState, startTransition, ViewTransition } from 'react';
31
+ import { addToTotal } from '../actions';
32
+
33
+ export function Total() {
34
+ const [total, setTotal] = useState<number | null>(null);
35
+
36
+ return (
37
+ <>
38
+ <ViewTransition>
39
+ <p key={String(total)}>{total ?? '—'}</p>
40
+ </ViewTransition>
41
+
42
+ <button
43
+ onClick={() =>
44
+ startTransition(async () => {
45
+ const next = await addToTotal(1);
46
+
47
+ startTransition(() => setTotal(next));
48
+ })
49
+ }
50
+ >
51
+ add
52
+ </button>
53
+ </>
54
+ );
55
+ }
56
+ ```
57
+
58
+ A server action is in the working column for a reason worth stating: its result
59
+ comes back as an ordinary return value, and what you do with it is `useState`.
60
+ That makes it a transition like any other. `<Form>` is built on `useState`
61
+ throughout, so everything it drives animates the same way.
62
+
63
+ ## Navigating between pages
64
+
65
+ Off by default, because it changes how every navigation commits:
66
+
67
+ ```ts title="vite.config.ts"
68
+ rscKit({ viewTransitions: true })
69
+ ```
70
+
71
+ A build-time constant rather than a runtime setting, so an app that does not
72
+ ask for it does not carry the boundary at all. What it animates is the segment
73
+ a navigation replaces; what a page does inside itself needs no flag.
74
+
75
+ ## Coming back to a page you were just on
76
+
77
+ A navigation to a page still being held reveals it rather than refetching it,
78
+ so the form you were filling in is still filled in. That applies to a link, not
79
+ only the back button — having one keep your work and the other throw it away is
80
+ a distinction nobody makes while using an app.
81
+
82
+ It is bounded, because the two halves pull against each other: what comes back
83
+ is the tree from when you left, so its data is from then. Thirty seconds by
84
+ default, which covers leaving a form to check something and coming straight
85
+ back. Past that a link refetches.
86
+
87
+ ```ts
88
+ import { setRevealWindow } from '@rsc-kit/core/navigate';
89
+
90
+ setRevealWindow(0); // never reveal — every link is a fresh request
91
+ ```
92
+
93
+ The back button is not bounded. It names a moment, and the page from that
94
+ moment is the right answer however old it is.
95
+
96
+ <Aside type="caution" title="It can hide a server that is down">
97
+ Revealing a held page fetches nothing, so those navigations keep working
98
+ when the backend does not. Measured with the process killed: a page visited
99
+ moments ago still navigates, one never visited does not. That is a reveal
100
+ window rather than offline support, and it means a dead server can go
101
+ unnoticed for as long as the window lasts — [`useOffline`](/guides/offline)
102
+ is what surfaces it. Actually surviving with no network is
103
+ [`offline: true`](/guides/offline#surviving-without-one), which is a service
104
+ worker and a different thing entirely.
105
+ </Aside>
106
+
107
+ ## Requirements
108
+
109
+ `react` and `react-dom` at **19.3 or newer**. A scaffolded app pins `^19.2`, so
110
+ this is an upgrade:
111
+
112
+ ```bash
113
+ npm install react@^19.3 react-dom@^19.3
114
+ ```
115
+
116
+ React also documents a [`browser`](https://react.dev/reference/react-dom/browser)
117
+ api for rendering a component only in the browser — the right tool for a value
118
+ the server cannot know. It is not in 19.3 yet; see
119
+ [static generation](/guides/static-generation#a-value-that-must-not-be-frozen)
120
+ for what to do meanwhile.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/mcp",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "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.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,12 +23,13 @@
23
23
  }
24
24
  },
25
25
  "files": [
26
- "dist"
26
+ "dist",
27
+ "guides"
27
28
  ],
28
29
  "scripts": {
29
30
  "test": "bun test tests",
30
31
  "typecheck": "tsc --noEmit",
31
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
32
+ "build": "rm -rf dist guides && tsc -p tsconfig.build.json && node scripts/bundle-guides.mjs",
32
33
  "prepack": "bun run build"
33
34
  },
34
35
  "dependencies": {