@pyreon/mcp 0.13.1 → 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.
@@ -1 +1,7 @@
1
- export { };
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ //#region src/index.d.ts
4
+ declare function createServer(): McpServer;
5
+ //#endregion
6
+ export { createServer };
7
+ //# sourceMappingURL=index2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;iBAuEgB,YAAA,CAAA,GAAgB,SAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/mcp",
3
- "version": "0.13.1",
3
+ "version": "0.14.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": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@pyreon/compiler": "^0.13.1",
49
+ "@pyreon/compiler": "^0.14.0",
50
50
  "zod": "^4.3.6"
51
51
  },
52
52
  "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
+ }