@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.
@@ -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
+ })