@rsc-kit/mcp 0.28.0 → 0.29.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/testing.md CHANGED
@@ -177,6 +177,29 @@ above. For the round trip itself, and for anything that depends on hydration —
177
177
  `useField` re-rendering, a navigation, an optimistic update reverting — use
178
178
  Playwright against `vite preview`, which serves the real build.
179
179
 
180
+ ## What a page watches
181
+
182
+ `watched()` renders a page and says what each region refreshes on - `page`
183
+ for the page's own `refreshOn`, a section's name for each section's - without
184
+ reading the signed names out of the payload:
185
+
186
+ ```ts
187
+ expect(await app.watched('/teams/acme')).toEqual({ page: ['team:acme'], apps: ['team:acme:apps'] })
188
+ ```
189
+
190
+ With a Go or Laravel backend stood in by `host`, `testChanges()` answers the
191
+ version questions the way the backend would, and its `changed` says a name
192
+ moved - what the backend's webhook would do:
193
+
194
+ ```ts
195
+ import { createTestApp, testChanges } from '@rsc-kit/core/testing'
196
+
197
+ const names = testChanges()
198
+ const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })
199
+
200
+ names.changed('team:1:repos')
201
+ ```
202
+
180
203
  ## What to test where
181
204
 
182
205
  | what | how |
@@ -186,5 +209,6 @@ Playwright against `vite preview`, which serves the real build.
186
209
  | an api route | call `GET`/`POST` with a `Request` |
187
210
  | anything that reads cookies or headers | `withRequest()` |
188
211
  | routing, guards, stored pages, status codes | `createTestApp().fetch()` |
212
+ | what a page refreshes on | `createTestApp().watched()` |
189
213
  | a client component | React + a DOM |
190
214
  | an action over the wire, hydration, navigation | Playwright |
@@ -44,6 +44,14 @@ change:
44
44
  nitro({ preset: 'vercel', serveStatic: 'inline' })
45
45
  ```
46
46
 
47
+ ## Streams on serverless platforms
48
+
49
+ `events()` routes and `refreshOn` hold a connection open. On Workers and
50
+ serverless functions each isolate or instance watches for its own tabs, a
51
+ platform's request limits cut streams (they reconnect and catch up), and
52
+ versions must live in a shared store. [On Workers and serverless](/guides/live-data#on-workers-and-serverless)
53
+ has the details.
54
+
47
55
  ## Running it
48
56
 
49
57
  ```bash
@@ -66,6 +66,15 @@ run *as the visitor*: your session middleware reads the cookie and finds the
66
66
  same person the page is being rendered for. During a build there is no
67
67
  visitor and the headers are absent.
68
68
 
69
+ **Arguments that do not fit the function are a failure, not a refusal.**
70
+ Too few, or a value your language cannot decode into the parameter's type:
71
+ answer 500 with `error`, as Go and Laravel do. The renderer's calls are typed
72
+ from your own manifest, so a mismatch is a bug in a caller - most often a
73
+ manifest older than the code - and a 500 is what makes it show up in your
74
+ logs. Extra arguments may be ignored. Values that decode but break your
75
+ rules - an email that is not one - are the input being invalid: a 422 with
76
+ `validationErrors`, below.
77
+
69
78
  **Check the secret first, in constant time, and refuse before dispatch.** This
70
79
  endpoint runs functions by name with none of your routing in front of it;
71
80
  `hash_equals('', '')` is true in PHP and its equivalents elsewhere, so an
@@ -89,7 +98,7 @@ message to tell an invalid form from a broken server:
89
98
  | the input is invalid | 422 | `validationErrors: { "email": ["…"], "address.city": ["…"] }`, `error` |
90
99
  | there is no session | 401 | `unauthenticated: true`, `error` |
91
100
  | there is one, and still no | 403 | `unauthorized: true`, `error` |
92
- | go somewhere else | **200** | `redirect: "/login"`, optionally `redirectStatus` (default 307) |
101
+ | go somewhere else | **200** | `redirect: "/login"`, and `redirectStatus` when you mean something other than 307 |
93
102
  | refused, with its own status | that status | `error`, `refusalStatus: 429` (any 4xx or 5xx: a 503 while a dependency is down) |
94
103
  | the function failed | 500 | `error`, and in development `debug` |
95
104
 
