@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/dist/index.js +31 -0
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +63 -7
- package/dist/recipes.js.map +1 -1
- package/guides/deployment.md +2 -2
- package/guides/go.md +36 -5
- package/guides/index.json +5 -0
- package/guides/laravel.md +1 -1
- package/guides/live-data.md +885 -0
- package/guides/mcp.md +38 -0
- package/guides/sections.md +4 -607
- package/guides/testing.md +24 -0
- package/guides/where-it-runs.md +8 -0
- package/guides/your-own-backend.md +36 -10
- package/package.json +2 -1
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 |
|
package/guides/where-it-runs.md
CHANGED
|
@@ -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"`,
|
|
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/
|
|
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.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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": {
|