@rsc-kit/mcp 0.1.0 → 0.12.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/recipes.js +23 -1
- package/dist/recipes.js.map +1 -1
- package/package.json +10 -1
package/dist/recipes.js
CHANGED
|
@@ -305,7 +305,29 @@ Sizes are read from the filename. The build says whether it worked:
|
|
|
305
305
|
There is no layout to edit — React hoists the tags into <head>.
|
|
306
306
|
|
|
307
307
|
Pair it with \`offline: true\`. They are separate options because they are
|
|
308
|
-
separate decisions
|
|
308
|
+
separate decisions.
|
|
309
|
+
|
|
310
|
+
**Push and background sync** need listeners the generated worker does not have,
|
|
311
|
+
so it imports yours from \`src/app/sw.js\` — plain javascript, evaluated by the
|
|
312
|
+
browser with no build step in front of it:
|
|
313
|
+
|
|
314
|
+
\`\`\`js
|
|
315
|
+
self.addEventListener('push', (event) => {
|
|
316
|
+
const payload = event.data ? event.data.json() : {}
|
|
317
|
+
|
|
318
|
+
event.waitUntil(self.registration.showNotification(payload.title, { body: payload.body }))
|
|
319
|
+
})
|
|
320
|
+
\`\`\`
|
|
321
|
+
|
|
322
|
+
The rest is the web api and \`web-push\`, not this package: VAPID keys, a
|
|
323
|
+
subscribe call behind a button, the subscription stored by a server action
|
|
324
|
+
against a USER rather than a session, and a sender that deletes an endpoint on
|
|
325
|
+
404 or 410 rather than retrying a dead one forever.
|
|
326
|
+
|
|
327
|
+
For background sync, make the endpoint idempotent. The browser decides when a
|
|
328
|
+
sync runs and may run it more than once — a request that reached the server
|
|
329
|
+
whose response did not arrive is retried, and if that posts a message twice the
|
|
330
|
+
person sent it twice.`,
|
|
309
331
|
},
|
|
310
332
|
{
|
|
311
333
|
topic: 'no-javascript',
|
package/dist/recipes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"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","sourcesContent":["// How to build the things this framework has, in the shape that works.\n//\n// The other half of this server, and the more useful one. Introspection answers\n// \"what did my build do\"; this answers \"how do I do X here\", which is the\n// question an agent actually has — and the one it otherwise answers from Next\n// and React habits that produce code which looks right and is not.\n//\n// Long-form on purpose. AGENTS.md has to be short enough to sit in context for\n// every turn, so it can only say the rule. These are fetched when the topic\n// comes up, so they can afford the working example and the caveat under it.\n//\n// Every snippet here is the recommended spelling from the guides, not a\n// paraphrase. When a guide changes, this changes with it — a recipe that has\n// drifted is worse than no recipe, because it is followed with confidence.\n\nexport interface Recipe {\n topic: string\n summary: string\n body: string\n}\n\nconst RECIPES: Recipe[] = [\n {\n topic: 'forms',\n summary: 'Submitting to a server action, with pending state and field errors',\n body: `Use <Form>. It takes the server action itself, not a url.\n\n\\`\\`\\`tsx\n'use client'\nimport Form from '@rsc-kit/core/Form'\nimport { createPost } from '../actions'\n\nexport function NewPost() {\n return (\n <Form action={createPost} schema={schema}>\n {({ pending, errors }) => (\n <>\n <input name=\"title\" />\n {errors.title?.[0] && <p>{errors.title[0]}</p>}\n <button disabled={pending}>Save</button>\n </>\n )}\n </Form>\n )\n}\n\\`\\`\\`\n\nPassing \\`schema\\` validates in the browser BEFORE the action is called, so a\nmistake costs no round trip. It is a courtesy, never a control: the action is a\npublic endpoint reachable without your form, so the server must check too.\n\nA schema on the server (\\`client.input(schema)\\`) does NOT give you client-side\nvalidation. Pass it to the form as well — the same schema is fine.\n\nFor imperative control use \\`useForm\\` instead:\n\n\\`\\`\\`ts\nconst form = useForm({ title: '' }, { schema })\nawait form.submit(createPost)\n\\`\\`\\`\n\nIt works with no javascript at all: the form posts, the action runs, the page\nre-renders. That is why the fields are real \\`name\\` attributes rather than\ncontrolled state.`,\n },\n {\n topic: 'prefetch',\n summary: 'Making a navigation feel instant',\n body: `\\`<Link>\\` prefetches on hover by default. Usually there is nothing to do.\n\n\\`\\`\\`tsx\nimport Link from '@rsc-kit/core/Link'\n\n<Link href=\"/orders\">Orders</Link>\n<Link href=\"/orders\" prefetch={false}>Orders</Link> // opt out\n<Link href=\"/orders\" cacheFor={30_000}>Orders</Link> // hold the payload longer\n\\`\\`\\`\n\n\\`href\\` is typed to the routes the build found, so a link to a page that no\nlonger exists stops compiling. Cast with \\`as Href\\` only when the destination\nis genuinely computed.\n\nTo prefetch from code — a row about to be clicked, a wizard's next step:\n\n\\`\\`\\`ts\nimport { prefetch } from '@rsc-kit/core/navigate'\n\nprefetch('/orders/42')\n\\`\\`\\`\n\nWhat is prefetched is the RSC payload, not the html, so it is small and it warms\nthe same cache the navigation will read.`,\n },\n {\n topic: 'validation',\n summary: 'Checking input — forms, actions, urls and request bodies',\n body: `One contract everywhere: any Standard Schema (Zod, Valibot, ArkType).\n\n**Actions** validate on arrival and RETURN their failures, because React strips\na thrown message in production:\n\n\\`\\`\\`ts\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\n\\`\\`\\`\n\n**Urls** validate by exporting a schema beside the page or route:\n\n\\`\\`\\`ts\nexport const params = z.object({ slug: z.string().min(1) })\nexport const searchParams = z.object({ page: z.coerce.number().int().min(1).default(1) })\n\\`\\`\\`\n\nValues arrive parsed and typed — \\`?page=3\\` is the number 3, a missing one is\nthe default. Never hand-parse \\`Number(searchParams.get('page'))\\`.\n\n**Api route bodies** the same way:\n\n\\`\\`\\`ts\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function POST(request: Request, { body }) {\n const { title } = await body\n}\n\\`\\`\\`\n\nThe failures answer differently on purpose:\n\n bad params 404 — the url does not describe a page\n bad searchParams the error boundary (400 for an api route)\n bad body 422, the status an action already uses\n\nA bad query is deliberately NOT a 404, or one bad link makes a real page look\ndeleted.`,\n },\n {\n topic: 'action-client',\n summary: 'Middleware for server actions, so a check cannot be forgotten',\n body: `\\`\\`\\`ts title=\"src/server/client.ts\"\n'use server'\nimport { createActionClient } from '@rsc-kit/core/action'\n\nexport const client = createActionClient({ onError: report })\n .use(async ({ next }) => {\n const user = await currentUser()\n\n if (!user) throw new ServerAuthenticationError()\n\n return next({ ctx: { user } })\n })\n\\`\\`\\`\n\n\\`\\`\\`ts title=\"src/server/posts.ts\"\n'use server'\nimport { client } from './client'\n\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\nexport const getPosts = client.query(async ({ ctx }) => …)\n\\`\\`\\`\n\n\\`.handler()\\` is a mutation (POST). \\`.query()\\` is a read (GET). Both run the\nchain, so \\`ctx.user\\` is typed and non-null inside them.\n\nThe point is not convenience. An action cannot be added without the check,\nbecause there is no other constructor to reach for.\n\nStack clients for a narrower rule:\n\n\\`\\`\\`ts\nexport const admin = client.use(async ({ ctx, next }) => {\n if (!ctx.user.isAdmin) throw new ServerAuthorizationError()\n return next({ ctx })\n})\n\\`\\`\\`\n\nRoute \\`middleware.ts\\` does NOT run for actions — an action renders no route.\nThat is why the check goes here.`,\n },\n {\n topic: 'data',\n summary: 'Loading data, streaming it, and when the browser needs to refetch',\n body: `**In a server component, just await it.** No loader, no getServerSideProps.\n\n\\`\\`\\`tsx\nexport default async function Page() {\n const posts = await db.posts.all()\n}\n\\`\\`\\`\n\n**Better: do not await.** Pass the promise down and let a client component\nresolve it — the shell paints at once and the rows stream into the same\nresponse, with no request from the browser:\n\n\\`\\`\\`tsx\nexport default function Page() {\n const posts = getPosts() // not awaited\n\n return (\n <Suspense fallback={<Skeleton />}>\n <List posts={posts} /> {/* 'use client': use(posts) */}\n </Suspense>\n )\n}\n\\`\\`\\`\n\nReach for this first. It is the thing RSC is for.\n\n**When the BROWSER decides to refetch** — a filter, a poll, a refresh — that is\na cache library's job and this package does not ship one:\n\n\\`\\`\\`tsx\nuseQuery({ queryKey: ['posts', kind], queryFn: () => fetchQuery(getPosts, [kind]) })\nuseSWR(['posts', kind], () => fetchQuery(getPosts, [kind]))\n\\`\\`\\`\n\n\\`fetchQuery\\` sends the read as a GET and goes to the server every time, which\nis what a fetcher needs — staleness and revalidation belong to the library\nholding the answer. Do not add a cache on top of it.\n\nKeep the arrow: TanStack calls a bare \\`queryFn\\` with its own context, and a\nserver function serialises whatever it is handed.`,\n },\n {\n topic: 'suspense',\n summary: 'Where boundaries go, and why the build cares',\n body: `A boundary is what lets a page be stored with a hole in it rather than not\nstored at all.\n\n\\`\\`\\`tsx\n<Suspense fallback={<Skeleton />}>\n <Slow />\n</Suspense>\n\\`\\`\\`\n\nOr a \\`loading.tsx\\` beside the page, which is the same thing for the whole\nroute.\n\nThe build renders every page. Whatever has not resolved when the budget expires\nbecomes the hole; everything above it is stored and served instantly. So a page\nwith no boundary above its slow part cannot be stored at all — the build says\nso:\n\n ƒ /orders\n blocks before anything can paint. Add a loading.tsx beside it, or put a\n <Suspense> above the waiting, and it has a skeleton to store.\n\nA boundary does NOT fix a frozen \\`Date.now()\\`. Prerendering renders straight\nthrough a component that never awaits, so the value is captured exactly as\nbefore. A boundary becomes a hole only when something inside it waits.`,\n },\n {\n topic: 'offline',\n summary: 'Service worker, and what it does and does not cache',\n body: `\\`\\`\\`ts title=\"vite.config.ts\"\nrscKit({ offline: true })\n\\`\\`\\`\n\nThe build writes a service worker that precaches the client bundle and caches\npages at runtime — a document fetch warms its payload, a payload fetch warms its\ndocument, so a page reached by a link still works when reloaded offline.\n\nPages the build stored whole are served from the cache FIRST, because they\ncannot change until a deploy and a deploy sweeps the cache. Everything else is\nnetwork-first with the cache as fallback.\n\nNothing marked \\`no-store\\` is ever kept — which is how a guarded page and a\nsession-reading query stay out of a cache that has no notion of who asked.\n\nIn a component:\n\n\\`\\`\\`tsx\nimport { useOnline } from '@rsc-kit/core/useOnline'\n\nconst online = useOnline()\n\\`\\`\\`\n\nThere is no push and no background sync. Push needs a subscription endpoint and\na sender; background sync needs idempotent replay. Both are the app's decisions.`,\n },\n {\n topic: 'pwa',\n summary: 'Making the app installable',\n body: `A manifest file beside the routes:\n\n\\`\\`\\`ts title=\"src/app/manifest.ts\"\nimport type { WebManifest } from '@rsc-kit/core/manifest-file'\n\nexport default {\n name: 'Orders',\n shortName: 'Orders',\n themeColor: '#0b0b0c',\n backgroundColor: '#ffffff',\n} satisfies WebManifest\n\\`\\`\\`\n\nRead at build time, so it must be an object literal — not computed, not\nimported from elsewhere.\n\n**Icons need no listing.** Put them in \\`src/app/\\` and the build finds them:\n\n favicon.ico served at /favicon.ico\n icon-192.png <link rel=\"icon\">, and the manifest's icons\n icon-512.png\n apple-icon.png <link rel=\"apple-touch-icon\">\n opengraph-image.png <meta property=\"og:image\">\n twitter-image.png <meta name=\"twitter:image\">\n\nSizes are read from the filename. The build says whether it worked:\n\n [rsc-kit] manifest: Orders is installable\n [rsc-kit] manifest: no icons, so no browser will offer to install this.\n\nThere is no layout to edit — React hoists the tags into <head>.\n\nPair it with \\`offline: true\\`. They are separate options because they are\nseparate decisions.`,\n },\n {\n topic: 'no-javascript',\n summary: 'Shipping a route with no client runtime at all',\n body: `\\`\\`\\`ts title=\"src/app/about/page.tsx\"\nexport const clientJs = false\n\\`\\`\\`\n\nThe route ships no bootstrap and no client runtime. The build REFUSES it if the\ntree renders a client component, and names the component — they usually come\nfrom a shared layout rather than the page itself.\n\nLinks still work; they are ordinary anchors, so navigation is a full page load.\n\nMost pages do not need this. A page with nothing interactive already ships only\nthe shared runtime, and the size column in the build output tells you what each\none actually costs.`,\n },\n {\n topic: 'api-routes',\n summary: 'HTTP endpoints beside the pages',\n body: `\\`src/app/**/route.ts\\`, one export per method:\n\n\\`\\`\\`ts title=\"src/app/api/posts/[id]/route.ts\"\nexport const params = z.object({ id: z.coerce.number().int() })\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function GET(request: Request, { params }) {\n const { id } = await params\n\n return Response.json(await findPost(id))\n}\n\nexport async function POST(request: Request, { params, body }) {\n const { title } = await body\n\n return Response.json(await createPost(title), { status: 201 })\n}\n\\`\\`\\`\n\nA real \\`Request\\` in, a real \\`Response\\` out. \\`params\\`, \\`searchParams\\` and\n\\`body\\` are awaited, the same way a page's props are.\n\nThey run their directory's \\`middleware.ts\\`, so an endpoint under a guarded\npath is guarded.\n\nA \\`GET\\` that reads nothing from the request is answered from disk. Awaiting\n\\`searchParams\\` says the answer depends on the query; never touching it means\nthe stored answer is served for any query at all.\n\nExporting a \\`body\\` schema consumes the stream, so \\`request.json()\\` inside the\nhandler will find it already read. Use the parsed value.`,\n },\n {\n topic: 'authorization',\n summary: 'Guarding pages, actions, api routes and queries',\n body: `Each entry point defends itself. There is no single place that covers all of\nthem, and believing otherwise is how a hole is left.\n\n**A page or an api route**: \\`middleware.ts\\` in its directory guards everything\nat or below it.\n\n\\`\\`\\`ts title=\"src/app/admin/middleware.ts\"\nimport { redirect } from '@rsc-kit/core/redirect'\n\nexport default async function guard() {\n if (!(await currentUser())) redirect('/login')\n}\n\\`\\`\\`\n\n**An action or a query**: middleware does NOT run — they render no route. Build\nthem from an action client so the check cannot be forgotten. See the\n\\`action-client\\` topic.\n\n**Authorise on identity, not arguments.** \\`deletePost(id)\\` that trusts the id\nis the whole of an IDOR: the caller chooses the id, so check the row belongs to\n\\`ctx.user\\`.\n\nA guarded page can still be frozen at build time — the guard is a serving\ndecision, not a build one. Its response is marked private so no cache keeps it.`,\n },\n {\n topic: 'dynamic',\n summary: 'Why a page is not static, and how to choose',\n body: `A page is stored at build time unless it reads the request. Reading it is what\nopts out, and the accessors are async:\n\n\\`\\`\\`ts\nimport { cookies, headers, searchParams, connection } from '@rsc-kit/core/request'\n\nconst theme = (await cookies()).get('theme')\nawait connection() // \"render this per visitor\", said deliberately\n\\`\\`\\`\n\nA page's \\`params\\` and \\`searchParams\\` props are promises for the same reason.\n\nThe build says which call did it, per route:\n\n ◐ /locale 85 kB\n dynamic — called cookies(), headers()\n\nThat is usually correct — a page whose content depends on who is asking cannot\nbe one stored file. Change it only when the read was accidental.\n\nFor a parameterised route, \\`generateStaticParams\\` turns one shell into a page\nper url:\n\n\\`\\`\\`ts\nexport async function generateStaticParams() {\n return (await db.posts.all()).map((p) => ({ slug: p.slug }))\n}\n\\`\\`\\`\n\nA value that must differ per visitor but needs no server — a clock,\nlocalStorage, a map — belongs in the browser only:\n\n\\`\\`\\`tsx\n'use client'\nimport { browser } from 'react-dom'\n\nfunction Clock() {\n use(browser('the time is the visitor\\\\'s, not the build machine\\\\'s'))\n}\n\\`\\`\\`\n\nIt needs a Suspense boundary, and the page stays frozen.`,\n },\n]\n\n/** Every topic, with one line each — what a caller reads before choosing. */\nexport function listTopics(): string {\n return [\n 'Topics. Ask for one with how_to({ topic }).',\n '',\n ...RECIPES.map((r) => `${r.topic.padEnd(16)} ${r.summary}`),\n ].join('\\n')\n}\n\n/** One recipe, or the list plus a nudge when the topic is not one. */\nexport function howTo(topic: string): string {\n const wanted = topic.trim().toLowerCase().replace(/[\\s_]+/g, '-')\n const found =\n RECIPES.find((r) => r.topic === wanted) ??\n // A near miss is common and worth answering rather than refusing: someone\n // asks for \"form\" or \"queries\" and means the obvious thing.\n RECIPES.find((r) => r.topic.startsWith(wanted) || wanted.startsWith(r.topic)) ??\n RECIPES.find((r) => r.summary.toLowerCase().includes(wanted))\n\n if (!found) return `No topic \"${topic}\".\\n\\n${listTopics()}`\n\n return `# ${found.topic} — ${found.summary}\\n\\n${found.body}`\n}\n\n/** For tests, so a recipe cannot be added without being reachable. */\nexport const TOPICS = RECIPES.map((r) => r.topic)\n"]}
|
|
1
|
+
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"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","sourcesContent":["// How to build the things this framework has, in the shape that works.\n//\n// The other half of this server, and the more useful one. Introspection answers\n// \"what did my build do\"; this answers \"how do I do X here\", which is the\n// question an agent actually has — and the one it otherwise answers from Next\n// and React habits that produce code which looks right and is not.\n//\n// Long-form on purpose. AGENTS.md has to be short enough to sit in context for\n// every turn, so it can only say the rule. These are fetched when the topic\n// comes up, so they can afford the working example and the caveat under it.\n//\n// Every snippet here is the recommended spelling from the guides, not a\n// paraphrase. When a guide changes, this changes with it — a recipe that has\n// drifted is worse than no recipe, because it is followed with confidence.\n\nexport interface Recipe {\n topic: string\n summary: string\n body: string\n}\n\nconst RECIPES: Recipe[] = [\n {\n topic: 'forms',\n summary: 'Submitting to a server action, with pending state and field errors',\n body: `Use <Form>. It takes the server action itself, not a url.\n\n\\`\\`\\`tsx\n'use client'\nimport Form from '@rsc-kit/core/Form'\nimport { createPost } from '../actions'\n\nexport function NewPost() {\n return (\n <Form action={createPost} schema={schema}>\n {({ pending, errors }) => (\n <>\n <input name=\"title\" />\n {errors.title?.[0] && <p>{errors.title[0]}</p>}\n <button disabled={pending}>Save</button>\n </>\n )}\n </Form>\n )\n}\n\\`\\`\\`\n\nPassing \\`schema\\` validates in the browser BEFORE the action is called, so a\nmistake costs no round trip. It is a courtesy, never a control: the action is a\npublic endpoint reachable without your form, so the server must check too.\n\nA schema on the server (\\`client.input(schema)\\`) does NOT give you client-side\nvalidation. Pass it to the form as well — the same schema is fine.\n\nFor imperative control use \\`useForm\\` instead:\n\n\\`\\`\\`ts\nconst form = useForm({ title: '' }, { schema })\nawait form.submit(createPost)\n\\`\\`\\`\n\nIt works with no javascript at all: the form posts, the action runs, the page\nre-renders. That is why the fields are real \\`name\\` attributes rather than\ncontrolled state.`,\n },\n {\n topic: 'prefetch',\n summary: 'Making a navigation feel instant',\n body: `\\`<Link>\\` prefetches on hover by default. Usually there is nothing to do.\n\n\\`\\`\\`tsx\nimport Link from '@rsc-kit/core/Link'\n\n<Link href=\"/orders\">Orders</Link>\n<Link href=\"/orders\" prefetch={false}>Orders</Link> // opt out\n<Link href=\"/orders\" cacheFor={30_000}>Orders</Link> // hold the payload longer\n\\`\\`\\`\n\n\\`href\\` is typed to the routes the build found, so a link to a page that no\nlonger exists stops compiling. Cast with \\`as Href\\` only when the destination\nis genuinely computed.\n\nTo prefetch from code — a row about to be clicked, a wizard's next step:\n\n\\`\\`\\`ts\nimport { prefetch } from '@rsc-kit/core/navigate'\n\nprefetch('/orders/42')\n\\`\\`\\`\n\nWhat is prefetched is the RSC payload, not the html, so it is small and it warms\nthe same cache the navigation will read.`,\n },\n {\n topic: 'validation',\n summary: 'Checking input — forms, actions, urls and request bodies',\n body: `One contract everywhere: any Standard Schema (Zod, Valibot, ArkType).\n\n**Actions** validate on arrival and RETURN their failures, because React strips\na thrown message in production:\n\n\\`\\`\\`ts\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\n\\`\\`\\`\n\n**Urls** validate by exporting a schema beside the page or route:\n\n\\`\\`\\`ts\nexport const params = z.object({ slug: z.string().min(1) })\nexport const searchParams = z.object({ page: z.coerce.number().int().min(1).default(1) })\n\\`\\`\\`\n\nValues arrive parsed and typed — \\`?page=3\\` is the number 3, a missing one is\nthe default. Never hand-parse \\`Number(searchParams.get('page'))\\`.\n\n**Api route bodies** the same way:\n\n\\`\\`\\`ts\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function POST(request: Request, { body }) {\n const { title } = await body\n}\n\\`\\`\\`\n\nThe failures answer differently on purpose:\n\n bad params 404 — the url does not describe a page\n bad searchParams the error boundary (400 for an api route)\n bad body 422, the status an action already uses\n\nA bad query is deliberately NOT a 404, or one bad link makes a real page look\ndeleted.`,\n },\n {\n topic: 'action-client',\n summary: 'Middleware for server actions, so a check cannot be forgotten',\n body: `\\`\\`\\`ts title=\"src/server/client.ts\"\n'use server'\nimport { createActionClient } from '@rsc-kit/core/action'\n\nexport const client = createActionClient({ onError: report })\n .use(async ({ next }) => {\n const user = await currentUser()\n\n if (!user) throw new ServerAuthenticationError()\n\n return next({ ctx: { user } })\n })\n\\`\\`\\`\n\n\\`\\`\\`ts title=\"src/server/posts.ts\"\n'use server'\nimport { client } from './client'\n\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\nexport const getPosts = client.query(async ({ ctx }) => …)\n\\`\\`\\`\n\n\\`.handler()\\` is a mutation (POST). \\`.query()\\` is a read (GET). Both run the\nchain, so \\`ctx.user\\` is typed and non-null inside them.\n\nThe point is not convenience. An action cannot be added without the check,\nbecause there is no other constructor to reach for.\n\nStack clients for a narrower rule:\n\n\\`\\`\\`ts\nexport const admin = client.use(async ({ ctx, next }) => {\n if (!ctx.user.isAdmin) throw new ServerAuthorizationError()\n return next({ ctx })\n})\n\\`\\`\\`\n\nRoute \\`middleware.ts\\` does NOT run for actions — an action renders no route.\nThat is why the check goes here.`,\n },\n {\n topic: 'data',\n summary: 'Loading data, streaming it, and when the browser needs to refetch',\n body: `**In a server component, just await it.** No loader, no getServerSideProps.\n\n\\`\\`\\`tsx\nexport default async function Page() {\n const posts = await db.posts.all()\n}\n\\`\\`\\`\n\n**Better: do not await.** Pass the promise down and let a client component\nresolve it — the shell paints at once and the rows stream into the same\nresponse, with no request from the browser:\n\n\\`\\`\\`tsx\nexport default function Page() {\n const posts = getPosts() // not awaited\n\n return (\n <Suspense fallback={<Skeleton />}>\n <List posts={posts} /> {/* 'use client': use(posts) */}\n </Suspense>\n )\n}\n\\`\\`\\`\n\nReach for this first. It is the thing RSC is for.\n\n**When the BROWSER decides to refetch** — a filter, a poll, a refresh — that is\na cache library's job and this package does not ship one:\n\n\\`\\`\\`tsx\nuseQuery({ queryKey: ['posts', kind], queryFn: () => fetchQuery(getPosts, [kind]) })\nuseSWR(['posts', kind], () => fetchQuery(getPosts, [kind]))\n\\`\\`\\`\n\n\\`fetchQuery\\` sends the read as a GET and goes to the server every time, which\nis what a fetcher needs — staleness and revalidation belong to the library\nholding the answer. Do not add a cache on top of it.\n\nKeep the arrow: TanStack calls a bare \\`queryFn\\` with its own context, and a\nserver function serialises whatever it is handed.`,\n },\n {\n topic: 'suspense',\n summary: 'Where boundaries go, and why the build cares',\n body: `A boundary is what lets a page be stored with a hole in it rather than not\nstored at all.\n\n\\`\\`\\`tsx\n<Suspense fallback={<Skeleton />}>\n <Slow />\n</Suspense>\n\\`\\`\\`\n\nOr a \\`loading.tsx\\` beside the page, which is the same thing for the whole\nroute.\n\nThe build renders every page. Whatever has not resolved when the budget expires\nbecomes the hole; everything above it is stored and served instantly. So a page\nwith no boundary above its slow part cannot be stored at all — the build says\nso:\n\n ƒ /orders\n blocks before anything can paint. Add a loading.tsx beside it, or put a\n <Suspense> above the waiting, and it has a skeleton to store.\n\nA boundary does NOT fix a frozen \\`Date.now()\\`. Prerendering renders straight\nthrough a component that never awaits, so the value is captured exactly as\nbefore. A boundary becomes a hole only when something inside it waits.`,\n },\n {\n topic: 'offline',\n summary: 'Service worker, and what it does and does not cache',\n body: `\\`\\`\\`ts title=\"vite.config.ts\"\nrscKit({ offline: true })\n\\`\\`\\`\n\nThe build writes a service worker that precaches the client bundle and caches\npages at runtime — a document fetch warms its payload, a payload fetch warms its\ndocument, so a page reached by a link still works when reloaded offline.\n\nPages the build stored whole are served from the cache FIRST, because they\ncannot change until a deploy and a deploy sweeps the cache. Everything else is\nnetwork-first with the cache as fallback.\n\nNothing marked \\`no-store\\` is ever kept — which is how a guarded page and a\nsession-reading query stay out of a cache that has no notion of who asked.\n\nIn a component:\n\n\\`\\`\\`tsx\nimport { useOnline } from '@rsc-kit/core/useOnline'\n\nconst online = useOnline()\n\\`\\`\\`\n\nThere is no push and no background sync. Push needs a subscription endpoint and\na sender; background sync needs idempotent replay. Both are the app's decisions.`,\n },\n {\n topic: 'pwa',\n summary: 'Making the app installable',\n body: `A manifest file beside the routes:\n\n\\`\\`\\`ts title=\"src/app/manifest.ts\"\nimport type { WebManifest } from '@rsc-kit/core/manifest-file'\n\nexport default {\n name: 'Orders',\n shortName: 'Orders',\n themeColor: '#0b0b0c',\n backgroundColor: '#ffffff',\n} satisfies WebManifest\n\\`\\`\\`\n\nRead at build time, so it must be an object literal — not computed, not\nimported from elsewhere.\n\n**Icons need no listing.** Put them in \\`src/app/\\` and the build finds them:\n\n favicon.ico served at /favicon.ico\n icon-192.png <link rel=\"icon\">, and the manifest's icons\n icon-512.png\n apple-icon.png <link rel=\"apple-touch-icon\">\n opengraph-image.png <meta property=\"og:image\">\n twitter-image.png <meta name=\"twitter:image\">\n\nSizes are read from the filename. The build says whether it worked:\n\n [rsc-kit] manifest: Orders is installable\n [rsc-kit] manifest: no icons, so no browser will offer to install this.\n\nThere is no layout to edit — React hoists the tags into <head>.\n\nPair it with \\`offline: true\\`. They are separate options because they are\nseparate decisions.\n\n**Push and background sync** need listeners the generated worker does not have,\nso it imports yours from \\`src/app/sw.js\\` — plain javascript, evaluated by the\nbrowser with no build step in front of it:\n\n\\`\\`\\`js\nself.addEventListener('push', (event) => {\n const payload = event.data ? event.data.json() : {}\n\n event.waitUntil(self.registration.showNotification(payload.title, { body: payload.body }))\n})\n\\`\\`\\`\n\nThe rest is the web api and \\`web-push\\`, not this package: VAPID keys, a\nsubscribe call behind a button, the subscription stored by a server action\nagainst a USER rather than a session, and a sender that deletes an endpoint on\n404 or 410 rather than retrying a dead one forever.\n\nFor background sync, make the endpoint idempotent. The browser decides when a\nsync runs and may run it more than once — a request that reached the server\nwhose response did not arrive is retried, and if that posts a message twice the\nperson sent it twice.`,\n },\n {\n topic: 'no-javascript',\n summary: 'Shipping a route with no client runtime at all',\n body: `\\`\\`\\`ts title=\"src/app/about/page.tsx\"\nexport const clientJs = false\n\\`\\`\\`\n\nThe route ships no bootstrap and no client runtime. The build REFUSES it if the\ntree renders a client component, and names the component — they usually come\nfrom a shared layout rather than the page itself.\n\nLinks still work; they are ordinary anchors, so navigation is a full page load.\n\nMost pages do not need this. A page with nothing interactive already ships only\nthe shared runtime, and the size column in the build output tells you what each\none actually costs.`,\n },\n {\n topic: 'api-routes',\n summary: 'HTTP endpoints beside the pages',\n body: `\\`src/app/**/route.ts\\`, one export per method:\n\n\\`\\`\\`ts title=\"src/app/api/posts/[id]/route.ts\"\nexport const params = z.object({ id: z.coerce.number().int() })\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function GET(request: Request, { params }) {\n const { id } = await params\n\n return Response.json(await findPost(id))\n}\n\nexport async function POST(request: Request, { params, body }) {\n const { title } = await body\n\n return Response.json(await createPost(title), { status: 201 })\n}\n\\`\\`\\`\n\nA real \\`Request\\` in, a real \\`Response\\` out. \\`params\\`, \\`searchParams\\` and\n\\`body\\` are awaited, the same way a page's props are.\n\nThey run their directory's \\`middleware.ts\\`, so an endpoint under a guarded\npath is guarded.\n\nA \\`GET\\` that reads nothing from the request is answered from disk. Awaiting\n\\`searchParams\\` says the answer depends on the query; never touching it means\nthe stored answer is served for any query at all.\n\nExporting a \\`body\\` schema consumes the stream, so \\`request.json()\\` inside the\nhandler will find it already read. Use the parsed value.`,\n },\n {\n topic: 'authorization',\n summary: 'Guarding pages, actions, api routes and queries',\n body: `Each entry point defends itself. There is no single place that covers all of\nthem, and believing otherwise is how a hole is left.\n\n**A page or an api route**: \\`middleware.ts\\` in its directory guards everything\nat or below it.\n\n\\`\\`\\`ts title=\"src/app/admin/middleware.ts\"\nimport { redirect } from '@rsc-kit/core/redirect'\n\nexport default async function guard() {\n if (!(await currentUser())) redirect('/login')\n}\n\\`\\`\\`\n\n**An action or a query**: middleware does NOT run — they render no route. Build\nthem from an action client so the check cannot be forgotten. See the\n\\`action-client\\` topic.\n\n**Authorise on identity, not arguments.** \\`deletePost(id)\\` that trusts the id\nis the whole of an IDOR: the caller chooses the id, so check the row belongs to\n\\`ctx.user\\`.\n\nA guarded page can still be frozen at build time — the guard is a serving\ndecision, not a build one. Its response is marked private so no cache keeps it.`,\n },\n {\n topic: 'dynamic',\n summary: 'Why a page is not static, and how to choose',\n body: `A page is stored at build time unless it reads the request. Reading it is what\nopts out, and the accessors are async:\n\n\\`\\`\\`ts\nimport { cookies, headers, searchParams, connection } from '@rsc-kit/core/request'\n\nconst theme = (await cookies()).get('theme')\nawait connection() // \"render this per visitor\", said deliberately\n\\`\\`\\`\n\nA page's \\`params\\` and \\`searchParams\\` props are promises for the same reason.\n\nThe build says which call did it, per route:\n\n ◐ /locale 85 kB\n dynamic — called cookies(), headers()\n\nThat is usually correct — a page whose content depends on who is asking cannot\nbe one stored file. Change it only when the read was accidental.\n\nFor a parameterised route, \\`generateStaticParams\\` turns one shell into a page\nper url:\n\n\\`\\`\\`ts\nexport async function generateStaticParams() {\n return (await db.posts.all()).map((p) => ({ slug: p.slug }))\n}\n\\`\\`\\`\n\nA value that must differ per visitor but needs no server — a clock,\nlocalStorage, a map — belongs in the browser only:\n\n\\`\\`\\`tsx\n'use client'\nimport { browser } from 'react-dom'\n\nfunction Clock() {\n use(browser('the time is the visitor\\\\'s, not the build machine\\\\'s'))\n}\n\\`\\`\\`\n\nIt needs a Suspense boundary, and the page stays frozen.`,\n },\n]\n\n/** Every topic, with one line each — what a caller reads before choosing. */\nexport function listTopics(): string {\n return [\n 'Topics. Ask for one with how_to({ topic }).',\n '',\n ...RECIPES.map((r) => `${r.topic.padEnd(16)} ${r.summary}`),\n ].join('\\n')\n}\n\n/** One recipe, or the list plus a nudge when the topic is not one. */\nexport function howTo(topic: string): string {\n const wanted = topic.trim().toLowerCase().replace(/[\\s_]+/g, '-')\n const found =\n RECIPES.find((r) => r.topic === wanted) ??\n // A near miss is common and worth answering rather than refusing: someone\n // asks for \"form\" or \"queries\" and means the obvious thing.\n RECIPES.find((r) => r.topic.startsWith(wanted) || wanted.startsWith(r.topic)) ??\n RECIPES.find((r) => r.summary.toLowerCase().includes(wanted))\n\n if (!found) return `No topic \"${topic}\".\\n\\n${listTopics()}`\n\n return `# ${found.topic} — ${found.summary}\\n\\n${found.body}`\n}\n\n/** For tests, so a recipe cannot be added without being reachable. */\nexport const TOPICS = RECIPES.map((r) => r.topic)\n"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rsc-kit/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rsc-kit/rsc-kit.git",
|
|
10
|
+
"directory": "packages/mcp"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://rsc-kit.dev",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rsc-kit/rsc-kit/issues"
|
|
15
|
+
},
|
|
7
16
|
"bin": {
|
|
8
17
|
"rsc-kit-mcp": "./dist/index.js"
|
|
9
18
|
},
|