@pyreon/mcp 0.13.1 → 0.15.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 +1346 -12977
- package/lib/types/index.d.ts +7 -1
- package/package.json +3 -2
- package/src/anti-patterns.ts +210 -0
- package/src/api-reference.ts +580 -76
- 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/lib/index.js.map +0 -1
package/lib/types/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "MCP server for Pyreon — AI-powered framework assistance",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/mcp#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"lib",
|
|
20
|
+
"!lib/**/*.map",
|
|
20
21
|
"src",
|
|
21
22
|
"README.md",
|
|
22
23
|
"LICENSE"
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"@pyreon/compiler": "^0.
|
|
50
|
+
"@pyreon/compiler": "^0.15.0",
|
|
50
51
|
"zod": "^4.3.6"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for `.claude/rules/anti-patterns.md`. Drives the `get_anti_patterns`
|
|
3
|
+
* MCP tool (T2.5.4) and the `detector-tag-consistency` test, so a single
|
|
4
|
+
* canonical source of truth produces the AI-facing list, the doc file,
|
|
5
|
+
* and the drift guard.
|
|
6
|
+
*
|
|
7
|
+
* Format assumptions (enforced by the consistency test):
|
|
8
|
+
* - Top-level category headings are `## <Name>` (second-level). The
|
|
9
|
+
* first paragraph (before the first `## `) is intro prose and is
|
|
10
|
+
* not returned as anti-patterns.
|
|
11
|
+
* - Each anti-pattern is a line that starts with `- **Name**` at
|
|
12
|
+
* column 0 and can continue onto subsequent lines (until the next
|
|
13
|
+
* `- **` or `## `).
|
|
14
|
+
* - An optional `[detector: <code>]` tag appears anywhere in the
|
|
15
|
+
* bullet's first line — it pairs the bullet with a static
|
|
16
|
+
* `PyreonDiagnosticCode`. Missing tag means the anti-pattern is
|
|
17
|
+
* doc-only.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type AntiPatternCategory =
|
|
21
|
+
| 'reactivity'
|
|
22
|
+
| 'jsx'
|
|
23
|
+
| 'context'
|
|
24
|
+
| 'architecture'
|
|
25
|
+
| 'testing'
|
|
26
|
+
| 'lifecycle'
|
|
27
|
+
| 'documentation'
|
|
28
|
+
|
|
29
|
+
export interface AntiPatternEntry {
|
|
30
|
+
/** Title extracted from `**...**` in the bullet */
|
|
31
|
+
name: string
|
|
32
|
+
/** Normalised category slug (matches the enum above) */
|
|
33
|
+
category: AntiPatternCategory
|
|
34
|
+
/** Category heading as it appears in the file (e.g. "Reactivity Mistakes") */
|
|
35
|
+
categoryHeading: string
|
|
36
|
+
/** Body text after the title, minus the detector tag */
|
|
37
|
+
description: string
|
|
38
|
+
/** Detector codes listed in `[detector: X / Y]` or `null` if none */
|
|
39
|
+
detectorCodes: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Heading → slug. Keep in sync with the anti-patterns.md section list.
|
|
43
|
+
const CATEGORY_MAP: Record<string, AntiPatternCategory> = {
|
|
44
|
+
'Reactivity Mistakes': 'reactivity',
|
|
45
|
+
'JSX Mistakes': 'jsx',
|
|
46
|
+
'Context & Provider Mistakes': 'context',
|
|
47
|
+
'Architecture Mistakes': 'architecture',
|
|
48
|
+
'Testing Mistakes': 'testing',
|
|
49
|
+
'Lifecycle & Cleanup Mistakes': 'lifecycle',
|
|
50
|
+
'Documentation Mistakes': 'documentation',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ANTI_PATTERN_CATEGORIES: readonly AntiPatternCategory[] = [
|
|
54
|
+
'reactivity',
|
|
55
|
+
'jsx',
|
|
56
|
+
'context',
|
|
57
|
+
'architecture',
|
|
58
|
+
'testing',
|
|
59
|
+
'lifecycle',
|
|
60
|
+
'documentation',
|
|
61
|
+
] as const
|
|
62
|
+
|
|
63
|
+
function normaliseCategory(heading: string): AntiPatternCategory | null {
|
|
64
|
+
const trimmed = heading.trim()
|
|
65
|
+
return CATEGORY_MAP[trimmed] ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function splitSections(doc: string): Array<{ heading: string; body: string }> {
|
|
69
|
+
const lines = doc.split('\n')
|
|
70
|
+
const sections: Array<{ heading: string; body: string }> = []
|
|
71
|
+
let currentHeading: string | null = null
|
|
72
|
+
let currentBody: string[] = []
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const headingMatch = /^## (.+)$/.exec(line)
|
|
75
|
+
if (headingMatch) {
|
|
76
|
+
if (currentHeading !== null) {
|
|
77
|
+
sections.push({ heading: currentHeading, body: currentBody.join('\n') })
|
|
78
|
+
}
|
|
79
|
+
currentHeading = headingMatch[1]!
|
|
80
|
+
currentBody = []
|
|
81
|
+
} else if (currentHeading !== null) {
|
|
82
|
+
currentBody.push(line)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (currentHeading !== null) {
|
|
86
|
+
sections.push({ heading: currentHeading, body: currentBody.join('\n') })
|
|
87
|
+
}
|
|
88
|
+
return sections
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function splitBullets(sectionBody: string): string[] {
|
|
92
|
+
// Split on lines that start with `- **` at column 0. Continuation
|
|
93
|
+
// lines (any indented or non-bullet content) stay attached to the
|
|
94
|
+
// previous bullet.
|
|
95
|
+
const lines = sectionBody.split('\n')
|
|
96
|
+
const bullets: string[] = []
|
|
97
|
+
let current: string[] = []
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (/^- \*\*/.test(line)) {
|
|
100
|
+
if (current.length > 0) bullets.push(current.join('\n').trim())
|
|
101
|
+
current = [line]
|
|
102
|
+
} else if (current.length > 0) {
|
|
103
|
+
current.push(line)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (current.length > 0) bullets.push(current.join('\n').trim())
|
|
107
|
+
return bullets.filter((b) => b.length > 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseBullet(bullet: string): {
|
|
111
|
+
name: string
|
|
112
|
+
description: string
|
|
113
|
+
detectorCodes: string[]
|
|
114
|
+
} | null {
|
|
115
|
+
// `- **Name** [detector: ...]: body...` or `- **Name**: body...`
|
|
116
|
+
// Extract the **bolded** name first.
|
|
117
|
+
const nameMatch = /^- \*\*([^*]+)\*\*/.exec(bullet)
|
|
118
|
+
if (!nameMatch) return null
|
|
119
|
+
const name = nameMatch[1]!.trim()
|
|
120
|
+
|
|
121
|
+
const afterName = bullet.slice(nameMatch[0].length)
|
|
122
|
+
|
|
123
|
+
// Pull out the detector tag if present. It can appear as:
|
|
124
|
+
// ` [detector: code]`
|
|
125
|
+
// ` \`[detector: code]\``
|
|
126
|
+
const detectorMatch = /`?\[detector:\s*([a-z0-9\-\/ ]+)\]`?/i.exec(afterName)
|
|
127
|
+
const detectorCodes: string[] = []
|
|
128
|
+
if (detectorMatch) {
|
|
129
|
+
for (const code of detectorMatch[1]!.split('/')) {
|
|
130
|
+
const c = code.trim()
|
|
131
|
+
if (c) detectorCodes.push(c)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Strip the detector tag + any leading `:` or spaces from the body.
|
|
136
|
+
let description = afterName
|
|
137
|
+
if (detectorMatch) {
|
|
138
|
+
description = description.replace(detectorMatch[0], '')
|
|
139
|
+
}
|
|
140
|
+
description = description.replace(/^[\s:]+/, '').trim()
|
|
141
|
+
|
|
142
|
+
return { name, description, detectorCodes }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function parseAntiPatterns(doc: string): AntiPatternEntry[] {
|
|
146
|
+
const sections = splitSections(doc)
|
|
147
|
+
const entries: AntiPatternEntry[] = []
|
|
148
|
+
for (const { heading, body } of sections) {
|
|
149
|
+
const category = normaliseCategory(heading)
|
|
150
|
+
if (!category) continue
|
|
151
|
+
for (const bullet of splitBullets(body)) {
|
|
152
|
+
const parsed = parseBullet(bullet)
|
|
153
|
+
if (!parsed) continue
|
|
154
|
+
entries.push({
|
|
155
|
+
name: parsed.name,
|
|
156
|
+
category,
|
|
157
|
+
categoryHeading: heading,
|
|
158
|
+
description: parsed.description,
|
|
159
|
+
detectorCodes: parsed.detectorCodes,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return entries
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Format a list of entries into a single Markdown block suitable for MCP. */
|
|
167
|
+
export function formatAntiPatterns(
|
|
168
|
+
entries: AntiPatternEntry[],
|
|
169
|
+
filterCategory: AntiPatternCategory | 'all',
|
|
170
|
+
): string {
|
|
171
|
+
if (entries.length === 0) {
|
|
172
|
+
return filterCategory === 'all'
|
|
173
|
+
? 'No anti-patterns found. Check that `.claude/rules/anti-patterns.md` is reachable.'
|
|
174
|
+
: `No anti-patterns found in category '${filterCategory}'. Valid categories: ${ANTI_PATTERN_CATEGORIES.join(', ')}, all.`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Group by category preserving the file order.
|
|
178
|
+
const byCategory = new Map<AntiPatternCategory, AntiPatternEntry[]>()
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (!byCategory.has(entry.category)) byCategory.set(entry.category, [])
|
|
181
|
+
byCategory.get(entry.category)!.push(entry)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const parts: string[] = []
|
|
185
|
+
const header =
|
|
186
|
+
filterCategory === 'all'
|
|
187
|
+
? `# Pyreon Anti-Patterns (${entries.length} total, ${byCategory.size} categor${byCategory.size === 1 ? 'y' : 'ies'})`
|
|
188
|
+
: `# Pyreon Anti-Patterns — ${filterCategory} (${entries.length})`
|
|
189
|
+
parts.push(header)
|
|
190
|
+
parts.push('')
|
|
191
|
+
parts.push(
|
|
192
|
+
'Each entry is a known mistake documented at `.claude/rules/anti-patterns.md`. Entries tagged `[detector: <code>]` are caught statically by the MCP `validate` tool — the rest require a human / AI review. Read them BEFORE writing new code, not during code review.',
|
|
193
|
+
)
|
|
194
|
+
parts.push('')
|
|
195
|
+
|
|
196
|
+
for (const [, catEntries] of byCategory) {
|
|
197
|
+
parts.push(`## ${catEntries[0]!.categoryHeading} (${catEntries.length})`)
|
|
198
|
+
parts.push('')
|
|
199
|
+
for (const entry of catEntries) {
|
|
200
|
+
const tag =
|
|
201
|
+
entry.detectorCodes.length > 0
|
|
202
|
+
? ` \`[detector: ${entry.detectorCodes.join(' / ')}]\``
|
|
203
|
+
: ''
|
|
204
|
+
parts.push(`- **${entry.name}**${tag}: ${entry.description}`)
|
|
205
|
+
}
|
|
206
|
+
parts.push('')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return parts.join('\n').trimEnd()
|
|
210
|
+
}
|