@@ -100,7 +109,7 @@ and under `""` for a message about the form itself. `unauthenticated` and
100
109
  `ServerAuthorizationError`, so a page answers 401 or 403 the way it would
101
110
  have if a JavaScript guard had thrown them. `redirect` travels the path every
102
111
  other redirect travels — a real 3xx above a Suspense boundary, a digest below
103
- one. `revalidate` names [sections](/guides/sections) or `page`, and the answer
112
+ one. `revalidate` names [sections](/guides/live-data) or `page`, and the answer
104
113
  to the action carries the re-rendered region with it rather than the browser
105
114
  being told to ask again.
106
115
 
@@ -108,7 +117,10 @@ Two things that are easy to get wrong:
108
117
 
109
118
  **A redirect is a 200.** An HTTP client follows a 3xx transparently, so a real
110
119
  one here would send the host call itself to the destination and hand whatever
111
- it found back to the render as the function's result.
120
+ it found back to the render as the function's result. The redirect's own
121
+ status goes in `redirectStatus`; without it the renderer uses 307, which keeps
122
+ the method. Send it whenever your framework says something else - Laravel's
123
+ redirects are 302, so the Laravel package always sends it.
112
124
 
113
125
  **Refusing is not failing.** A form filled in wrongly is the ordinary case, and
114
126
  `validationErrors` is checked before `error` — a reply carrying both is read as
@@ -169,11 +181,19 @@ whole-batch form every call in it waits for the slowest.
169
181
 
170
182
  Either way, run every call and answer every one — a refusal in the second is
171
183
  that call's answer, not a reason to leave the third out. Each call's
172
- `revalidate` stays with that call. A backend that has not implemented
173
- batches at all loses nothing but the saving: the renderer reads its "no
174
- function name" answer as "no batches here" and sends single calls from then
175
- on. Batches never mix visitors; every call in one carried the same forwarded
176
- headers.
184
+ `revalidate` stays with that call. Batches never mix visitors; every call in
185
+ one carried the same forwarded headers.
186
+
187
+ **At most 50 calls.** The renderer never sends more in one batch; a page with
188
+ more is split. Refusing a larger one - 413, before running anything - is a
189
+ safe limit, and Laravel's.
190
+
191
+ **Batches are not optional.** The renderer always sends calls from the same
192
+ tick together, and an answer that is neither form - whatever its status -
193
+ fails every call in the batch with what your backend said. None is sent
194
+ again one by one: they may have run, and an action run twice is worse than
195
+ one reported as failed. The conformance suite checks batches, so a backend
196
+ that passes it takes them.
177
197
 
178
198
  ## Route middleware
179
199
 
@@ -222,7 +242,9 @@ an action. Every open tab showing it refreshes, with nothing polling. Two
222
242
  things to provide:
223
243
 
224
244
  **A way to say a name changed**, callable from anywhere in your backend. It
225
- moves the name's version: a number, kept somewhere every instance of your
245
+ moves the name's version to the larger of one past where it was and the
246
+ current time in milliseconds - so a version never comes round again, and an
247
+ old name can be deleted at any time - kept somewhere every instance of your
226
248
  backend shares - a cache, a table - since a webhook lands on whichever
227
249
  instance the balancer picked. A name never changed is at 0.
228
250
 
@@ -236,7 +258,11 @@ how long it would wait:
236
258
  Answer `{ "result": { "versions": { ... } } }` with every name in `since`
237
259
  whose version differs now, and nothing else - an empty object when none
238
260
  does. If you can, hold the call up to `wait` milliseconds for one to move
239
- and answer the moment it does: that is what makes the refresh immediate. If
261
+ and answer the moment it does: that is what makes the refresh immediate.
262
+ The renderer asks with 5 seconds; hold no longer than `wait`, and never past
263
+ 30 seconds, where the renderer gives up on any call. A name you have never
264
+ seen is at 0, compared like any other: held while the asker also holds 0,
265
+ answered the moment it is changed. If
240
266
  you cannot - PHP-FPM, a worker that must answer and go - answer at once; the
241
267
  renderer asks again on its interval, a couple of seconds apart. Both are
242
268
  conformant, and the page is the same.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/mcp",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.30.0",
37
+ "create-rsc-kit": "^0.29.0",
37
38
  "zod": "^3.25"
38
39
  },
39
40
  "devDependencies": {