@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/dist/answers.d.ts +14 -1
- package/dist/answers.js +76 -34
- package/dist/answers.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +434 -14
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +12 -0
- package/dist/report.js +17 -15
- package/dist/report.js.map +1 -1
- package/guides/api-routes.md +106 -6
- package/guides/authorization.md +39 -0
- package/guides/backend-answered-pages.md +163 -0
- package/guides/coming-from-next.md +12 -4
- package/guides/deployment.md +129 -0
- package/guides/domains.md +129 -0
- package/guides/emails.md +90 -0
- package/guides/errors.md +16 -2
- package/guides/go.md +194 -0
- package/guides/images.md +10 -4
- package/guides/index.json +50 -0
- package/guides/installation.md +31 -13
- package/guides/instrumentation.md +91 -0
- package/guides/introduction.md +4 -0
- package/guides/laravel.md +406 -0
- package/guides/mcp.md +1 -1
- package/guides/metadata.md +1 -1
- package/guides/offline.md +8 -0
- package/guides/queries.md +138 -8
- package/guides/quick-start.md +31 -0
- package/guides/redirects.md +14 -0
- package/guides/response-headers.md +22 -0
- package/guides/routing.md +5 -1
- package/guides/seo-files.md +140 -0
- package/guides/typed-routes.md +26 -0
- package/guides/view-transitions.md +53 -7
- package/guides/where-it-runs.md +155 -0
- package/guides/your-own-backend.md +238 -0
- package/package.json +2 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Your own backend
|
|
2
|
+
|
|
3
|
+
> The one endpoint a backend in any language answers.
|
|
4
|
+
|
|
5
|
+
The model is a [BAP — Backend-Answered Pages](/hosts/backend-answered-pages);
|
|
6
|
+
this page is the wire.
|
|
7
|
+
|
|
8
|
+
There is no adapter to write on the JavaScript side. The renderer is what
|
|
9
|
+
Nitro builds, on every host, and it already knows how to ask a backend for
|
|
10
|
+
things: a server component calls `rpc()`, and the call leaves the process as
|
|
11
|
+
an ordinary POST. What a backend implements is that one endpoint. Laravel's is
|
|
12
|
+
[about 400 lines of PHP](/hosts/laravel); [Go's](/hosts/go) is about the same.
|
|
13
|
+
|
|
14
|
+
This page is the contract, so a Rails, Django, .NET or Elixir application can
|
|
15
|
+
answer it the same way.
|
|
16
|
+
|
|
17
|
+
## Telling the renderer where you are
|
|
18
|
+
|
|
19
|
+
Two variables, and the renderer wires itself. Both in development — `vite`
|
|
20
|
+
reads the project's `.env` — and in production, where the built server reads
|
|
21
|
+
its process environment:
|
|
22
|
+
|
|
23
|
+
```ini title=".env"
|
|
24
|
+
RSC_BACKEND=http://127.0.0.1:8080
|
|
25
|
+
RSC_HOST_CALL_SECRET=a-long-random-string
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`APP_URL` is read where `RSC_BACKEND` is absent, which is what makes a Laravel
|
|
29
|
+
app need nothing extra. Optional beside them: `RSC_HOST_CALL_PATH` (default
|
|
30
|
+
`/__rsc/host-call`) and `RSC_HOST_GLOBAL` (default `rpc`, the name server
|
|
31
|
+
components call). Both, or neither: a secret without a backend has nowhere to
|
|
32
|
+
go, and a backend without a secret is refused at the door — the renderer does
|
|
33
|
+
not gate on one of the pair alone.
|
|
34
|
+
|
|
35
|
+
Both are read when the first request arrives, not when the module loads, so
|
|
36
|
+
they work wherever the built server runs: a process with an environment, or a
|
|
37
|
+
Worker, where Nitro maps the bindings in `wrangler.json` — a `var` for the
|
|
38
|
+
address, a `secret` for the secret — onto `process.env` per request. On a
|
|
39
|
+
Worker the backend has to be reachable from Cloudflare's network, so it is an
|
|
40
|
+
`https` origin behind the secret rather than a loopback address.
|
|
41
|
+
|
|
42
|
+
Then the plugin's `hostCall` option overrides any of it for a setup that
|
|
43
|
+
would rather not use the environment:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
rscKit({ hostCall: { endpoint: 'http://127.0.0.1:8080', secret, path: '/__rsc/host-call' } })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## The request
|
|
50
|
+
|
|
51
|
+
```http
|
|
52
|
+
POST /__rsc/host-call
|
|
53
|
+
Content-Type: application/json
|
|
54
|
+
X-Rsc-Host-Secret: a-long-random-string
|
|
55
|
+
Cookie: <the visitor's, forwarded unchanged>
|
|
56
|
+
Authorization: <likewise, if the page request had one>
|
|
57
|
+
|
|
58
|
+
{ "function": "Orders.recent", "args": [5] }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`function` is whatever the component passed to `rpc()`; the naming scheme is
|
|
62
|
+
yours. `args` is positional, exactly as passed. `Cookie` and `Authorization`
|
|
63
|
+
are the only headers copied from the page request — everything else either
|
|
64
|
+
describes this POST or is meaningless to you — and they are what let the call
|
|
65
|
+
run *as the visitor*: your session middleware reads the cookie and finds the
|
|
66
|
+
same person the page is being rendered for. During a build there is no
|
|
67
|
+
visitor and the headers are absent.
|
|
68
|
+
|
|
69
|
+
**Check the secret first, in constant time, and refuse before dispatch.** This
|
|
70
|
+
endpoint runs functions by name with none of your routing in front of it;
|
|
71
|
+
`hash_equals('', '')` is true in PHP and its equivalents elsewhere, so an
|
|
72
|
+
unconfigured secret has to be rejected before the comparison rather than
|
|
73
|
+
trusted to it. Better still, do not register the endpoint at all when no
|
|
74
|
+
secret is configured — absent, not open.
|
|
75
|
+
|
|
76
|
+
## The reply
|
|
77
|
+
|
|
78
|
+
JSON, and the fields keep the outcomes apart so the renderer never reads a
|
|
79
|
+
message to tell an invalid form from a broken server:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "result": [ … ], "revalidate": ["orders"] }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| you want to say | status | fields |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| here is the answer | 200 | `result` |
|
|
88
|
+
| …and the action made these regions stale | 200 | `result`, `revalidate: ["orders", "page"]` |
|
|
89
|
+
| the input is invalid | 422 | `validationErrors: { "email": ["…"], "address.city": ["…"] }`, `error` |
|
|
90
|
+
| there is no session | 401 | `unauthenticated: true`, `error` |
|
|
91
|
+
| there is one, and still no | 403 | `unauthorized: true`, `error` |
|
|
92
|
+
| go somewhere else | **200** | `redirect: "/login"`, optionally `redirectStatus` (default 307) |
|
|
93
|
+
| a guard refused with its own status | that status | `error`, `refusalStatus: 429` |
|
|
94
|
+
| the function failed | 500 | `error` |
|
|
95
|
+
|
|
96
|
+
What each becomes on the other side: `validationErrors` reaches the form
|
|
97
|
+
that submitted, each message under its input, dot-joined for a nested field
|
|
98
|
+
and under `""` for a message about the form itself. `unauthenticated` and
|
|
99
|
+
`unauthorized` become the engine's own `ServerAuthenticationError` and
|
|
100
|
+
`ServerAuthorizationError`, so a page answers 401 or 403 the way it would
|
|
101
|
+
have if a JavaScript guard had thrown them. `redirect` travels the path every
|
|
102
|
+
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
|
|
104
|
+
to the action carries the re-rendered region with it rather than the browser
|
|
105
|
+
being told to ask again.
|
|
106
|
+
|
|
107
|
+
Two things that are easy to get wrong:
|
|
108
|
+
|
|
109
|
+
**A redirect is a 200.** An HTTP client follows a 3xx transparently, so a real
|
|
110
|
+
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.
|
|
112
|
+
|
|
113
|
+
**Refusing is not failing.** A form filled in wrongly is the ordinary case, and
|
|
114
|
+
`validationErrors` is checked before `error` — a reply carrying both is read as
|
|
115
|
+
a refusal with fields, not a failure with none. Reserve `error` alone, with a
|
|
116
|
+
500, for the thing the visitor did not cause.
|
|
117
|
+
|
|
118
|
+
## Batches
|
|
119
|
+
|
|
120
|
+
Calls issued in the same tick of a render — sibling components each awaiting
|
|
121
|
+
`rpc()` — arrive as one POST, so a page's parallel reads cost you one request
|
|
122
|
+
rather than one each:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{ "calls": [ { "function": "Orders.recent", "args": [5] }, { "function": "Me.profile", "args": [] } ] }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Answer with `replies`, one per call in order, each the reply it would have had
|
|
129
|
+
alone plus the `status` it would have carried:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "replies": [ { "status": 200, "result": [ … ] }, { "status": 401, "unauthenticated": true } ] }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Run every call, in order, and answer every one — a refusal in the second is
|
|
136
|
+
that call's answer, not a reason to leave the third out. Each call's
|
|
137
|
+
`revalidate` stays with that call. A backend that has not implemented this
|
|
138
|
+
loses nothing but the saving: the renderer reads its "no function name" answer
|
|
139
|
+
as "no batches here" and sends single calls from then on. Batches never mix
|
|
140
|
+
visitors; every call in one carried the same forwarded headers.
|
|
141
|
+
|
|
142
|
+
## Route middleware
|
|
143
|
+
|
|
144
|
+
A `middleware.ts` beside or above a page may name guards in your vocabulary:
|
|
145
|
+
|
|
146
|
+
```ts title="app/admin/middleware.ts"
|
|
147
|
+
export const middleware = ['auth', 'can:manage-users']
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Only the names, no default export: the engine's own guards are a
|
|
151
|
+
`middleware.ts` *default export*, a function it runs itself, and a file may
|
|
152
|
+
carry either or both. (`route.ts` may carry the names too, from before
|
|
153
|
+
`middleware.ts` could.)
|
|
154
|
+
|
|
155
|
+
The renderer does not know what those mean. Before anything at or below that
|
|
156
|
+
directory renders — including a page frozen at build time, before the file is
|
|
157
|
+
served — it calls the reserved function with the list:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{ "function": "__rsc.middleware", "args": [["auth", "can:manage-users"]] }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Answer `{ "result": true }` to let the render go ahead. **Anything else is a
|
|
164
|
+
refusal**: `false`, `null`, a string, an object, a 4xx, a connection error.
|
|
165
|
+
The engine reads the literal `true` and nothing else, so a guard that aborts,
|
|
166
|
+
redirects or simply throws keeps the page from rendering rather than being
|
|
167
|
+
read as silence. Refuse with the fields above — `unauthenticated` when there
|
|
168
|
+
is no session, `redirect` to send them to sign in, `refusalStatus` for a
|
|
169
|
+
throttle's 429 — and the page answers accordingly.
|
|
170
|
+
|
|
171
|
+
Run them in order, outermost first, and stop at the first refusal: an outer
|
|
172
|
+
guard saying no means the inner one should never have been asked.
|
|
173
|
+
|
|
174
|
+
## Server actions
|
|
175
|
+
|
|
176
|
+
A `"use server"` function the browser can call is a name the renderer forwards
|
|
177
|
+
to you. Write the map to `rsc-host-actions.json` at the project root before
|
|
178
|
+
each build — the JavaScript name to whatever your side dispatches on:
|
|
179
|
+
|
|
180
|
+
```json title="rsc-host-actions.json"
|
|
181
|
+
{ "ordersCancel": "Orders.cancel", "profileUpdate": "Profile.update" }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The build writes `server-actions.generated.ts` in the source directory
|
|
185
|
+
exporting each as an action, so a client component imports `ordersCancel`
|
|
186
|
+
and calls it. Regenerate the file as part of `build` rather than by hand: a
|
|
187
|
+
stale map names a method that has since been renamed, and nothing fails until
|
|
188
|
+
the browser calls it. Laravel's `rsc:action-manifest` is this step; a Go
|
|
189
|
+
registry writes the same file from the names it holds.
|
|
190
|
+
|
|
191
|
+
## Urls you own
|
|
192
|
+
|
|
193
|
+
The renderer forwards any url the route tree does not own — `/login`, a
|
|
194
|
+
webhook, an uploaded file — to `RSC_BACKEND`, with `X-Forwarded-Host` and
|
|
195
|
+
`X-Forwarded-Proto` set and the header `x-rsc-renderer-fallback: 1`. Trust
|
|
196
|
+
the renderer as a proxy so your absolute urls come out against the public
|
|
197
|
+
origin.
|
|
198
|
+
|
|
199
|
+
If your application also proxies to the renderer — sitting in front of it,
|
|
200
|
+
the way Laravel does with `RSC_RENDERER_URL` — two things keep a url neither
|
|
201
|
+
side owns from bouncing between you forever:
|
|
202
|
+
|
|
203
|
+
- Set `x-rsc-proxied-by-backend: 1` on what you forward. The renderer answers
|
|
204
|
+
404 itself instead of handing it back.
|
|
205
|
+
- When a request arrives carrying `x-rsc-renderer-fallback`, answer 404 for
|
|
206
|
+
anything you do not route. It has already been through the renderer's table.
|
|
207
|
+
|
|
208
|
+
Whichever process faces the internet, the host-call endpoint must not:
|
|
209
|
+
restrict it at the web server, bind the listener to loopback, or serve it on
|
|
210
|
+
a unix socket. The secret is the layer the protocol guarantees; the network
|
|
211
|
+
is the one it cannot.
|
|
212
|
+
|
|
213
|
+
## What the renderer expects of you
|
|
214
|
+
|
|
215
|
+
- **Answer within 30 seconds.** A render blocked on a host that never answers
|
|
216
|
+
is a hung request, and the renderer gives up at 30s (`timeoutMs` on
|
|
217
|
+
`httpHostCalls`, for a host that embeds the engine itself).
|
|
218
|
+
- **One process is not enough if you proxy.** A server that proxies a page
|
|
219
|
+
to the renderer holds a worker for the whole render, and the render calls
|
|
220
|
+
back to that same server for its data. With one worker, nobody is left to
|
|
221
|
+
answer. Laravel refuses `php artisan serve` for exactly this.
|
|
222
|
+
- **A panic is one failed call.** Recover it into a 500 with `error`; the
|
|
223
|
+
other renders in flight should survive it.
|
|
224
|
+
- **Serialise what you return as JSON.** It is decoded on the other side as
|
|
225
|
+
whatever `rpc<T>()` was told it is; there is no schema between you.
|
|
226
|
+
|
|
227
|
+
## Testing it without the renderer
|
|
228
|
+
|
|
229
|
+
The contract is plain HTTP, so a backend's own test suite can cover it
|
|
230
|
+
without a JavaScript process: POST the request shape, assert the reply
|
|
231
|
+
shape. Laravel's Pest suite does this; the Go adapter's `go test` does the
|
|
232
|
+
same. The end-to-end proof — a real page rendered with data from your
|
|
233
|
+
process — lives with the engine, which is where a rendering regression can be
|
|
234
|
+
caught.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
Reference implementations: [Laravel](/hosts/laravel) and [Go](/hosts/go).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rsc-kit/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"guides"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "bun test tests",
|
|
30
|
+
"test": "tsc --noEmit && bun test tests",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"build": "rm -rf dist guides && tsc -p tsconfig.build.json && node scripts/bundle-guides.mjs",
|
|
33
33
|
"prepack": "bun run build"
|