@pyreon/mcp 0.13.0 → 0.14.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/README.md +62 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1306 -303
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/anti-patterns.ts +210 -0
- package/src/api-reference.ts +495 -72
- package/src/changelog.ts +433 -0
- package/src/index.ts +279 -33
- package/src/manifest.ts +187 -0
- package/src/patterns.ts +243 -0
- package/src/tests/anti-patterns.test.ts +180 -0
- package/src/tests/changelog-server.test.ts +176 -0
- package/src/tests/changelog.test.ts +312 -0
- package/src/tests/manifest-snapshot.test.ts +36 -0
- package/src/tests/patterns-code.test.ts +216 -0
- package/src/tests/patterns-content.test.ts +147 -0
- package/src/tests/patterns-server.test.ts +160 -0
- package/src/tests/patterns.test.ts +236 -0
- package/src/tests/server-integration.test.ts +155 -0
- package/src/tests/test-audit-server.test.ts +128 -0
- package/src/tests/validate.test.ts +69 -0
package/src/index.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* get_routes — List all routes in the current project
|
|
14
14
|
* get_components — List all components with their props and signals
|
|
15
15
|
* get_browser_smoke_status — Report which browser-categorized packages have smoke coverage
|
|
16
|
+
* get_pattern — Fetch a "how do I do X" pattern body from docs/patterns/
|
|
17
|
+
* get_anti_patterns — Browse the anti-patterns catalog, optionally filtered by category
|
|
18
|
+
* get_changelog — Recent release notes for a @pyreon/* package, parsed from CHANGELOG.md
|
|
19
|
+
* audit_test_environment — Scan test files for mock-vnode patterns (PR #197 bug class)
|
|
16
20
|
*
|
|
17
21
|
* Usage:
|
|
18
22
|
* bunx @pyreon/mcp # stdio transport (for IDE integration)
|
|
@@ -20,42 +24,75 @@
|
|
|
20
24
|
|
|
21
25
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
22
26
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
23
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type AuditRisk,
|
|
29
|
+
auditTestEnvironment,
|
|
30
|
+
detectPyreonPatterns,
|
|
31
|
+
detectReactPatterns,
|
|
32
|
+
diagnoseError,
|
|
33
|
+
formatTestAudit,
|
|
34
|
+
migrateReactCode,
|
|
35
|
+
} from '@pyreon/compiler'
|
|
36
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
37
|
+
import { dirname, join, resolve } from 'node:path'
|
|
24
38
|
import { z } from 'zod'
|
|
25
39
|
import packageJson from '../package.json' with { type: 'json' }
|
|
40
|
+
import {
|
|
41
|
+
ANTI_PATTERN_CATEGORIES,
|
|
42
|
+
type AntiPatternCategory,
|
|
43
|
+
formatAntiPatterns,
|
|
44
|
+
parseAntiPatterns,
|
|
45
|
+
} from './anti-patterns'
|
|
26
46
|
import { API_REFERENCE } from './api-reference'
|
|
47
|
+
import {
|
|
48
|
+
findChangelog,
|
|
49
|
+
formatChangelog,
|
|
50
|
+
formatChangelogIndex,
|
|
51
|
+
loadChangelogRegistry,
|
|
52
|
+
suggestChangelogs,
|
|
53
|
+
} from './changelog'
|
|
54
|
+
import {
|
|
55
|
+
findPattern,
|
|
56
|
+
formatPatternBody,
|
|
57
|
+
formatPatternIndex,
|
|
58
|
+
loadPatternRegistry,
|
|
59
|
+
suggestPatterns,
|
|
60
|
+
} from './patterns'
|
|
27
61
|
import { generateContext, type ProjectContext } from './project-scanner'
|
|
28
62
|
|
|
29
63
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
-
// Server setup
|
|
64
|
+
// Server setup — exported as a factory so tests can stand up a server with an
|
|
65
|
+
// in-memory transport instead of stdio.
|
|
31
66
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
32
67
|
|
|
33
|
-
const server = new McpServer({
|
|
34
|
-
name: 'pyreon',
|
|
35
|
-
version: packageJson.version,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// Cache project context (regenerated on demand)
|
|
39
|
-
let cachedContext: ProjectContext | null = null
|
|
40
|
-
let contextCwd = process.cwd()
|
|
41
|
-
|
|
42
|
-
function getContext(): ProjectContext {
|
|
43
|
-
if (!cachedContext || contextCwd !== process.cwd()) {
|
|
44
|
-
contextCwd = process.cwd()
|
|
45
|
-
cachedContext = generateContext(contextCwd)
|
|
46
|
-
}
|
|
47
|
-
return cachedContext
|
|
48
|
-
}
|
|
49
|
-
|
|
50
68
|
function textResult(text: string) {
|
|
51
69
|
return { content: [{ type: 'text' as const, text }] }
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
export function createServer(): McpServer {
|
|
73
|
+
const server = new McpServer({
|
|
74
|
+
name: 'pyreon',
|
|
75
|
+
version: packageJson.version,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Project context cache is per-server-instance so the test server and the
|
|
79
|
+
// prod server do not share state.
|
|
80
|
+
let cachedContext: ProjectContext | null = null
|
|
81
|
+
let contextCwd = process.cwd()
|
|
82
|
+
|
|
83
|
+
function getContext(): ProjectContext {
|
|
84
|
+
if (!cachedContext || contextCwd !== process.cwd()) {
|
|
85
|
+
contextCwd = process.cwd()
|
|
86
|
+
cachedContext = generateContext(contextCwd)
|
|
87
|
+
}
|
|
88
|
+
return cachedContext
|
|
89
|
+
}
|
|
57
90
|
|
|
58
|
-
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
92
|
+
// Tool: get_api
|
|
93
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
94
|
+
|
|
95
|
+
server.tool(
|
|
59
96
|
'get_api',
|
|
60
97
|
{
|
|
61
98
|
package: z.string(),
|
|
@@ -97,13 +134,33 @@ server.tool(
|
|
|
97
134
|
filename: z.string().optional(),
|
|
98
135
|
},
|
|
99
136
|
async ({ code, filename }) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
// Run both detectors. The React detector flags "coming from React"
|
|
138
|
+
// mistakes (useState, className, .value writes) — relevant when the
|
|
139
|
+
// code has not yet committed to Pyreon. The Pyreon detector flags
|
|
140
|
+
// "using Pyreon wrong" mistakes (missing <For by>, destructured
|
|
141
|
+
// props, typeof-process dev gates) — relevant once the imports are
|
|
142
|
+
// Pyreon. A single snippet may trigger both sets, so we merge.
|
|
143
|
+
const fname = filename ?? 'snippet.tsx'
|
|
144
|
+
const reactDiags = detectReactPatterns(code, fname)
|
|
145
|
+
const pyreonDiags = detectPyreonPatterns(code, fname)
|
|
146
|
+
|
|
147
|
+
if (reactDiags.length === 0 && pyreonDiags.length === 0) {
|
|
103
148
|
return textResult('✓ No issues found. The code follows Pyreon patterns correctly.')
|
|
104
149
|
}
|
|
105
150
|
|
|
106
|
-
|
|
151
|
+
type Diag = {
|
|
152
|
+
code: string
|
|
153
|
+
message: string
|
|
154
|
+
line: number
|
|
155
|
+
column: number
|
|
156
|
+
current: string
|
|
157
|
+
suggested: string
|
|
158
|
+
fixable: boolean
|
|
159
|
+
}
|
|
160
|
+
const merged: Diag[] = [...reactDiags, ...pyreonDiags]
|
|
161
|
+
merged.sort((a, b) => a.line - b.line || a.column - b.column)
|
|
162
|
+
|
|
163
|
+
const issueText = merged
|
|
107
164
|
.map(
|
|
108
165
|
(d, i) =>
|
|
109
166
|
`${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'}`,
|
|
@@ -111,7 +168,7 @@ server.tool(
|
|
|
111
168
|
.join('\n\n')
|
|
112
169
|
|
|
113
170
|
return textResult(
|
|
114
|
-
`Found ${
|
|
171
|
+
`Found ${merged.length} issue${merged.length === 1 ? '' : 's'}:\n\n${issueText}`,
|
|
115
172
|
)
|
|
116
173
|
},
|
|
117
174
|
)
|
|
@@ -393,16 +450,205 @@ server.tool(
|
|
|
393
450
|
},
|
|
394
451
|
)
|
|
395
452
|
|
|
453
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
454
|
+
// Tool: get_pattern — serves docs/patterns/<name>.md
|
|
455
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
server.tool(
|
|
458
|
+
'get_pattern',
|
|
459
|
+
{
|
|
460
|
+
name: z
|
|
461
|
+
.string()
|
|
462
|
+
.optional()
|
|
463
|
+
.describe(
|
|
464
|
+
'Pattern slug (e.g. "dev-warnings", "controllable-state"). Omit to list available patterns.',
|
|
465
|
+
),
|
|
466
|
+
},
|
|
467
|
+
async ({ name }) => {
|
|
468
|
+
const registry = loadPatternRegistry()
|
|
469
|
+
if (!name) return textResult(formatPatternIndex(registry))
|
|
470
|
+
|
|
471
|
+
const pattern = findPattern(registry, name)
|
|
472
|
+
if (pattern) return textResult(formatPatternBody(pattern))
|
|
473
|
+
|
|
474
|
+
const suggestions = suggestPatterns(registry, name)
|
|
475
|
+
const suggestText =
|
|
476
|
+
suggestions.length > 0
|
|
477
|
+
? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join('\n')}\n\nOr call get_pattern() with no arg to see the full list.`
|
|
478
|
+
: 'Call get_pattern() with no arg to see available patterns.'
|
|
479
|
+
return textResult(`Pattern "${name}" not found.\n\n${suggestText}`)
|
|
480
|
+
},
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
484
|
+
// Tool: get_anti_patterns — parses .claude/rules/anti-patterns.md
|
|
485
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
486
|
+
|
|
487
|
+
server.tool(
|
|
488
|
+
'get_anti_patterns',
|
|
489
|
+
{
|
|
490
|
+
category: z
|
|
491
|
+
.enum([
|
|
492
|
+
'reactivity',
|
|
493
|
+
'jsx',
|
|
494
|
+
'context',
|
|
495
|
+
'architecture',
|
|
496
|
+
'testing',
|
|
497
|
+
'lifecycle',
|
|
498
|
+
'documentation',
|
|
499
|
+
'all',
|
|
500
|
+
])
|
|
501
|
+
.optional()
|
|
502
|
+
.describe(
|
|
503
|
+
`Category filter: ${ANTI_PATTERN_CATEGORIES.join(', ')}, or "all" (default).`,
|
|
504
|
+
),
|
|
505
|
+
},
|
|
506
|
+
async ({ category = 'all' }) => {
|
|
507
|
+
const cat = category as AntiPatternCategory | 'all'
|
|
508
|
+
const doc = loadAntiPatternsDoc()
|
|
509
|
+
if (!doc) {
|
|
510
|
+
return textResult(
|
|
511
|
+
'Could not locate `.claude/rules/anti-patterns.md`. This tool reads the file from the Pyreon monorepo — running in a consumer project without the rules directory surfaces this miss. File issues against pyreon/pyreon if the file exists but is not being found.',
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
const all = parseAntiPatterns(doc)
|
|
515
|
+
const filtered = cat === 'all' ? all : all.filter((e) => e.category === cat)
|
|
516
|
+
return textResult(formatAntiPatterns(filtered, cat))
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
521
|
+
// Tool: get_changelog — recent release notes for @pyreon/* packages
|
|
522
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
523
|
+
|
|
524
|
+
server.tool(
|
|
525
|
+
'get_changelog',
|
|
526
|
+
{
|
|
527
|
+
package: z
|
|
528
|
+
.string()
|
|
529
|
+
.optional()
|
|
530
|
+
.describe(
|
|
531
|
+
'Package name, e.g. "query" or "@pyreon/query". Omit to list every package with a CHANGELOG.',
|
|
532
|
+
),
|
|
533
|
+
limit: z
|
|
534
|
+
.number()
|
|
535
|
+
.int()
|
|
536
|
+
.positive()
|
|
537
|
+
.optional()
|
|
538
|
+
.describe('Maximum number of substantive versions to return. Default 5.'),
|
|
539
|
+
includeDependencyUpdates: z
|
|
540
|
+
.boolean()
|
|
541
|
+
.optional()
|
|
542
|
+
.describe('Include `Updated dependencies` bullets. Default false (usually noise).'),
|
|
543
|
+
since: z
|
|
544
|
+
.string()
|
|
545
|
+
.optional()
|
|
546
|
+
.describe(
|
|
547
|
+
'Only include versions strictly newer than this one (e.g. "0.12.0"). Useful when an agent knows the version it was trained against and wants just the delta.',
|
|
548
|
+
),
|
|
549
|
+
},
|
|
550
|
+
async ({ package: pkg, limit, includeDependencyUpdates, since }) => {
|
|
551
|
+
const registry = loadChangelogRegistry()
|
|
552
|
+
if (!pkg) return textResult(formatChangelogIndex(registry))
|
|
553
|
+
|
|
554
|
+
const changelog = findChangelog(registry, pkg)
|
|
555
|
+
if (changelog) {
|
|
556
|
+
return textResult(
|
|
557
|
+
formatChangelog(changelog, {
|
|
558
|
+
limit,
|
|
559
|
+
includeDependencyUpdates,
|
|
560
|
+
since,
|
|
561
|
+
}),
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const suggestions = suggestChangelogs(registry, pkg)
|
|
566
|
+
const suggestText =
|
|
567
|
+
suggestions.length > 0
|
|
568
|
+
? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join('\n')}\n\nOr call get_changelog() with no arg for the full list.`
|
|
569
|
+
: 'Call get_changelog() with no arg for the list of packages that ship a CHANGELOG.'
|
|
570
|
+
return textResult(`Changelog for "${pkg}" not found.\n\n${suggestText}`)
|
|
571
|
+
},
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
575
|
+
// Tool: audit_test_environment — mock-vnode pattern audit (T2.5.7)
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
577
|
+
|
|
578
|
+
server.tool(
|
|
579
|
+
'audit_test_environment',
|
|
580
|
+
{
|
|
581
|
+
minRisk: z
|
|
582
|
+
.enum(['high', 'medium', 'low'])
|
|
583
|
+
.optional()
|
|
584
|
+
.describe(
|
|
585
|
+
'Minimum risk level to surface. Default "medium" — HIGH + MEDIUM files. Use "high" for only the riskiest, "low" to see everything.',
|
|
586
|
+
),
|
|
587
|
+
limit: z
|
|
588
|
+
.number()
|
|
589
|
+
.int()
|
|
590
|
+
.positive()
|
|
591
|
+
.optional()
|
|
592
|
+
.describe('Maximum entries to show per risk group. Default 20.'),
|
|
593
|
+
},
|
|
594
|
+
async ({ minRisk, limit }) => {
|
|
595
|
+
const result = auditTestEnvironment(process.cwd())
|
|
596
|
+
return textResult(
|
|
597
|
+
formatTestAudit(result, {
|
|
598
|
+
minRisk: minRisk as AuditRisk | undefined,
|
|
599
|
+
limit,
|
|
600
|
+
}),
|
|
601
|
+
)
|
|
602
|
+
},
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return server
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Locate `.claude/rules/anti-patterns.md` by walking up from cwd.
|
|
610
|
+
* Returns the file contents or null if not found within 30 levels.
|
|
611
|
+
* Separate from the patterns loader because the doc path is fixed
|
|
612
|
+
* (`.claude/rules/`) — no glob needed.
|
|
613
|
+
*/
|
|
614
|
+
function loadAntiPatternsDoc(startDir: string = process.cwd()): string | null {
|
|
615
|
+
let dir = resolve(startDir)
|
|
616
|
+
for (let i = 0; i < 30; i++) {
|
|
617
|
+
const candidate = join(dir, '.claude', 'rules', 'anti-patterns.md')
|
|
618
|
+
if (existsSync(candidate)) {
|
|
619
|
+
try {
|
|
620
|
+
return readFileSync(candidate, 'utf8')
|
|
621
|
+
} catch {
|
|
622
|
+
return null
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const parent = dirname(dir)
|
|
626
|
+
if (parent === dir) return null
|
|
627
|
+
dir = parent
|
|
628
|
+
}
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
|
|
396
632
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
397
|
-
// Start server
|
|
633
|
+
// Start server (stdio transport) when invoked directly as a binary.
|
|
634
|
+
// Imports for tests do NOT auto-start — the integration test in
|
|
635
|
+
// `tests/validate.test.ts` wires up an in-memory transport instead.
|
|
398
636
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
399
637
|
|
|
400
638
|
async function main(): Promise<void> {
|
|
639
|
+
const server = createServer()
|
|
401
640
|
const transport = new StdioServerTransport()
|
|
402
641
|
await server.connect(transport)
|
|
403
642
|
}
|
|
404
643
|
|
|
405
|
-
main
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
644
|
+
// `import.meta.main` is Bun's "entry module" flag. The compiled Node bin
|
|
645
|
+
// (via bun build) preserves this — the bunx / tsx invocation of the
|
|
646
|
+
// shebang sets it truthy; `import { createServer } from '...'` does not.
|
|
647
|
+
// Covers both "run as CLI" and "imported by a test" without needing
|
|
648
|
+
// require.main shims.
|
|
649
|
+
if (import.meta.main) {
|
|
650
|
+
main().catch((err) => {
|
|
651
|
+
console.error('MCP server error:', err)
|
|
652
|
+
process.exit(1)
|
|
653
|
+
})
|
|
654
|
+
}
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { defineManifest } from '@pyreon/manifest'
|
|
2
|
+
|
|
3
|
+
export default defineManifest({
|
|
4
|
+
name: '@pyreon/mcp',
|
|
5
|
+
title: 'MCP Server',
|
|
6
|
+
tagline:
|
|
7
|
+
'Model Context Protocol server — live API lookup, validation, migration, anti-pattern catalog, changelog, test-environment audit',
|
|
8
|
+
description:
|
|
9
|
+
'MCP server (stdio transport) that exposes Pyreon\\\'s structured knowledge to AI coding assistants (Claude Code, Cursor, etc.). Eleven tools: `get_api` (look up any Pyreon API), `validate` (catch React + Pyreon-specific anti-patterns in a snippet), `migrate_react` (auto-convert React code), `diagnose` (parse a Pyreon error into structured fix info), `get_routes` / `get_components` (project introspection), `get_browser_smoke_status` (which packages need a browser smoke test), `get_pattern` (canonical "how do I do X" docs), `get_anti_patterns` (the catalog from `.claude/rules/anti-patterns.md`), `get_changelog` (recent release notes per package), and `audit_test_environment` (mock-vnode test scanner — PR #197 bug class).',
|
|
10
|
+
category: 'server',
|
|
11
|
+
features: [
|
|
12
|
+
'Eleven tools covering lookup, validation, migration, diagnosis, introspection, audit',
|
|
13
|
+
'stdio transport — drop-in compatible with every MCP client',
|
|
14
|
+
'Project context cached per server instance, auto-invalidates on cwd change',
|
|
15
|
+
'Manifest-driven — `get_api` reads `api-reference.ts`, regenerated from package manifests',
|
|
16
|
+
'AST-based detectors — `validate` catches React + Pyreon-specific patterns statically',
|
|
17
|
+
'Real-repo audit tools (`audit_test_environment`, `get_browser_smoke_status`) walk packages/',
|
|
18
|
+
],
|
|
19
|
+
longExample: `// .mcp/config.json — register the server with any MCP-aware client
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"pyreon": {
|
|
23
|
+
"command": "bunx",
|
|
24
|
+
"args": ["@pyreon/mcp"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Then from the client (Claude Code, Cursor, etc.):
|
|
30
|
+
// get_api({ package: 'flow', symbol: 'createFlow' })
|
|
31
|
+
// → signature, example, common mistakes
|
|
32
|
+
// validate({ code: '<MyButton onClick={handler}>...' })
|
|
33
|
+
// → React-pattern + Pyreon-pattern diagnostics with line/col
|
|
34
|
+
// get_pattern({ name: 'controllable-state' })
|
|
35
|
+
// → canonical pattern body from docs/patterns/
|
|
36
|
+
// get_anti_patterns({ category: 'reactivity' })
|
|
37
|
+
// → reactivity foot-guns from .claude/rules/anti-patterns.md
|
|
38
|
+
// get_changelog({ package: 'flow', limit: 5 })
|
|
39
|
+
// → recent release notes filtered through ceremonial-bump removal
|
|
40
|
+
// audit_test_environment({ minRisk: 'medium' })
|
|
41
|
+
// → mock-vnode test files ranked HIGH / MEDIUM / LOW`,
|
|
42
|
+
api: [
|
|
43
|
+
{
|
|
44
|
+
name: 'get_browser_smoke_status',
|
|
45
|
+
kind: 'constant',
|
|
46
|
+
signature: 'tool: get_browser_smoke_status — no args',
|
|
47
|
+
summary:
|
|
48
|
+
"Companion to the `pyreon/require-browser-smoke-test` lint rule. Reports which browser-categorized Pyreon packages have at least one `*.browser.test.{ts,tsx}` file under `src/`. Uses the same `.claude/rules/browser-packages.json` single source of truth as the rule + the CI script. Lets an AI agent check coverage before writing a new browser package (so it adds a smoke test in the same PR) instead of discovering the failure when CI runs. Falls back with a clear message if the JSON isn't present (e.g. consumer apps that don't ship the Pyreon monorepo layout).",
|
|
49
|
+
example: `// Ask the MCP server:
|
|
50
|
+
// "which Pyreon packages are missing browser smoke coverage?"
|
|
51
|
+
// Tool walks packages/, matches against .claude/rules/browser-packages.json,
|
|
52
|
+
// returns a coverage report.`,
|
|
53
|
+
mistakes: [
|
|
54
|
+
"Using the tool's output as a substitute for running the CI script — this tool only checks file existence, not the self-expiring-exemption check that `bun run lint:browser-smoke` performs",
|
|
55
|
+
],
|
|
56
|
+
seeAlso: ['audit_test_environment'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'get_api',
|
|
60
|
+
kind: 'constant',
|
|
61
|
+
signature: 'tool: get_api({ package: string; symbol: string }) → APIEntry',
|
|
62
|
+
summary:
|
|
63
|
+
'Look up any Pyreon API by `package` (e.g. `"flow"` or `"@pyreon/flow"`) and `symbol` (e.g. `"createFlow"`). Returns the canonical signature, example, foot-gun catalogue, and cross-references — drawn from `api-reference.ts`, which is regenerated from each package\\\'s `manifest.ts`. The single agent-facing entry point for "what does this API do and how do I avoid the common mistakes."',
|
|
64
|
+
example: `// Agent-side
|
|
65
|
+
get_api({ package: 'flow', symbol: 'createFlow' })
|
|
66
|
+
get_api({ package: '@pyreon/router', symbol: 'useTypedSearchParams' })`,
|
|
67
|
+
seeAlso: ['validate', 'get_pattern'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'validate',
|
|
71
|
+
kind: 'constant',
|
|
72
|
+
signature: 'tool: validate({ code: string; filename?: string }) → Diagnostics[]',
|
|
73
|
+
summary:
|
|
74
|
+
'Two AST-based detectors run in parallel: `detectReactPatterns` flags "coming from React" mistakes (`useState`, `useEffect`, `className`, `onChange` on inputs, React-package imports), and `detectPyreonPatterns` flags "using Pyreon wrong" mistakes (`<For>` missing `by`, props destructured at component signature, `typeof process` dev gates, raw `addEventListener`, `Date.now() + Math.random()` IDs). Diagnostics are merged + sorted by line / column for top-down reading.',
|
|
75
|
+
example: `validate({ code: \`
|
|
76
|
+
function MyComp(props) {
|
|
77
|
+
const { value } = props // → props-destructured
|
|
78
|
+
return <For each={items}>{...}</For> // → for-missing-by
|
|
79
|
+
}
|
|
80
|
+
\` })`,
|
|
81
|
+
seeAlso: ['get_anti_patterns', 'migrate_react'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'migrate_react',
|
|
85
|
+
kind: 'constant',
|
|
86
|
+
signature: 'tool: migrate_react({ code: string; filename?: string }) → MigrationResult',
|
|
87
|
+
summary:
|
|
88
|
+
'Convert React code to idiomatic Pyreon. Handles `useState` → `signal()`, `useEffect` → `effect()`, `className` → `class`, `onChange` → `onInput`, `useMemo` → `computed()`, React imports → Pyreon imports. Reports per-edit fixable diagnostics so callers can apply or review.',
|
|
89
|
+
example: `migrate_react({ code: \`
|
|
90
|
+
import { useState, useEffect } from 'react'
|
|
91
|
+
function Counter() {
|
|
92
|
+
const [count, setCount] = useState(0)
|
|
93
|
+
useEffect(() => { console.log(count) }, [count])
|
|
94
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
95
|
+
}
|
|
96
|
+
\` })`,
|
|
97
|
+
seeAlso: ['validate'],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'diagnose',
|
|
101
|
+
kind: 'constant',
|
|
102
|
+
signature: 'tool: diagnose({ error: string }) → DiagnoseResult',
|
|
103
|
+
summary:
|
|
104
|
+
'Parse a Pyreon runtime / build error message into structured fix information: probable cause, recommended fix, related docs, and the `.claude/rules/anti-patterns.md` entry (if any) the error matches. Useful when an agent sees a stack trace and wants to skip the "search the codebase for similar errors" step.',
|
|
105
|
+
example: `diagnose({ error: 'Cannot redefine property X on object [object Object]' })
|
|
106
|
+
// → cause: configurable: false on a getter; fix: set configurable: true`,
|
|
107
|
+
seeAlso: ['validate', 'get_anti_patterns'],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'get_routes',
|
|
111
|
+
kind: 'constant',
|
|
112
|
+
signature: 'tool: get_routes() → Route[]',
|
|
113
|
+
summary:
|
|
114
|
+
'List every route in the current project — path, loader presence, guards, params, and named-route name. Walks the project source from `process.cwd()` down. Cached per server instance with auto-invalidation on `cwd` change.',
|
|
115
|
+
example: `get_routes()
|
|
116
|
+
// → [{ path: '/', name: 'home', hasLoader: true, params: [] }, ...]`,
|
|
117
|
+
seeAlso: ['get_components'],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'get_components',
|
|
121
|
+
kind: 'constant',
|
|
122
|
+
signature: 'tool: get_components() → ComponentInfo[]',
|
|
123
|
+
summary:
|
|
124
|
+
'List every component in the current project with its props and signal usage. Same scanner as `get_routes`. Useful for an agent before generating new code that needs to reference existing components.',
|
|
125
|
+
example: `get_components()
|
|
126
|
+
// → [{ name: 'Button', file: 'src/Button.tsx', props: ['onClick', 'children'], signals: ['count'] }, ...]`,
|
|
127
|
+
seeAlso: ['get_routes'],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'get_pattern',
|
|
131
|
+
kind: 'constant',
|
|
132
|
+
signature: 'tool: get_pattern({ name?: string }) → PatternBody | string[]',
|
|
133
|
+
summary:
|
|
134
|
+
'Fetch a canonical "how do I do X" pattern body from `docs/patterns/`. Eight foundational patterns ship: `dev-warnings`, `controllable-state`, `ssr-safe-hooks`, `signal-writes`, `keyed-lists`, `reactive-context`, `event-listeners`, `form-fields`. Omit `name` to list available patterns. Drop a new `docs/patterns/<slug>.md` file to add one — picked up on next call.',
|
|
135
|
+
example: `get_pattern({ name: 'controllable-state' })
|
|
136
|
+
// → full canonical pattern body
|
|
137
|
+
get_pattern({})
|
|
138
|
+
// → [{ name: 'controllable-state', summary: '...' }, ...]`,
|
|
139
|
+
seeAlso: ['get_anti_patterns'],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'get_anti_patterns',
|
|
143
|
+
kind: 'constant',
|
|
144
|
+
signature:
|
|
145
|
+
"tool: get_anti_patterns({ category?: 'reactivity' | 'jsx' | 'context' | 'architecture' | 'testing' | 'lifecycle' | 'documentation' | 'all' }) → AntiPattern[]",
|
|
146
|
+
summary:
|
|
147
|
+
'Browse the anti-patterns catalog parsed from `.claude/rules/anti-patterns.md`. Each entry surfaces its `[detector: <code>]` tag inline so an agent can pair the catalog entry with the live static detector exposed by `validate`. Optional `category` filter; default returns all categories.',
|
|
148
|
+
example: `get_anti_patterns({ category: 'reactivity' })
|
|
149
|
+
// → ['Bare signal in JSX text', 'Stale closures', 'Destructuring props', ...]`,
|
|
150
|
+
seeAlso: ['validate', 'get_pattern'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'get_changelog',
|
|
154
|
+
kind: 'constant',
|
|
155
|
+
signature:
|
|
156
|
+
'tool: get_changelog({ package?: string; limit?: number; includeDependencyUpdates?: boolean; since?: string }) → ChangelogEntry[]',
|
|
157
|
+
summary:
|
|
158
|
+
'Recent release notes for any `@pyreon/*` package without scraping `git log`. Parses `packages/**/CHANGELOG.md` into version entries (`{ version, changes[], dependencyUpdates[], empty }`) and returns the N most recent substantive versions (default 5). Filters out ceremonial version bumps (pure dependency-update releases with no user-facing body) by default — opt back in with `includeDependencyUpdates: true`. `since: "0.12.0"` returns the delta from a known floor — useful when an agent knows the version it was trained against.',
|
|
159
|
+
example: `get_changelog({ package: 'flow', limit: 5 })
|
|
160
|
+
get_changelog({ package: '@pyreon/router', since: '0.12.0' })`,
|
|
161
|
+
seeAlso: ['get_api'],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'audit_test_environment',
|
|
165
|
+
kind: 'constant',
|
|
166
|
+
signature:
|
|
167
|
+
"tool: audit_test_environment({ minRisk?: 'high' | 'medium' | 'low'; limit?: number }) → AuditReport",
|
|
168
|
+
summary:
|
|
169
|
+
'Scan every `*.test.{ts,tsx}` under `packages/` for the mock-vnode anti-pattern that caused PR #197\\\'s silent metadata drop. Files are classified HIGH / MEDIUM / LOW based on the balance of mock-vnode literals + helpers + helper-call sites vs real `h()` calls + `@pyreon/core` import. Three context-aware skips (helper-def vs binding discrimination, type-guard call-arg skip, template-string fixture mask) keep the false-positive rate low. Run before merging a new test file or after a framework change.',
|
|
170
|
+
example: `audit_test_environment({ minRisk: 'medium', limit: 10 })
|
|
171
|
+
// → grouped report with HIGH / MEDIUM / LOW sections`,
|
|
172
|
+
seeAlso: ['get_browser_smoke_status'],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
gotchas: [
|
|
176
|
+
{
|
|
177
|
+
label: 'Project-context caching',
|
|
178
|
+
note:
|
|
179
|
+
'Each `createServer()` instance maintains its own cached context (routes, components, islands). The cache auto-resets when `process.cwd()` changes between tool invocations, so the same server can operate across multiple projects in one session.',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
label: 'Manifest-driven',
|
|
183
|
+
note:
|
|
184
|
+
'`get_api` reads `api-reference.ts`, which is generated from each package\\\'s `manifest.ts`. The marker-pair protocol (`<gen-docs:api-reference:start @pyreon/<name>>`) lets some packages be generated and others stay hand-written during incremental migration.',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
})
|