@pyreon/mcp
v0.15.0
Published
MCP server for Pyreon — AI-powered framework assistance
Readme
@pyreon/mcp
Model Context Protocol server for AI-assisted Pyreon development. Gives AI coding assistants direct access to Pyreon's API reference, code validation, React-to-Pyreon migration, and project scanning.
Install
bun add -d @pyreon/mcpQuick Start
bunx @pyreon/mcp # starts stdio MCP serverIDE Integration
Claude Code
// .mcp.json (project root)
{
"mcpServers": {
"pyreon": {
"command": "bunx",
"args": ["@pyreon/mcp"]
}
}
}Cursor
// .cursor/mcp.json
{
"mcpServers": {
"pyreon": {
"command": "bunx",
"args": ["@pyreon/mcp"]
}
}
}Windsurf
// .windsurf/mcp.json (same format as Cursor)
{
"mcpServers": {
"pyreon": {
"command": "bunx",
"args": ["@pyreon/mcp"]
}
}
}Tools
get_api — Look up any Pyreon API
get_api({ package: "reactivity", symbol: "signal" })Returns signature, usage example, common mistakes. Covers all @pyreon/* packages.
Example response:
## @pyreon/reactivity — signal
**Signature:**
signal<T>(initialValue: T): Signal<T>
**Usage:**
const count = signal(0)
count() // read: 0
count.set(1) // write
count.update(n => n + 1) // update
**Common mistakes:**
- Using .value instead of calling the signal: count() not count.value
- Forgetting to call signal() when reading in JSX: {count()} not {count}validate — Check code for anti-patterns
validate({ code: "import { useState } from 'react'" })Runs two detectors and returns the merged result, sorted by source line:
- React / coming-from-React mistakes —
useState,useEffect,useMemo,useRef,className,htmlFor,onChangeon inputs,.valuewrites on signals,Array.map()in JSX,memo/forwardRefwrappers, React-package imports. - Pyreon / using-Pyreon-wrong mistakes (added 2026-Q2):
for-missing-by—<For each={...}>without abyprop.for-with-key—<For key={...}>(the keying prop isby; JSX reserveskey).props-destructured—({ foo }: Props) => <JSX />captures props at setup and loses reactivity. Useprops.fooorsplitProps(props, [...]).process-dev-gate—typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'is dead code in real Vite browser bundles. Useimport.meta.env?.DEV.empty-theme—.theme({})is a no-op chain.raw-add-event-listener/raw-remove-event-listener— useuseEventListenerfrom@pyreon/hooksfor auto-cleanup.date-math-random-id—Date.now() + Math.random()ID schemes collide under rapid operations; use a monotonic counter.on-click-undefined—onClick={undefined}(or anyon*={undefined}); omit the prop or gate with a ternary.
Each diagnostic carries { code, message, line, column, current, suggested, fixable }. React diagnostics may report fixable: true (handled by the migrate_react tool). All Pyreon diagnostics report fixable: false — no companion migrate_pyreon tool exists yet, so claiming auto-fix here would mislead consumers wiring UX off the flag. The invariant is locked in packages/core/compiler/src/tests/pyreon-intercept.test.ts under "fixable contract".
migrate_react — Convert React code to Pyreon
migrate_react({
code: `
import { useState, useEffect } from "react"
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000)
return () => clearInterval(id)
}, [])
return <div className="timer">{seconds}s</div>
}
`
})Response:
import { signal, effect } from '@pyreon/reactivity'
function Timer() {
const seconds = signal(0)
effect(() => {
const id = setInterval(() => seconds.update((s) => s + 1), 1000)
return () => clearInterval(id)
})
return <div class="timer">{seconds()}s</div>
}diagnose — Parse error messages
diagnose({ error: "TypeError: count is not a function" })Response:
**Cause:** Signal accessed without calling it — signals are functions.
**Fix:** Call the signal to read its value: count() instead of count.
// Wrong:
<div>{count}</div>
// Right:
<div>{count()}</div>get_pattern — Fetch a "how do I do X" pattern
get_pattern({ name: "dev-warnings" }) # full body of the pattern
get_pattern({}) # list available patternsReads docs/patterns/<name>.md from the Pyreon monorepo. Each pattern file answers a "how do I do X the right way" question with a correct example, the rationale, and the anti-pattern to avoid. 8 foundational patterns ship today: dev-warnings, controllable-state, ssr-safe-hooks, signal-writes, keyed-lists, reactive-context, event-listeners, form-fields. A misspelled name returns substring-match suggestions. Complements get_api (symbol reference) and get_anti_patterns (catalogue of mistakes).
get_anti_patterns — Browse the anti-patterns catalog
get_anti_patterns({}) # full list
get_anti_patterns({ category: "reactivity" }) # single categoryParses .claude/rules/anti-patterns.md into per-category listings. Valid categories: reactivity, jsx, context, architecture, testing, lifecycle, documentation, all. Each entry carries a title, a rationale, and — when applicable — a [detector: <code>] tag pairing it with a validate-tool diagnostic. Use it BEFORE writing new code: the catalog is the surface validate enforces reactively.
get_changelog — Recent release notes for a @pyreon/* package
get_changelog({ package: "query" }) # latest 5 substantive versions
get_changelog({ package: "query", limit: 10 }) # expand the window
get_changelog({ package: "query", since: "0.12.0" }) # only versions newer than 0.12.0
get_changelog({ package: "query", includeDependencyUpdates: true })
get_changelog({}) # list every package + latest versionParses the CHANGELOG.md file (changesets-populated) for the named package. The short slug auto-prefixes @pyreon/ — "query" and "@pyreon/query" resolve to the same result. Empty "ceremonial" version bumps (pure dependency bumps with no user-facing body) are filtered by default; when the whole history is ceremonial, the tool surfaces a clear "no substantive changes" message. since accepts any semver-ish string changesets emits ("0.13.0", "1.0.0-alpha.3") — when the floor equals or exceeds the latest substantive version, the tool returns a dedicated "no changes since vX" miss message. Complements get_api (current symbol reference) — changelog answers "what changed" while api-reference answers "what is it now".
get_routes — List project routes
get_routes({})Scans for createRouter([...]) and const routes = [...] patterns.
get_components — List components with props and signals
get_components({})Returns component names, file paths, props, and signal usage.
audit_test_environment — Scan tests for mock-vnode patterns
audit_test_environment({}) # risk-grouped report (minRisk: medium)
audit_test_environment({ minRisk: "high" }) # only the riskiest
audit_test_environment({ minRisk: "low", limit: 3 }) # full coverage, 3 per groupScans every *.test.ts / *.test.tsx file under the packages tree for mock-vnode patterns — tests that construct { type, props, children } object literals or a custom vnode() helper instead of going through the real h() from @pyreon/core. This class of pattern silently drops rocketstyle / compiler / attrs work from the pipeline and was the cause of PR #197's silent metadata drop.
Classification per file:
- HIGH — mock patterns present, no real
h()calls, and nohimport from@pyreon/core. At risk: the file has no pathway to exercise the real pipeline. - MEDIUM — mock patterns present AND some real
h()usage, but mocks outnumber real calls. Spot-check coverage. - LOW — either no mocks, or mock count is dwarfed by real usage.
Each entry reports the literal count, helper count (definitions of vnode / mockVNode / createVNode / VNodeMock / makeVNode), real h(...) call count, and whether h is imported. Use the tool BEFORE modifying a test file to see if strengthening is warranted, or AFTER a framework change to audit for regression.
Pyreon Patterns for AI
These are the key patterns the MCP server teaches AI assistants:
State management
// React // Pyreon
const [x, setX] = useState(0) const x = signal(0)
const val = useMemo(() => a+b, [a,b]) const val = computed(() => a()+b())
useEffect(() => { ... }, [dep]) effect(() => { ... })JSX differences
// React // Pyreon
<div className="box" /> <div class="box" />
<label htmlFor="name" /> <label for="name" />
<input onChange={handler} /> <input onInput={handler} />
{condition && <Child />} <Show when={condition}><Child /></Show>
{items.map(i => <li key={i.id}>)} <For each={items} by={i => i.id}>{i => <li>}</For>Component patterns
// React: hooks, re-renders entire component
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// Pyreon: signals, fine-grained DOM updates (no re-render)
function Counter() {
const count = signal(0)
return <button onClick={() => count.update((c) => c + 1)}>{count()}</button>
}Refs
// React
const ref = useRef<HTMLDivElement>(null)
<div ref={ref} />
// Pyreon: object ref
const ref = { current: null as HTMLDivElement | null }
<div ref={ref} />
// Pyreon: callback ref
<div ref={(el) => { myElement = el }} />Context
// React
const ThemeCtx = React.createContext("light")
<ThemeCtx.Provider value="dark"><App /></ThemeCtx.Provider>
const theme = useContext(ThemeCtx)
// Pyreon
const ThemeCtx = createContext<string>("light")
<ThemeCtx.Provider value="dark"><App /></ThemeCtx.Provider>
const theme = useContext(ThemeCtx) // same APILifecycle
// React
useEffect(() => {
const handler = () => { ... }
window.addEventListener("resize", handler)
return () => window.removeEventListener("resize", handler)
}, [])
// Pyreon
onMount(() => {
const handler = () => { ... }
window.addEventListener("resize", handler)
return () => window.removeEventListener("resize", handler) // cleanup
})Routing
// React Router
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<User />} />
</Routes>
</BrowserRouter>
// Pyreon Router
const router = createRouter({
routes: [
{ path: "/", component: Home },
{ path: "/user/:id", component: User },
],
})
<RouterProvider router={router}>
<RouterView />
</RouterProvider>Lists
// React: key on element
{
items.map((item) => <li key={item.id}>{item.name}</li>)
}
// Pyreon: by prop on For (not key — JSX extracts key specially)
;<For each={items} by={(item) => item.id}>
{(item) => <li>{item.name}</li>}
</For>Lazy loading
// React
const Dashboard = React.lazy(() => import("./Dashboard"))
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
// Pyreon
const Dashboard = lazy(() => import("./Dashboard"))
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>Islands (partial hydration)
// Server: define island
import { island } from "@pyreon/server"
const Counter = island(() => import("./Counter"), {
name: "Counter",
hydrate: "visible", // load | idle | visible | media(...) | never
})
// Use in page (renders static HTML, hydrates on client)
<Counter initial={0} />
// Client: register islands
import { hydrateIslands } from "@pyreon/server/client"
hydrateIslands({
Counter: () => import("./Counter"),
})