@pyreon/mcp 0.5.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/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +13376 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +3392 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +1 -0
- package/package.json +55 -0
- package/src/api-reference.ts +447 -0
- package/src/index.ts +245 -0
- package/src/project-scanner.ts +215 -0
- package/src/tests/api-reference.test.ts +14 -0
- package/src/tests/project-scanner.test.ts +294 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pyreon API reference database — structured documentation for every public export.
|
|
3
|
+
* Used by the MCP server's get_api tool and the llms.txt generator.
|
|
4
|
+
*
|
|
5
|
+
* Format: "package/symbol" → { signature, example, notes?, mistakes? }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ApiEntry {
|
|
9
|
+
signature: string
|
|
10
|
+
example: string
|
|
11
|
+
notes?: string
|
|
12
|
+
mistakes?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const API_REFERENCE: Record<string, ApiEntry> = {
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// @pyreon/reactivity
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
"reactivity/signal": {
|
|
21
|
+
signature: "signal<T>(initialValue: T, options?: { name?: string }): Signal<T>",
|
|
22
|
+
example: `const count = signal(0)
|
|
23
|
+
|
|
24
|
+
// Read (subscribes to updates):
|
|
25
|
+
count() // 0
|
|
26
|
+
|
|
27
|
+
// Write:
|
|
28
|
+
count.set(5) // sets to 5
|
|
29
|
+
|
|
30
|
+
// Update:
|
|
31
|
+
count.update(n => n + 1) // 6
|
|
32
|
+
|
|
33
|
+
// Read without subscribing:
|
|
34
|
+
count.peek() // 6`,
|
|
35
|
+
notes:
|
|
36
|
+
"Signals are callable functions, NOT .value getters. Components run once — signal reads in JSX auto-subscribe.",
|
|
37
|
+
mistakes: `- \`count.value\` → Use \`count()\` to read
|
|
38
|
+
- \`{count}\` in JSX → Use \`{count()}\` to read (or let the compiler wrap it)
|
|
39
|
+
- \`const [val, setVal] = signal(0)\` → Not destructurable. Use \`const val = signal(0)\``,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
"reactivity/computed": {
|
|
43
|
+
signature:
|
|
44
|
+
"computed<T>(fn: () => T, options?: { equals?: (a: T, b: T) => boolean }): Computed<T>",
|
|
45
|
+
example: `const count = signal(0)
|
|
46
|
+
const doubled = computed(() => count() * 2)
|
|
47
|
+
|
|
48
|
+
doubled() // 0
|
|
49
|
+
count.set(5)
|
|
50
|
+
doubled() // 10`,
|
|
51
|
+
notes:
|
|
52
|
+
"Dependencies auto-tracked. No dependency array needed. Memoized — only recomputes when dependencies change.",
|
|
53
|
+
mistakes: `- \`computed(() => count)\` → Must call signal: \`computed(() => count())\`
|
|
54
|
+
- Don't use for side effects — use effect() instead`,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"reactivity/effect": {
|
|
58
|
+
signature: "effect(fn: () => (() => void) | void): () => void",
|
|
59
|
+
example: `const count = signal(0)
|
|
60
|
+
|
|
61
|
+
// Auto-tracks count() dependency:
|
|
62
|
+
const dispose = effect(() => {
|
|
63
|
+
console.log("Count is:", count())
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// With cleanup:
|
|
67
|
+
effect(() => {
|
|
68
|
+
const handler = () => console.log(count())
|
|
69
|
+
window.addEventListener("resize", handler)
|
|
70
|
+
return () => window.removeEventListener("resize", handler)
|
|
71
|
+
})`,
|
|
72
|
+
notes:
|
|
73
|
+
"Returns a dispose function. Dependencies auto-tracked on each run. For DOM-specific effects, use renderEffect().",
|
|
74
|
+
mistakes: `- Don't pass a dependency array — Pyreon auto-tracks
|
|
75
|
+
- \`effect(() => { count })\` → Must call: \`effect(() => { count() })\``,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"reactivity/batch": {
|
|
79
|
+
signature: "batch(fn: () => void): void",
|
|
80
|
+
example: `const a = signal(1)
|
|
81
|
+
const b = signal(2)
|
|
82
|
+
|
|
83
|
+
// Updates subscribers only once:
|
|
84
|
+
batch(() => {
|
|
85
|
+
a.set(10)
|
|
86
|
+
b.set(20)
|
|
87
|
+
})`,
|
|
88
|
+
notes: "Defers all signal notifications until the batch completes. Nested batches are merged.",
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
"reactivity/createStore": {
|
|
92
|
+
signature: "createStore<T extends object>(initialValue: T): T",
|
|
93
|
+
example: `const store = createStore({
|
|
94
|
+
user: { name: "Alice", age: 30 },
|
|
95
|
+
items: [1, 2, 3]
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Granular reactivity — only rerenders what changed:
|
|
99
|
+
store.user.name = "Bob" // only name subscribers fire
|
|
100
|
+
store.items.push(4) // only items subscribers fire`,
|
|
101
|
+
notes:
|
|
102
|
+
"Deep proxy — nested objects are automatically reactive. Use reconcile() for bulk updates.",
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
"reactivity/createResource": {
|
|
106
|
+
signature:
|
|
107
|
+
"createResource<T>(fetcher: () => Promise<T>, options?: ResourceOptions): Resource<T>",
|
|
108
|
+
example: `const users = createResource(() => fetch("/api/users").then(r => r.json()))
|
|
109
|
+
|
|
110
|
+
// In JSX:
|
|
111
|
+
<Show when={!users.loading()}>
|
|
112
|
+
<For each={users()} by={u => u.id}>
|
|
113
|
+
{user => <li>{user.name}</li>}
|
|
114
|
+
</For>
|
|
115
|
+
</Show>`,
|
|
116
|
+
notes:
|
|
117
|
+
"Integrates with Suspense. Access .loading(), .error(), and call resource() for the value.",
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
121
|
+
// @pyreon/core
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
"core/h": {
|
|
125
|
+
signature:
|
|
126
|
+
"h<P>(type: ComponentFn<P> | string | symbol, props: P | null, ...children: VNodeChild[]): VNode",
|
|
127
|
+
example: `// Usually use JSX instead:
|
|
128
|
+
const vnode = h("div", { class: "container" },
|
|
129
|
+
h("h1", null, "Hello"),
|
|
130
|
+
h(Counter, { initial: 0 })
|
|
131
|
+
)`,
|
|
132
|
+
notes: "Low-level API. Prefer JSX which compiles to h() calls (or _tpl() for templates).",
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
"core/Fragment": {
|
|
136
|
+
signature: "Fragment: symbol",
|
|
137
|
+
example: `// JSX:
|
|
138
|
+
<>
|
|
139
|
+
<h1>Title</h1>
|
|
140
|
+
<p>Content</p>
|
|
141
|
+
</>
|
|
142
|
+
|
|
143
|
+
// h() API:
|
|
144
|
+
h(Fragment, null, h("h1", null, "Title"), h("p", null, "Content"))`,
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
"core/onMount": {
|
|
148
|
+
signature: "onMount(fn: () => CleanupFn | undefined): void",
|
|
149
|
+
example: `const Timer = () => {
|
|
150
|
+
const count = signal(0)
|
|
151
|
+
|
|
152
|
+
onMount(() => {
|
|
153
|
+
const id = setInterval(() => count.update(n => n + 1), 1000)
|
|
154
|
+
return () => clearInterval(id) // cleanup
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return <div>{count()}</div>
|
|
158
|
+
}`,
|
|
159
|
+
notes: "Must return undefined or a cleanup function. Do NOT return void.",
|
|
160
|
+
mistakes: `- \`onMount(() => { doStuff() })\` → Must return undefined: \`onMount(() => { doStuff(); return undefined })\`
|
|
161
|
+
- Or return cleanup: \`onMount(() => { const id = setInterval(...); return () => clearInterval(id) })\``,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
"core/onUnmount": {
|
|
165
|
+
signature: "onUnmount(fn: () => void): void",
|
|
166
|
+
example: `onUnmount(() => {
|
|
167
|
+
console.log("Component removed from DOM")
|
|
168
|
+
})`,
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
"core/createContext": {
|
|
172
|
+
signature: "createContext<T>(defaultValue: T): Context<T>",
|
|
173
|
+
example: `const ThemeContext = createContext<"light" | "dark">("light")
|
|
174
|
+
|
|
175
|
+
// Provide:
|
|
176
|
+
const App = () => {
|
|
177
|
+
pushContext(new Map([[ThemeContext.id, "dark"]]))
|
|
178
|
+
onUnmount(() => popContext())
|
|
179
|
+
return <Child />
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Consume:
|
|
183
|
+
const Child = () => {
|
|
184
|
+
const theme = useContext(ThemeContext)
|
|
185
|
+
return <div class={theme}>...</div>
|
|
186
|
+
}`,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
"core/useContext": {
|
|
190
|
+
signature: "useContext<T>(ctx: Context<T>): T",
|
|
191
|
+
example: `const theme = useContext(ThemeContext) // returns provided value or default`,
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
"core/For": {
|
|
195
|
+
signature: "<For each={items} by={keyFn}>{renderFn}</For>",
|
|
196
|
+
example: `const items = signal([
|
|
197
|
+
{ id: 1, name: "Apple" },
|
|
198
|
+
{ id: 2, name: "Banana" },
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
<For each={items()} by={item => item.id}>
|
|
202
|
+
{item => <li>{item.name}</li>}
|
|
203
|
+
</For>`,
|
|
204
|
+
notes: "Uses 'by' prop (not 'key') because JSX extracts 'key' as a special VNode prop.",
|
|
205
|
+
mistakes: `- \`<For each={items}>\` → Must call signal: \`<For each={items()}>\`
|
|
206
|
+
- \`<For each={items()} key={...}>\` → Use \`by\` not \`key\`
|
|
207
|
+
- \`{items().map(...)}\` → Use <For> for reactive list rendering`,
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
"core/Show": {
|
|
211
|
+
signature: "<Show when={condition} fallback={alternative}>{children}</Show>",
|
|
212
|
+
example: `<Show when={isLoggedIn()} fallback={<LoginForm />}>
|
|
213
|
+
<Dashboard />
|
|
214
|
+
</Show>`,
|
|
215
|
+
notes:
|
|
216
|
+
"More efficient than ternary for signal-driven conditions. Only mounts/unmounts when condition changes.",
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
"core/Suspense": {
|
|
220
|
+
signature: "<Suspense fallback={loadingUI}>{children}</Suspense>",
|
|
221
|
+
example: `const LazyPage = lazy(() => import("./HeavyPage"))
|
|
222
|
+
|
|
223
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
224
|
+
<LazyPage />
|
|
225
|
+
</Suspense>`,
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
"core/lazy": {
|
|
229
|
+
signature:
|
|
230
|
+
"lazy(loader: () => Promise<{ default: ComponentFn }>, options?: LazyOptions): LazyComponent",
|
|
231
|
+
example: `const Settings = lazy(() => import("./pages/Settings"))
|
|
232
|
+
|
|
233
|
+
// Use in JSX (wrap with Suspense):
|
|
234
|
+
<Suspense fallback={<Spinner />}>
|
|
235
|
+
<Settings />
|
|
236
|
+
</Suspense>`,
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
"core/Dynamic": {
|
|
240
|
+
signature: "<Dynamic component={comp} {...props} />",
|
|
241
|
+
example: `const components = { home: HomePage, about: AboutPage }
|
|
242
|
+
const current = signal("home")
|
|
243
|
+
|
|
244
|
+
<Dynamic component={components[current()]} />`,
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
"core/ErrorBoundary": {
|
|
248
|
+
signature: "<ErrorBoundary onCatch={handler} fallback={errorUI}>{children}</ErrorBoundary>",
|
|
249
|
+
example: `<ErrorBoundary
|
|
250
|
+
onCatch={(err) => console.error(err)}
|
|
251
|
+
fallback={(err) => <div>Error: {err.message}</div>}
|
|
252
|
+
>
|
|
253
|
+
<App />
|
|
254
|
+
</ErrorBoundary>`,
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
258
|
+
// @pyreon/router
|
|
259
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
260
|
+
|
|
261
|
+
"router/createRouter": {
|
|
262
|
+
signature: "createRouter(options: RouterOptions | RouteRecord[]): Router",
|
|
263
|
+
example: `const router = createRouter([
|
|
264
|
+
{ path: "/", component: Home },
|
|
265
|
+
{ path: "/user/:id", component: User, loader: ({ params }) => fetchUser(params.id) },
|
|
266
|
+
{ path: "/admin", component: Admin, beforeEnter: requireAuth, children: [
|
|
267
|
+
{ path: "settings", component: Settings },
|
|
268
|
+
]},
|
|
269
|
+
])`,
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
"router/RouterProvider": {
|
|
273
|
+
signature: "<RouterProvider router={router}>{children}</RouterProvider>",
|
|
274
|
+
example: `const App = () => (
|
|
275
|
+
<RouterProvider router={router}>
|
|
276
|
+
<nav><RouterLink to="/">Home</RouterLink></nav>
|
|
277
|
+
<RouterView />
|
|
278
|
+
</RouterProvider>
|
|
279
|
+
)`,
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
"router/RouterView": {
|
|
283
|
+
signature: "<RouterView />",
|
|
284
|
+
example: `// Renders the matched route's component
|
|
285
|
+
<RouterView />
|
|
286
|
+
|
|
287
|
+
// Nested routes: parent component includes <RouterView /> for children
|
|
288
|
+
const Admin = () => (
|
|
289
|
+
<div>
|
|
290
|
+
<h1>Admin</h1>
|
|
291
|
+
<RouterView /> {/* renders Settings, Users, etc. */}
|
|
292
|
+
</div>
|
|
293
|
+
)`,
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
"router/RouterLink": {
|
|
297
|
+
signature: "<RouterLink to={path} activeClass={cls} exactActiveClass={cls} />",
|
|
298
|
+
example: `<RouterLink to="/" activeClass="nav-active">Home</RouterLink>
|
|
299
|
+
<RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink>`,
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
"router/useRouter": {
|
|
303
|
+
signature: "useRouter(): Router",
|
|
304
|
+
example: `const router = useRouter()
|
|
305
|
+
|
|
306
|
+
router.push("/settings")
|
|
307
|
+
router.push({ name: "user", params: { id: "42" } })
|
|
308
|
+
router.replace("/login")
|
|
309
|
+
router.back()
|
|
310
|
+
router.forward()
|
|
311
|
+
router.go(-2)`,
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
"router/useRoute": {
|
|
315
|
+
signature: "useRoute<TPath extends string>(): () => ResolvedRoute<ExtractParams<TPath>>",
|
|
316
|
+
example: `// Type-safe params:
|
|
317
|
+
const route = useRoute<"/user/:id">()
|
|
318
|
+
const userId = route().params.id // string
|
|
319
|
+
|
|
320
|
+
// Access query, meta, etc:
|
|
321
|
+
route().query
|
|
322
|
+
route().meta`,
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
"router/useSearchParams": {
|
|
326
|
+
signature:
|
|
327
|
+
"useSearchParams<T>(defaults?: T): [get: () => T, set: (updates: Partial<T>) => Promise<void>]",
|
|
328
|
+
example: `const [search, setSearch] = useSearchParams({ page: "1", sort: "name" })
|
|
329
|
+
|
|
330
|
+
// Read:
|
|
331
|
+
search().page // "1"
|
|
332
|
+
|
|
333
|
+
// Write:
|
|
334
|
+
setSearch({ page: "2" })`,
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
"router/useLoaderData": {
|
|
338
|
+
signature: "useLoaderData<T>(): T",
|
|
339
|
+
example: `// Route: { path: "/user/:id", component: User, loader: ({ params }) => fetchUser(params.id) }
|
|
340
|
+
|
|
341
|
+
const User = () => {
|
|
342
|
+
const data = useLoaderData<UserData>()
|
|
343
|
+
return <div>{data.name}</div>
|
|
344
|
+
}`,
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
348
|
+
// @pyreon/head
|
|
349
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
350
|
+
|
|
351
|
+
"head/useHead": {
|
|
352
|
+
signature: "useHead(input: UseHeadInput | (() => UseHeadInput)): void",
|
|
353
|
+
example: `// Static:
|
|
354
|
+
useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
|
|
355
|
+
|
|
356
|
+
// Reactive (updates when signals change):
|
|
357
|
+
useHead(() => ({
|
|
358
|
+
title: \`\${username()} — Profile\`,
|
|
359
|
+
meta: [{ property: "og:title", content: username() }]
|
|
360
|
+
}))`,
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
"head/HeadProvider": {
|
|
364
|
+
signature: "<HeadProvider>{children}</HeadProvider>",
|
|
365
|
+
example: `// Client-side setup:
|
|
366
|
+
mount(
|
|
367
|
+
<HeadProvider>
|
|
368
|
+
<App />
|
|
369
|
+
</HeadProvider>,
|
|
370
|
+
document.getElementById("app")!
|
|
371
|
+
)`,
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
375
|
+
// @pyreon/server
|
|
376
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
377
|
+
|
|
378
|
+
"server/createHandler": {
|
|
379
|
+
signature: "createHandler(options: HandlerOptions): (req: Request) => Promise<Response>",
|
|
380
|
+
example: `import { createHandler } from "@pyreon/server"
|
|
381
|
+
|
|
382
|
+
export default createHandler({
|
|
383
|
+
App,
|
|
384
|
+
routes,
|
|
385
|
+
clientEntry: "/src/entry-client.ts",
|
|
386
|
+
mode: "stream", // or "string"
|
|
387
|
+
})`,
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
"server/island": {
|
|
391
|
+
signature:
|
|
392
|
+
"island(loader: () => Promise<ComponentFn>, options: { name: string; hydrate?: HydrationStrategy }): ComponentFn",
|
|
393
|
+
example: `const SearchBar = island(
|
|
394
|
+
() => import("./SearchBar"),
|
|
395
|
+
{ name: "SearchBar", hydrate: "visible" }
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
// Hydration strategies: "load" | "idle" | "visible" | "media" | "never"`,
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
"server/prerender": {
|
|
402
|
+
signature: "prerender(options: PrerenderOptions): Promise<PrerenderResult>",
|
|
403
|
+
example: `await prerender({
|
|
404
|
+
handler,
|
|
405
|
+
paths: ["/", "/about", "/blog/1", "/blog/2"],
|
|
406
|
+
outDir: "./dist",
|
|
407
|
+
})`,
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
411
|
+
// @pyreon/runtime-dom
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
413
|
+
|
|
414
|
+
"runtime-dom/mount": {
|
|
415
|
+
signature: "mount(root: VNodeChild, container: Element): () => void",
|
|
416
|
+
example: `import { mount } from "@pyreon/runtime-dom"
|
|
417
|
+
|
|
418
|
+
const dispose = mount(<App />, document.getElementById("app")!)
|
|
419
|
+
|
|
420
|
+
// To unmount:
|
|
421
|
+
dispose()`,
|
|
422
|
+
mistakes: `- \`createRoot(container).render(<App />)\` → Use \`mount(<App />, container)\`
|
|
423
|
+
- Container must not be null/undefined`,
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
"runtime-dom/hydrateRoot": {
|
|
427
|
+
signature: "hydrateRoot(root: VNodeChild, container: Element): () => void",
|
|
428
|
+
example: `import { hydrateRoot } from "@pyreon/runtime-dom"
|
|
429
|
+
|
|
430
|
+
// Hydrate server-rendered HTML:
|
|
431
|
+
hydrateRoot(<App />, document.getElementById("app")!)`,
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
"runtime-dom/Transition": {
|
|
435
|
+
signature: "<Transition name={name} mode={mode}>{children}</Transition>",
|
|
436
|
+
example: `<Transition name="fade" mode="out-in">
|
|
437
|
+
<Show when={visible()}>
|
|
438
|
+
<div>Content</div>
|
|
439
|
+
</Show>
|
|
440
|
+
</Transition>
|
|
441
|
+
|
|
442
|
+
/* CSS:
|
|
443
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s }
|
|
444
|
+
.fade-enter-from, .fade-leave-to { opacity: 0 }
|
|
445
|
+
*/`,
|
|
446
|
+
},
|
|
447
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @pyreon/mcp — Model Context Protocol server for Pyreon
|
|
4
|
+
*
|
|
5
|
+
* Exposes tools that AI coding assistants (Claude Code, Cursor, etc.) can use
|
|
6
|
+
* to generate, validate, and migrate Pyreon code.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* get_api — Look up any Pyreon API: signature, usage, common mistakes
|
|
10
|
+
* validate — Check a code snippet for Pyreon anti-patterns
|
|
11
|
+
* migrate_react — Convert React code to idiomatic Pyreon
|
|
12
|
+
* diagnose — Parse an error message into structured fix information
|
|
13
|
+
* get_routes — List all routes in the current project
|
|
14
|
+
* get_components — List all components with their props and signals
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* bunx @pyreon/mcp # stdio transport (for IDE integration)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
21
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
22
|
+
import { detectReactPatterns, diagnoseError, migrateReactCode } from "@pyreon/compiler"
|
|
23
|
+
import { z } from "zod"
|
|
24
|
+
import { API_REFERENCE } from "./api-reference"
|
|
25
|
+
import { generateContext, type ProjectContext } from "./project-scanner"
|
|
26
|
+
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// Server setup
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: "pyreon",
|
|
33
|
+
version: "0.4.0",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Cache project context (regenerated on demand)
|
|
37
|
+
let cachedContext: ProjectContext | null = null
|
|
38
|
+
let contextCwd = process.cwd()
|
|
39
|
+
|
|
40
|
+
function getContext(): ProjectContext {
|
|
41
|
+
if (!cachedContext || contextCwd !== process.cwd()) {
|
|
42
|
+
contextCwd = process.cwd()
|
|
43
|
+
cachedContext = generateContext(contextCwd)
|
|
44
|
+
}
|
|
45
|
+
return cachedContext
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function textResult(text: string) {
|
|
49
|
+
return { content: [{ type: "text" as const, text }] }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
// Tool: get_api
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
// @ts-expect-error — MCP SDK + Zod generic inference is excessively deep
|
|
57
|
+
server.tool(
|
|
58
|
+
"get_api",
|
|
59
|
+
{
|
|
60
|
+
package: z.string(),
|
|
61
|
+
symbol: z.string(),
|
|
62
|
+
},
|
|
63
|
+
async ({ package: pkg, symbol }) => {
|
|
64
|
+
const key = `${pkg}/${symbol}`
|
|
65
|
+
const entry = API_REFERENCE[key]
|
|
66
|
+
|
|
67
|
+
if (!entry) {
|
|
68
|
+
const allKeys = Object.keys(API_REFERENCE)
|
|
69
|
+
const suggestions = allKeys
|
|
70
|
+
.filter((k) => k.toLowerCase().includes(symbol.toLowerCase()))
|
|
71
|
+
.slice(0, 5)
|
|
72
|
+
|
|
73
|
+
return textResult(
|
|
74
|
+
`Symbol '${symbol}' not found in @pyreon/${pkg}.\n\n${
|
|
75
|
+
suggestions.length > 0
|
|
76
|
+
? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}`
|
|
77
|
+
: "No similar symbols found."
|
|
78
|
+
}`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return textResult(
|
|
83
|
+
`## @pyreon/${pkg} — ${symbol}\n\n**Signature:**\n\`\`\`typescript\n${entry.signature}\n\`\`\`\n\n**Usage:**\n\`\`\`typescript\n${entry.example}\n\`\`\`\n\n${entry.notes ? `**Notes:** ${entry.notes}\n\n` : ""}${entry.mistakes ? `**Common mistakes:**\n${entry.mistakes}\n` : ""}`,
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
89
|
+
// Tool: validate
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
server.tool(
|
|
93
|
+
"validate",
|
|
94
|
+
{
|
|
95
|
+
code: z.string(),
|
|
96
|
+
filename: z.string().optional(),
|
|
97
|
+
},
|
|
98
|
+
async ({ code, filename }) => {
|
|
99
|
+
const diagnostics = detectReactPatterns(code, filename ?? "snippet.tsx")
|
|
100
|
+
|
|
101
|
+
if (diagnostics.length === 0) {
|
|
102
|
+
return textResult("✓ No issues found. The code follows Pyreon patterns correctly.")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const issueText = diagnostics
|
|
106
|
+
.map(
|
|
107
|
+
(d, i) =>
|
|
108
|
+
`${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ? "yes" : "no"}`,
|
|
109
|
+
)
|
|
110
|
+
.join("\n\n")
|
|
111
|
+
|
|
112
|
+
return textResult(
|
|
113
|
+
`Found ${diagnostics.length} issue${diagnostics.length === 1 ? "" : "s"}:\n\n${issueText}`,
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
119
|
+
// Tool: migrate_react
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
"migrate_react",
|
|
124
|
+
{
|
|
125
|
+
code: z.string(),
|
|
126
|
+
filename: z.string().optional(),
|
|
127
|
+
},
|
|
128
|
+
async ({ code, filename }) => {
|
|
129
|
+
const result = migrateReactCode(code, filename ?? "component.tsx")
|
|
130
|
+
|
|
131
|
+
const changeList = result.changes.map((c) => `- Line ${c.line}: ${c.description}`).join("\n")
|
|
132
|
+
|
|
133
|
+
const remainingIssues = result.diagnostics.filter((d) => !d.fixable)
|
|
134
|
+
const manualText =
|
|
135
|
+
remainingIssues.length > 0
|
|
136
|
+
? `\n\n**Remaining issues (manual fix needed):**\n${remainingIssues.map((d) => `- Line ${d.line}: ${d.message}\n Suggested: \`${d.suggested}\``).join("\n")}`
|
|
137
|
+
: ""
|
|
138
|
+
|
|
139
|
+
return textResult(
|
|
140
|
+
`## Migrated Code\n\n\`\`\`tsx\n${result.code}\n\`\`\`\n\n**Changes applied (${result.changes.length}):**\n${changeList || "No changes needed."}${manualText}`,
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
// Tool: diagnose
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
|
|
149
|
+
server.tool(
|
|
150
|
+
"diagnose",
|
|
151
|
+
{
|
|
152
|
+
error: z.string(),
|
|
153
|
+
},
|
|
154
|
+
async ({ error }) => {
|
|
155
|
+
const diagnosis = diagnoseError(error)
|
|
156
|
+
|
|
157
|
+
if (!diagnosis) {
|
|
158
|
+
return textResult(
|
|
159
|
+
`Could not identify a Pyreon-specific pattern in this error.\n\nError: ${error}\n\nSuggestions:\n- Check for typos in variable/function names\n- Verify all imports are correct\n- Run \`bun run typecheck\` for full TypeScript diagnostics\n- Run \`pyreon doctor\` for project-wide health check`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let text = `**Cause:** ${diagnosis.cause}\n\n**Fix:** ${diagnosis.fix}`
|
|
164
|
+
if (diagnosis.fixCode) {
|
|
165
|
+
text += `\n\n**Code:**\n\`\`\`typescript\n${diagnosis.fixCode}\n\`\`\``
|
|
166
|
+
}
|
|
167
|
+
if (diagnosis.related) {
|
|
168
|
+
text += `\n\n**Related:** ${diagnosis.related}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return textResult(text)
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
176
|
+
// Tool: get_routes
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
178
|
+
|
|
179
|
+
server.tool("get_routes", {}, async () => {
|
|
180
|
+
const ctx = getContext()
|
|
181
|
+
|
|
182
|
+
if (ctx.routes.length === 0) {
|
|
183
|
+
return textResult(
|
|
184
|
+
"No routes detected. Routes are defined via createRouter() or a routes array.",
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const routeTable = ctx.routes
|
|
189
|
+
.map((r) => {
|
|
190
|
+
const flags = [
|
|
191
|
+
r.hasLoader ? "loader" : "",
|
|
192
|
+
r.hasGuard ? "guard" : "",
|
|
193
|
+
r.params.length > 0 ? `params: ${r.params.join(", ")}` : "",
|
|
194
|
+
r.name ? `name: "${r.name}"` : "",
|
|
195
|
+
]
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
.join(", ")
|
|
198
|
+
|
|
199
|
+
return ` ${r.path}${flags ? ` (${flags})` : ""}`
|
|
200
|
+
})
|
|
201
|
+
.join("\n")
|
|
202
|
+
|
|
203
|
+
return textResult(`**Routes (${ctx.routes.length}):**\n\n${routeTable}`)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// Tool: get_components
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
server.tool("get_components", {}, async () => {
|
|
211
|
+
const ctx = getContext()
|
|
212
|
+
|
|
213
|
+
if (ctx.components.length === 0) {
|
|
214
|
+
return textResult("No components detected.")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const compList = ctx.components
|
|
218
|
+
.map((c) => {
|
|
219
|
+
const details = [
|
|
220
|
+
c.props.length > 0 ? `props: { ${c.props.join(", ")} }` : "",
|
|
221
|
+
c.hasSignals ? `signals: [${c.signalNames.join(", ")}]` : "",
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join(", ")
|
|
225
|
+
|
|
226
|
+
return ` ${c.name} — ${c.file}${details ? `\n ${details}` : ""}`
|
|
227
|
+
})
|
|
228
|
+
.join("\n")
|
|
229
|
+
|
|
230
|
+
return textResult(`**Components (${ctx.components.length}):**\n\n${compList}`)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
234
|
+
// Start server
|
|
235
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
236
|
+
|
|
237
|
+
async function main(): Promise<void> {
|
|
238
|
+
const transport = new StdioServerTransport()
|
|
239
|
+
await server.connect(transport)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
main().catch((err) => {
|
|
243
|
+
console.error("MCP server error:", err)
|
|
244
|
+
process.exit(1)
|
|
245
|
+
})
|