@webjsdev/mcp 0.1.19 → 0.1.20
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/package.json +3 -3
- package/src/guides.js +319 -0
- package/src/mcp-source.js +11 -2
- package/src/mcp.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Model Context Protocol server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Gives AI coding agents the WebJs brief, live app introspection over routes, actions, components, and convention checks, and the framework source.",
|
|
6
6
|
"bin": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@webjsdev/server": "^0.10.
|
|
28
|
-
"@webjsdev/ui": "^0.5.
|
|
27
|
+
"@webjsdev/server": "^0.10.1",
|
|
28
|
+
"@webjsdev/ui": "^0.5.1"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
package/src/guides.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern guides: how WebJs wants a feature built, one short page per need
|
|
3
|
+
* (routing, the module layout, progressive enhancement, optimistic UI, type
|
|
4
|
+
* safety, data flow, authentication, testing, uploads). `npx webjs source <guide>` prints one,
|
|
5
|
+
* as does the MCP `source` tool given the guide's name as `export`.
|
|
6
|
+
*
|
|
7
|
+
* These are not a docs corpus (the removed `docs` tool served the whole site).
|
|
8
|
+
* Each guide is capped (under 40 lines, held by test/guides.test.mjs) and
|
|
9
|
+
* ends in actions, never prose: the exports to look up next and the docs page
|
|
10
|
+
* for the long version. Every export a guide names is checked to exist. A
|
|
11
|
+
* guide is printed only when asked for by name; nothing lists them up front.
|
|
12
|
+
*
|
|
13
|
+
* Guide names are slugs no package exports (`optimistic-ui`, not
|
|
14
|
+
* `optimistic`), so a lookup of an export never lands on a guide.
|
|
15
|
+
*
|
|
16
|
+
* @module guides
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, { need: string, text: string }>} */
|
|
20
|
+
export const GUIDES = {
|
|
21
|
+
routing: {
|
|
22
|
+
need: 'the app/ file conventions: pages, layouts, route.ts, middleware.ts, boundaries',
|
|
23
|
+
text: `Guide: routing (the app/ file conventions, as in the Next.js App Router)
|
|
24
|
+
|
|
25
|
+
app/page.ts / default export (ctx) => html, server only
|
|
26
|
+
app/layout.ts wraps every page below it; only the root one writes <html>
|
|
27
|
+
app/posts/[id]/page.ts /posts/1 params.id, or const { id } = await params
|
|
28
|
+
app/docs/[...slug]/page.ts /docs/a/b params.slug is a string[] ([[...slug]] matches /docs too)
|
|
29
|
+
app/(admin)/users/page.ts /users a (group) adds no URL segment; _folders are not routed
|
|
30
|
+
app/api/posts/[id]/route.ts export async function GET(req: Request, { params }) {
|
|
31
|
+
const { id } = await params; return Response.json(...) }
|
|
32
|
+
GET, POST, PUT, PATCH, DELETE, each returning a Response
|
|
33
|
+
app/middleware.ts export default async function middleware(request: Request),
|
|
34
|
+
run before every page, form post and route.ts under it;
|
|
35
|
+
return a Response to answer, nothing to continue
|
|
36
|
+
app/admin/middleware.ts the same for /admin and everything under it
|
|
37
|
+
middleware.ts beside app/ the app-wide one, which also sees action RPC calls
|
|
38
|
+
|
|
39
|
+
Gate a section in a middleware.ts, not in a layout: a page renders before its
|
|
40
|
+
layouts, and a client navigation skips the layouts the browser already has.
|
|
41
|
+
|
|
42
|
+
Boundaries, the nearest one up the folders renders inside the layouts above it:
|
|
43
|
+
loading.ts shown while the page loads
|
|
44
|
+
error.ts a render that throws, given { error }
|
|
45
|
+
not-found.ts notFound(), or (at the root) no route matched
|
|
46
|
+
forbidden.ts forbidden(), a 403 unauthorized.ts unauthorized(), a 401
|
|
47
|
+
Throw them, or redirect('/login'), from a page, a layout or a form action.
|
|
48
|
+
|
|
49
|
+
Metadata: export const metadata = { title, description } from a page or a
|
|
50
|
+
layout, or export async function generateMetadata({ params }) for a dynamic one.
|
|
51
|
+
|
|
52
|
+
Look up: npx webjs source redirect, npx webjs source notFound, npx webjs source generateMetadata
|
|
53
|
+
Docs: https://webjs.dev/docs/routing`,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
modules: {
|
|
57
|
+
need: 'how an app is laid out',
|
|
58
|
+
text: `Guide: modules (how a WebJs app is laid out)
|
|
59
|
+
|
|
60
|
+
One folder per feature under modules/<feature>/, and thin routes:
|
|
61
|
+
modules/posts/actions/create-post.server.ts 'use server', one action per file
|
|
62
|
+
modules/posts/queries/list-posts.server.ts server-only reads, called by pages
|
|
63
|
+
modules/posts/components/post-list.ts Lit components (these ship)
|
|
64
|
+
modules/posts/types.ts types derived from the schema
|
|
65
|
+
app/posts/page.ts awaits a query, renders components
|
|
66
|
+
db/schema.server.ts every table, one defineRelations
|
|
67
|
+
|
|
68
|
+
Each table goes in db/schema.server.ts with its row type beside it
|
|
69
|
+
(export type Post = typeof posts.$inferSelect), and its relations in the one
|
|
70
|
+
defineRelations call. modules/users/ in a fresh app is a working example of
|
|
71
|
+
the whole shape: read it, then replace it with the app's own features.
|
|
72
|
+
|
|
73
|
+
Next: npx webjs source progressive-enhancement (or optimistic-ui, type-safety, data-flow)
|
|
74
|
+
Docs: https://webjs.dev/docs/conventions`,
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'progressive-enhancement': {
|
|
78
|
+
need: 'forms that work with and without JavaScript',
|
|
79
|
+
text: `Guide: progressive-enhancement (forms that work before and after JavaScript)
|
|
80
|
+
|
|
81
|
+
Bind every write to a 'use server' action, never fetch() plus a click handler:
|
|
82
|
+
<form action=\${createPost} class="group">
|
|
83
|
+
<input name="title" value=\${actionData?.values?.title ?? ''}>
|
|
84
|
+
\${actionData?.fieldErrors?.title ? html\`<p role="alert">\${actionData.fieldErrors.title}</p>\` : nothing}
|
|
85
|
+
<button class="group-aria-busy:opacity-50" type="submit">Save</button>
|
|
86
|
+
</form>
|
|
87
|
+
|
|
88
|
+
- JavaScript off: the browser posts; a success that calls redirect() is a 303,
|
|
89
|
+
and a failure re-renders the page at 422 with the result on actionData.
|
|
90
|
+
- JavaScript on: the client router posts the same body and applies the result
|
|
91
|
+
in place (no reload, scroll and focus kept, the form reset on success), with
|
|
92
|
+
aria-busy on the form while it is in flight.
|
|
93
|
+
- Invalid input: return { success: false, fieldErrors, values, status: 422 }.
|
|
94
|
+
- Several actions on one form: <button formaction=\${deletePost}>.
|
|
95
|
+
|
|
96
|
+
Look up: npx webjs source ActionResult, npx webjs source PageProps
|
|
97
|
+
Docs: https://webjs.dev/docs/progressive-enhancement`,
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
'optimistic-ui': {
|
|
101
|
+
need: 'show a write before the server confirms it',
|
|
102
|
+
text: `Guide: optimistic-ui (show a write before the server confirms it)
|
|
103
|
+
|
|
104
|
+
Inside a Lit component that owns the list, seeded by its page as a property:
|
|
105
|
+
pending = optimistic(this, { source: () => this.posts, update: (rows, p) => ... });
|
|
106
|
+
onDelete(e, id) { e.preventDefault(); this.pending.add({ id }, deletePost(new FormData(e.target))); }
|
|
107
|
+
|
|
108
|
+
- Keep the reducer pure (it runs on every render); mint a temp id in the handler.
|
|
109
|
+
- The entry drops when the promise settles; on success write the server's row
|
|
110
|
+
into the seeded property.
|
|
111
|
+
- Keep the form bound to the action (action=\${deletePost}) so it posts with
|
|
112
|
+
JavaScript off; @submit only intercepts it.
|
|
113
|
+
- One value (a like, a toggle): optimistic(signal, value, () => action()).
|
|
114
|
+
- Skip it when the result cannot be predicted from the input.
|
|
115
|
+
|
|
116
|
+
Look up: npx webjs source optimistic (a full component example)
|
|
117
|
+
Docs: https://webjs.dev/docs/optimistic-ui`,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
'type-safety': {
|
|
121
|
+
need: 'one set of types from the database to the page',
|
|
122
|
+
text: `Guide: type-safety (one set of types from the database to the page)
|
|
123
|
+
|
|
124
|
+
TypeScript must be erasable: no enum, decorator, value namespace or parameter
|
|
125
|
+
property. Types are stripped to whitespace and nothing is compiled.
|
|
126
|
+
- Rows: export type Post = typeof posts.$inferSelect beside the table;
|
|
127
|
+
inserts use typeof posts.$inferInsert. Never hand-write a row type.
|
|
128
|
+
- Actions return ActionResult<Post>, and the one function is typed for a form
|
|
129
|
+
binding, an RPC call from a component and a route handler alike.
|
|
130
|
+
- Pages: PageProps<'/posts/[id]'> types params from the route; actionData is a
|
|
131
|
+
failed action's result (pass its type as the second parameter).
|
|
132
|
+
- Components take types with import type, which is erased, so a server
|
|
133
|
+
module never reaches the browser.
|
|
134
|
+
- npx webjs types writes typed hrefs; npx webjs typecheck runs tsc.
|
|
135
|
+
|
|
136
|
+
Look up: npx webjs source ActionResult, npx webjs source PageProps
|
|
137
|
+
Docs: https://webjs.dev/docs/typescript`,
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
'data-flow': {
|
|
141
|
+
need: 'queries, actions, RPC and route handlers',
|
|
142
|
+
text: `Guide: data-flow (where server code goes and how the browser reaches it)
|
|
143
|
+
|
|
144
|
+
- Reads: queries/*.server.ts with no 'use server', called from a page or a
|
|
145
|
+
layout, which run only on the server (as Next.js server components do).
|
|
146
|
+
- Drizzle 1.0's db.query takes objects, not Drizzle 0.x's eq() / asc():
|
|
147
|
+
db.query.posts.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, with: { author: true } })
|
|
148
|
+
eq(), and() and lt() go in db.select().from(posts).where(...).
|
|
149
|
+
- SQLite's drivers are synchronous: db.transaction((tx) => { ... }) takes a
|
|
150
|
+
sync callback whose queries end in .all(), .get() or .run() (never async).
|
|
151
|
+
- Writes: actions/*.server.ts with 'use server'. A form binds one; a component
|
|
152
|
+
imports and calls it, which goes over RPC (a Date or a Map survives).
|
|
153
|
+
- Public HTTP API: app/api/<name>/route.ts exporting GET, POST and the rest,
|
|
154
|
+
calling the same actions (or wrap one with route()):
|
|
155
|
+
// app/api/polls/[slug]/route.ts
|
|
156
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
|
|
157
|
+
const poll = await findPoll((await params).slug);
|
|
158
|
+
return poll ? Response.json(poll) : Response.json({ error: 'Not found' }, { status: 404 });
|
|
159
|
+
}
|
|
160
|
+
- Components never import the database or a server-only module: data arrives
|
|
161
|
+
as a property from the page, or through an action call.
|
|
162
|
+
- After a write: revalidatePath('/posts') refreshes cached pages, redirect()
|
|
163
|
+
navigates, and a returned failure re-renders with its message.
|
|
164
|
+
- Cache a read with cache(fn, { key, ttl, tags }) and revalidateTag(tag).
|
|
165
|
+
|
|
166
|
+
Look up: npx webjs source route, npx webjs source cache, npx webjs source revalidatePath
|
|
167
|
+
Docs: https://webjs.dev/docs/server-actions`,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
authentication: {
|
|
171
|
+
need: 'accounts, sign-in and protected pages',
|
|
172
|
+
text: `Guide: authentication (accounts, sign-in, protected pages)
|
|
173
|
+
|
|
174
|
+
createAuth is Auth.js's API, written once in a server-only module:
|
|
175
|
+
// modules/auth/auth.server.ts
|
|
176
|
+
export const { auth, signIn, signOut, handlers } = createAuth({
|
|
177
|
+
providers: [Credentials({ async authorize(c: { email: string; password: string }) {
|
|
178
|
+
const user = await findUserByEmail(c.email); // your own query
|
|
179
|
+
if (!user || !(await verifyPassword(c.password, user.passwordHash))) return null;
|
|
180
|
+
return { id: String(user.id), email: user.email };
|
|
181
|
+
} })],
|
|
182
|
+
});
|
|
183
|
+
// app/api/auth/[...path]/route.ts
|
|
184
|
+
export const { GET, POST } = handlers;
|
|
185
|
+
- Keep a password hash on users, never the password: scrypt from node:crypto
|
|
186
|
+
with a random salt, compared with timingSafeEqual.
|
|
187
|
+
- Log in: a 'use server' action awaits signIn('credentials', formData,
|
|
188
|
+
{ redirectTo: '/' }). A wrong password throws CredentialsSignin: catch it and
|
|
189
|
+
return { success: false, error }. Log out: await signOut({ redirectTo: '/login' }).
|
|
190
|
+
- Gate pages in app/middleware.ts, not in a layout:
|
|
191
|
+
const PUBLIC = ['/login', '/signup', '/api/auth'];
|
|
192
|
+
export default async function middleware(request: Request) {
|
|
193
|
+
const { pathname } = new URL(request.url);
|
|
194
|
+
if (PUBLIC.some((p) => pathname === p || pathname.startsWith(p + '/'))) return;
|
|
195
|
+
if (!(await auth())?.user) return Response.redirect(new URL('/login', request.url), 303);
|
|
196
|
+
}
|
|
197
|
+
- An action a component calls directly skips app/middleware.ts: check
|
|
198
|
+
(await auth())?.user inside it.
|
|
199
|
+
- Add GitHub or Google: providers: [GitHub({ clientId, clientSecret })].
|
|
200
|
+
AUTH_SECRET signs the session; development and tests generate one.
|
|
201
|
+
|
|
202
|
+
Look up: npx webjs source createAuth, npx webjs source Credentials
|
|
203
|
+
Docs: https://webjs.dev/docs/auth`,
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
testing: {
|
|
207
|
+
need: 'server tests against a real database',
|
|
208
|
+
text: `Guide: testing (server tests against a real database)
|
|
209
|
+
|
|
210
|
+
npx webjs test runs test/**/*.test.ts (node --test, or bun test on Bun) on a
|
|
211
|
+
fresh migrated SQLite database, so a test never touches db/dev.db.
|
|
212
|
+
- Pages and forms, through the real request handler:
|
|
213
|
+
const app = await createRequestHandler({ appDir, dev: true });
|
|
214
|
+
const res = await submitForm(app.handle, '/posts/new', { title: 'Hi' });
|
|
215
|
+
assert.equal(res.status, 303);
|
|
216
|
+
- testRequest(app.handle, '/posts') for a GET or a JSON route. Carry a cookie
|
|
217
|
+
the app set into the next request:
|
|
218
|
+
const cookie = cookiesToHeader(getSetCookies(res));
|
|
219
|
+
await submitForm(app.handle, '/p/abc', { option: '1' }, { cookies: cookie });
|
|
220
|
+
loginAndGetCookies signs in with credentials and returns the same header.
|
|
221
|
+
- Call a query or an action directly for its logic; an action that redirects
|
|
222
|
+
throws, so test that one through submitForm.
|
|
223
|
+
- Components: npx webjs test --browser, with fixture() from @webjsdev/core/testing.
|
|
224
|
+
|
|
225
|
+
Look up: npx webjs source submitForm, npx webjs source testRequest
|
|
226
|
+
Docs: https://webjs.dev/docs/testing`,
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
uploads: {
|
|
230
|
+
need: 'attach a file to a record, store it and serve it back',
|
|
231
|
+
text: `Guide: uploads (attach a file to a record, store it and serve it back)
|
|
232
|
+
|
|
233
|
+
The row keeps the storage key, never the bytes, so the table gets a
|
|
234
|
+
fileKey: text() column beside the record's own fields.
|
|
235
|
+
- The form: <form action=\${attachFile}> holding <input type="file" name="file"
|
|
236
|
+
accept="image/png,image/jpeg,application/pdf">. A bound form gets
|
|
237
|
+
enctype="multipart/form-data" from the renderer (writing it is harmless).
|
|
238
|
+
- The 'use server' action checks the file before storing it:
|
|
239
|
+
const file = formData.get('file');
|
|
240
|
+
const fail = (msg: string) => ({ success: false as const, fieldErrors: { file: msg }, status: 422 });
|
|
241
|
+
if (!(file instanceof File) || file.size === 0) return fail('Choose a file.');
|
|
242
|
+
if (file.size > 5 * 1024 * 1024) return fail('At most 5 MB.');
|
|
243
|
+
if (!['image/png', 'image/jpeg', 'application/pdf'].includes(file.type)) return fail('An image or a PDF.');
|
|
244
|
+
const key = generateKey(file.name); // <uuid>.<ext>, safe as a path
|
|
245
|
+
await getFileStore().put(key, file); // streamed into .webjs/uploads
|
|
246
|
+
await db.update(documents).set({ fileKey: key }).where(eq(documents.id, id));
|
|
247
|
+
- Serving: nothing serves stored files by default, so the app adds a route:
|
|
248
|
+
// app/files/[key]/route.ts
|
|
249
|
+
export async function GET(req: Request, { params }: { params: Promise<{ key: string }> }) {
|
|
250
|
+
const { key } = await params;
|
|
251
|
+
const check = verifySignedUrl(new URL(req.url), process.env.FILE_URL_SECRET!);
|
|
252
|
+
if (!check.valid || check.key !== key) return new Response('Forbidden', { status: 403 });
|
|
253
|
+
const stored = await getFileStore().get(key);
|
|
254
|
+
if (!stored) return new Response('Not found', { status: 404 });
|
|
255
|
+
return new Response(stored.body, { headers: { 'content-type': stored.contentType,
|
|
256
|
+
'x-content-type-options': 'nosniff', 'content-disposition': 'attachment' } });
|
|
257
|
+
}
|
|
258
|
+
- The link, minted in a page: base is that route's path, since the default is
|
|
259
|
+
the store's /uploads/<key>, which nothing serves.
|
|
260
|
+
signedUrl(doc.fileKey, { secret: process.env.FILE_URL_SECRET!, base: '/files/' + doc.fileKey, expiresIn: 3600 })
|
|
261
|
+
- file.type is what the browser claims, so keep nosniff and attachment on
|
|
262
|
+
anything a user sent. Set FILE_URL_SECRET in .env.
|
|
263
|
+
|
|
264
|
+
Look up: npx webjs source signedUrl, npx webjs source getFileStore, npx webjs source generateKey
|
|
265
|
+
Docs: https://webjs.dev/docs/file-storage`,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* File convention names, as an agent looks them up, onto the guide that
|
|
271
|
+
* explains them. In the #1692 evals agents ran `webjs source not-found`,
|
|
272
|
+
* `route` and `middleware` and then read the router to learn the conventions.
|
|
273
|
+
* Matched case-sensitively, so a name that differs from an export only in case
|
|
274
|
+
* stays the export (`notfound` is the guide, `notFound` the function). None
|
|
275
|
+
* of these is an export: test/guides.test.mjs holds that. `route` and
|
|
276
|
+
* `middleware` are exports (the `route()` adapter, the middleware type), so
|
|
277
|
+
* they print the export with a pointer here (`conventionPointer`). The words
|
|
278
|
+
* for a file upload (`upload`, `attachments`) find the `uploads` guide.
|
|
279
|
+
* @type {Record<string, string>}
|
|
280
|
+
*/
|
|
281
|
+
export const GUIDE_ALIASES = Object.fromEntries([
|
|
282
|
+
'routes', 'router', 'app-router', 'file-conventions',
|
|
283
|
+
'page', 'page.ts', 'layout', 'layout.ts', 'route.ts', 'route-handler', 'route-handlers',
|
|
284
|
+
'middleware.ts', 'proxy.ts', 'not-found', 'not-found.ts', 'notfound', 'error', 'error.ts',
|
|
285
|
+
'loading', 'loading.ts', 'forbidden.ts', 'unauthorized.ts', 'global-error', 'global-error.ts',
|
|
286
|
+
'global-not-found', 'boundaries', 'dynamic-routes', 'catch-all', 'route-groups',
|
|
287
|
+
].map((alias) => [alias, 'routing']).concat(
|
|
288
|
+
['upload', 'file-upload', 'file-uploads', 'attachments', 'attachment'].map((alias) => [alias, 'uploads']),
|
|
289
|
+
));
|
|
290
|
+
|
|
291
|
+
/** The exports whose name is also a file convention (`route.ts`, `middleware.ts`). */
|
|
292
|
+
const CONVENTION_EXPORTS = new Set(['route', 'middleware']);
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The line an export lookup appends when the name is also a file convention,
|
|
296
|
+
* so `webjs source route` (the `route()` adapter) still leads to `route.ts`.
|
|
297
|
+
* @param {string} name
|
|
298
|
+
* @returns {string | null}
|
|
299
|
+
*/
|
|
300
|
+
export function conventionPointer(name) {
|
|
301
|
+
return CONVENTION_EXPORTS.has(String(name ?? '').trim())
|
|
302
|
+
? 'For the route.ts / middleware.ts file convention: npx webjs source routing'
|
|
303
|
+
: null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* One guide's text, or null when the name is not a guide. Case and spaces are
|
|
308
|
+
* forgiven for a guide's own name (`Optimistic UI` finds `optimistic-ui`); an
|
|
309
|
+
* alias (`not-found`, `middleware.ts`, `app/page.ts`) matches as written.
|
|
310
|
+
* @param {string} name
|
|
311
|
+
* @returns {string | null}
|
|
312
|
+
*/
|
|
313
|
+
export function guide(name) {
|
|
314
|
+
const raw = String(name ?? '').trim();
|
|
315
|
+
const key = raw.toLowerCase().replace(/[\s_]+/g, '-');
|
|
316
|
+
if (Object.hasOwn(GUIDES, key)) return GUIDES[key].text;
|
|
317
|
+
const alias = raw.replace(/^\.?\/?app\//, '').replace(/\.(?:m?js|mts)$/, '.ts');
|
|
318
|
+
return Object.hasOwn(GUIDE_ALIASES, alias) ? GUIDES[GUIDE_ALIASES[alias]].text : null;
|
|
319
|
+
}
|
package/src/mcp-source.js
CHANGED
|
@@ -29,6 +29,7 @@ import { createRequire } from 'node:module';
|
|
|
29
29
|
import { join, resolve, sep, relative } from 'node:path';
|
|
30
30
|
import { readdirSync, statSync } from 'node:fs';
|
|
31
31
|
import { featureCatalog } from './catalog.js';
|
|
32
|
+
import { guide, conventionPointer } from './guides.js';
|
|
32
33
|
|
|
33
34
|
/** The published framework packages whose source an agent may want to read. */
|
|
34
35
|
export const FRAMEWORK_PACKAGES = ['core', 'server', 'cli', 'intellisense', 'ui'];
|
|
@@ -465,7 +466,8 @@ async function lookupMember(deps, roots, n) {
|
|
|
465
466
|
* The `source` tool entry point. Dispatches on the args: `export` looks up one
|
|
466
467
|
* export's signature and doc, `path` reads a file, `query` greps, `package`
|
|
467
468
|
* lists that package's entry points, and no args prints the feature catalogue
|
|
468
|
-
* (what WebJs provides, by need).
|
|
469
|
+
* (what WebJs provides, by need). An `export` that names a pattern guide
|
|
470
|
+
* (`modules`, `optimistic-ui`, ...) returns the guide. PURE given `deps`
|
|
469
471
|
* (`{ roots, readFile, readdir }`).
|
|
470
472
|
*
|
|
471
473
|
* @param {object} deps
|
|
@@ -474,7 +476,14 @@ async function lookupMember(deps, roots, n) {
|
|
|
474
476
|
*/
|
|
475
477
|
export async function runSourceTool(deps, args) {
|
|
476
478
|
const a = args || {};
|
|
477
|
-
|
|
479
|
+
// A guide name (`optimistic-ui`, `modules`) is a slug no package exports.
|
|
480
|
+
if (a.export) {
|
|
481
|
+
const g = a.package ? null : guide(a.export);
|
|
482
|
+
if (g) return g;
|
|
483
|
+
const out = await lookupExport(deps, a.export, a.package);
|
|
484
|
+
const pointer = conventionPointer(a.export);
|
|
485
|
+
return pointer ? `${out}\n\n${pointer}` : out;
|
|
486
|
+
}
|
|
478
487
|
if (a.path) return readSource(deps, a.path);
|
|
479
488
|
if (a.query) return grepSources(deps, a.query);
|
|
480
489
|
if (a.package) return listSources(deps, a.package);
|
package/src/mcp.js
CHANGED
|
@@ -29,6 +29,7 @@ import { resolveFrameworkRoots, runSourceTool, listDir } from './mcp-source.js';
|
|
|
29
29
|
// `webjs source <Export>` in the CLI (#1623), which shells the same text.
|
|
30
30
|
export { lookupExport, resolveFrameworkRoots, listDir, listSources } from './mcp-source.js';
|
|
31
31
|
export { featureCatalog, FEATURE_CATALOG } from './catalog.js';
|
|
32
|
+
export { guide, GUIDES, GUIDE_ALIASES, conventionPointer } from './guides.js';
|
|
32
33
|
import { projectRoutes } from './routes-report.js';
|
|
33
34
|
|
|
34
35
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
@@ -142,7 +143,7 @@ const TOOL_DEFS = [
|
|
|
142
143
|
{
|
|
143
144
|
name: 'source',
|
|
144
145
|
description:
|
|
145
|
-
'Read the FRAMEWORK authored source (
|
|
146
|
+
'Read the FRAMEWORK authored source (WebJs is buildless: node_modules/@webjsdev/*/src is the JSDoc source, run directly server-side; only the core browser bundle is built into dist/, which this skips). Pass `export` for the signature plus doc comment of ONE export (the cheap way to check a contract: `export: "createAuth"`), `path` to read a file (e.g. server/src/ssr/head.js; the bare server/src/ssr.js is a barrel that re-exports from the sibling directory), `query` to grep the @webjsdev/* src trees, `package` to list one package\'s entry points, or NO ARGS for the feature catalogue: what WebJs provides by need (auth, sessions, caching, rate limits, uploads, realtime, SEO, test helpers), each name one `export` call away from its signature and example. A file-convention name as `export` (such as `not-found` or `middleware.ts`) returns a short guide on how WebJs wants that built. Read-only.',
|
|
146
147
|
inputSchema: SOURCE_SCHEMA,
|
|
147
148
|
},
|
|
148
149
|
{
|