@pyreon/mcp 0.14.0 → 0.16.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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +865 -12740
- package/lib/manifest-BCmgA06z.js +245 -0
- package/package.json +3 -2
- package/src/anti-patterns.ts +2 -2
- package/src/api-reference.ts +893 -21
- package/src/changelog.ts +3 -3
- package/src/index.ts +53 -0
- package/src/manifest.ts +40 -6
- package/src/tests/audit-islands-server.test.ts +100 -0
- package/src/tests/changelog-server.test.ts +1 -30
- package/src/tests/changelog.test.ts +1 -1
- package/src/tests/diagnose-server.test.ts +76 -0
- package/src/tests/get-api-server.test.ts +66 -0
- package/src/tests/get-browser-smoke-server.test.ts +75 -0
- package/src/tests/get-components-server.test.ts +57 -0
- package/src/tests/get-routes-server.test.ts +67 -0
- package/src/tests/helpers.ts +57 -0
- package/src/tests/manifest-snapshot.test.ts +2 -0
- package/src/tests/mcp-overview.test.ts +110 -0
- package/src/tests/migrate-react-server.test.ts +77 -0
- package/src/tests/patterns-content.test.ts +5 -1
- package/src/tests/patterns-server.test.ts +1 -30
- package/src/tests/patterns.test.ts +2 -1
- package/src/tests/test-audit-server.test.ts +1 -30
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/src/changelog.ts
CHANGED
|
@@ -177,16 +177,16 @@ export function parseChangelog(body: string): ChangelogEntry[] {
|
|
|
177
177
|
|
|
178
178
|
// `### Patch Changes` / `### Minor Changes` / `### Major Changes` — ignore
|
|
179
179
|
// the heading itself but keep reading bullets under it.
|
|
180
|
-
if (
|
|
180
|
+
if (line.startsWith('### ')) {
|
|
181
181
|
flushBullet()
|
|
182
182
|
continue
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// Start of a new top-level bullet.
|
|
186
|
-
if (
|
|
186
|
+
if (line.startsWith('- ')) {
|
|
187
187
|
flushBullet()
|
|
188
188
|
currentBuf = [line.replace(/^- /, '')]
|
|
189
|
-
bufKind =
|
|
189
|
+
bufKind = line.startsWith('- Updated dependencies') ? 'dep' : 'change'
|
|
190
190
|
continue
|
|
191
191
|
}
|
|
192
192
|
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* to generate, validate, and migrate Pyreon code.
|
|
7
7
|
*
|
|
8
8
|
* Tools:
|
|
9
|
+
* mcp_overview — Discoverability map: every tool's "when to use" + example, in one call
|
|
9
10
|
* get_api — Look up any Pyreon API: signature, usage, common mistakes
|
|
10
11
|
* validate — Check a code snippet for Pyreon anti-patterns
|
|
11
12
|
* migrate_react — Convert React code to idiomatic Pyreon
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
* get_anti_patterns — Browse the anti-patterns catalog, optionally filtered by category
|
|
18
19
|
* get_changelog — Recent release notes for a @pyreon/* package, parsed from CHANGELOG.md
|
|
19
20
|
* audit_test_environment — Scan test files for mock-vnode patterns (PR #197 bug class)
|
|
21
|
+
* audit_islands — Project-wide islands audit (5 cross-file foot-guns)
|
|
20
22
|
*
|
|
21
23
|
* Usage:
|
|
22
24
|
* bunx @pyreon/mcp # stdio transport (for IDE integration)
|
|
@@ -26,10 +28,12 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
|
26
28
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
27
29
|
import {
|
|
28
30
|
type AuditRisk,
|
|
31
|
+
auditIslands,
|
|
29
32
|
auditTestEnvironment,
|
|
30
33
|
detectPyreonPatterns,
|
|
31
34
|
detectReactPatterns,
|
|
32
35
|
diagnoseError,
|
|
36
|
+
formatIslandAudit,
|
|
33
37
|
formatTestAudit,
|
|
34
38
|
migrateReactCode,
|
|
35
39
|
} from '@pyreon/compiler'
|
|
@@ -602,6 +606,55 @@ server.tool(
|
|
|
602
606
|
},
|
|
603
607
|
)
|
|
604
608
|
|
|
609
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
610
|
+
// Tool: audit_islands — project-wide islands audit (PR C of islands DX roadmap)
|
|
611
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
612
|
+
|
|
613
|
+
server.tool(
|
|
614
|
+
'audit_islands',
|
|
615
|
+
{
|
|
616
|
+
json: z
|
|
617
|
+
.boolean()
|
|
618
|
+
.optional()
|
|
619
|
+
.describe(
|
|
620
|
+
'Return raw JSON output instead of human-readable markdown. Useful when an agent wants to programmatically count findings by code or filter by location.',
|
|
621
|
+
),
|
|
622
|
+
},
|
|
623
|
+
async ({ json }) => {
|
|
624
|
+
const result = auditIslands(process.cwd())
|
|
625
|
+
return textResult(formatIslandAudit(result, { json }))
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
630
|
+
// Tool: mcp_overview — discoverability "what tool when" map (T2.5.9)
|
|
631
|
+
//
|
|
632
|
+
// Reads from this package's own manifest at runtime — single source of truth.
|
|
633
|
+
// Reuses the same data that drives api-reference.ts + llms-full.txt + the
|
|
634
|
+
// generated docs/docs/mcp.md sections. Adding a new tool to manifest.ts
|
|
635
|
+
// automatically surfaces it here on next call; no second wiring step.
|
|
636
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
637
|
+
|
|
638
|
+
server.tool('mcp_overview', {}, async () => {
|
|
639
|
+
const { default: manifest } = await import('./manifest')
|
|
640
|
+
const tools = manifest.api.filter((e) => e.signature.startsWith('tool: '))
|
|
641
|
+
|
|
642
|
+
const rows = tools.map((e) => {
|
|
643
|
+
const whenToUse = (e.summary.split(/(?<=[.!?])\s+/)[0] ?? e.summary)
|
|
644
|
+
.trim()
|
|
645
|
+
.replace(/\|/g, '\\|')
|
|
646
|
+
const example = (e.example.split('\n')[0] ?? '').trim().replace(/\|/g, '\\|')
|
|
647
|
+
return `| \`${e.name}\` | ${whenToUse} | \`${example}\` |`
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
return textResult(
|
|
651
|
+
`**MCP Tools (${tools.length}):**\n\n` +
|
|
652
|
+
'| Tool | When to use | Example |\n' +
|
|
653
|
+
'|---|---|---|\n' +
|
|
654
|
+
rows.join('\n'),
|
|
655
|
+
)
|
|
656
|
+
})
|
|
657
|
+
|
|
605
658
|
return server
|
|
606
659
|
}
|
|
607
660
|
|
package/src/manifest.ts
CHANGED
|
@@ -4,17 +4,17 @@ export default defineManifest({
|
|
|
4
4
|
name: '@pyreon/mcp',
|
|
5
5
|
title: 'MCP Server',
|
|
6
6
|
tagline:
|
|
7
|
-
'Model Context Protocol server — live API lookup, validation, migration, anti-pattern catalog, changelog, test-environment audit',
|
|
7
|
+
'Model Context Protocol server — discoverability map, live API lookup, validation, migration, anti-pattern catalog, changelog, test-environment audit',
|
|
8
8
|
description:
|
|
9
|
-
'MCP server (stdio transport) that exposes Pyreon\\\'s structured knowledge to AI coding assistants (Claude Code, Cursor, etc.).
|
|
9
|
+
'MCP server (stdio transport) that exposes Pyreon\\\'s structured knowledge to AI coding assistants (Claude Code, Cursor, etc.). Thirteen tools: `mcp_overview` (start here — markdown table of every tool with "when to use" + example, read straight from this manifest), `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), `audit_test_environment` (mock-vnode test scanner — PR #197 bug class), and `audit_islands` (project-wide islands cross-file audit — duplicate names, dead islands, registry drift, nested islands, never-with-registry).',
|
|
10
10
|
category: 'server',
|
|
11
11
|
features: [
|
|
12
|
-
'
|
|
12
|
+
'Thirteen tools covering discovery, lookup, validation, migration, diagnosis, introspection, audit',
|
|
13
13
|
'stdio transport — drop-in compatible with every MCP client',
|
|
14
14
|
'Project context cached per server instance, auto-invalidates on cwd change',
|
|
15
15
|
'Manifest-driven — `get_api` reads `api-reference.ts`, regenerated from package manifests',
|
|
16
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/',
|
|
17
|
+
'Real-repo audit tools (`audit_test_environment`, `audit_islands`, `get_browser_smoke_status`) walk packages/',
|
|
18
18
|
],
|
|
19
19
|
longExample: `// .mcp/config.json — register the server with any MCP-aware client
|
|
20
20
|
{
|
|
@@ -27,6 +27,8 @@ export default defineManifest({
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Then from the client (Claude Code, Cursor, etc.):
|
|
30
|
+
// mcp_overview()
|
|
31
|
+
// → markdown table: tool | when_to_use | example (start here)
|
|
30
32
|
// get_api({ package: 'flow', symbol: 'createFlow' })
|
|
31
33
|
// → signature, example, common mistakes
|
|
32
34
|
// validate({ code: '<MyButton onClick={handler}>...' })
|
|
@@ -38,8 +40,27 @@ export default defineManifest({
|
|
|
38
40
|
// get_changelog({ package: 'flow', limit: 5 })
|
|
39
41
|
// → recent release notes filtered through ceremonial-bump removal
|
|
40
42
|
// audit_test_environment({ minRisk: 'medium' })
|
|
41
|
-
// → mock-vnode test files ranked HIGH / MEDIUM / LOW
|
|
43
|
+
// → mock-vnode test files ranked HIGH / MEDIUM / LOW
|
|
44
|
+
// audit_islands({})
|
|
45
|
+
// → project-wide islands audit (5 cross-file foot-guns)`,
|
|
42
46
|
api: [
|
|
47
|
+
{
|
|
48
|
+
name: 'mcp_overview',
|
|
49
|
+
kind: 'constant',
|
|
50
|
+
signature: 'tool: mcp_overview() → MarkdownTable',
|
|
51
|
+
summary:
|
|
52
|
+
'Returns a markdown table of every registered MCP tool with a one-sentence "when to use" description and a one-line example. Reads from this same manifest at runtime — single source of truth (the same data feeds `api-reference.ts`, `llms-full.txt`, and `docs/docs/mcp.md`). Intended as the first call for any AI agent connecting to the server: enumerates the surface so the agent can navigate by intent (e.g. "I need release notes" → `get_changelog`) rather than guessing tool names from `tools/list`.',
|
|
53
|
+
example: `mcp_overview()
|
|
54
|
+
// → | Tool | When to use | Example |
|
|
55
|
+
// |------|-------------|---------|
|
|
56
|
+
// | mcp_overview | Returns a markdown table of every registered MCP tool... | mcp_overview() |
|
|
57
|
+
// | get_api | Look up any Pyreon API by package and symbol... | get_api({ package: 'flow', symbol: 'createFlow' }) |
|
|
58
|
+
// | ...`,
|
|
59
|
+
mistakes: [
|
|
60
|
+
'Skipping this tool and calling `tools/list` instead — that returns names + parameter schemas but no "when to use" guidance, so an agent has to call multiple tools to figure out which one fits the task.',
|
|
61
|
+
],
|
|
62
|
+
seeAlso: ['get_api'],
|
|
63
|
+
},
|
|
43
64
|
{
|
|
44
65
|
name: 'get_browser_smoke_status',
|
|
45
66
|
kind: 'constant',
|
|
@@ -169,7 +190,20 @@ get_changelog({ package: '@pyreon/router', since: '0.12.0' })`,
|
|
|
169
190
|
'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
191
|
example: `audit_test_environment({ minRisk: 'medium', limit: 10 })
|
|
171
192
|
// → grouped report with HIGH / MEDIUM / LOW sections`,
|
|
172
|
-
seeAlso: ['get_browser_smoke_status'],
|
|
193
|
+
seeAlso: ['get_browser_smoke_status', 'audit_islands'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'audit_islands',
|
|
197
|
+
kind: 'constant',
|
|
198
|
+
signature: 'tool: audit_islands({ json?: boolean }) → IslandAuditReport',
|
|
199
|
+
summary:
|
|
200
|
+
'Project-wide cross-file islands audit (PR C of the islands DX roadmap). Walks `packages/` + `examples/` and runs five detectors that auto-registry can\\\'t reach (manual `hydrateIslands({...})` for non-Vite consumers / library authors) AND PR G\\\'s per-file `island-never-with-registry-entry` detector misses (it only catches the same-file shape): `duplicate-name`, `never-with-registry-entry`, `registry-mismatch`, `nested-island`, `dead-island`. Each finding ships with file path + line/column + actionable fix suggestion. Companion to the `pyreon doctor --check-islands` CLI flag (same scanner, same five detectors). Run before merging an island PR; CI gate by piping `--json` and grepping `findings.length > 0`.',
|
|
201
|
+
example: `audit_islands({})
|
|
202
|
+
// → markdown-grouped report with one section per finding code
|
|
203
|
+
|
|
204
|
+
audit_islands({ json: true })
|
|
205
|
+
// → machine-readable { root, findings: [...], summary: {...} }`,
|
|
206
|
+
seeAlso: ['audit_test_environment', 'get_anti_patterns'],
|
|
173
207
|
},
|
|
174
208
|
],
|
|
175
209
|
gotchas: [
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
|
|
3
|
+
// MCP server <-> client round-trip for the PR C `audit_islands` tool.
|
|
4
|
+
// Same InMemoryTransport shape as `test-audit-server.test.ts`, so we
|
|
5
|
+
// exercise the registration, JSON-RPC framing, formatter, and zod
|
|
6
|
+
// validation in one pass. Pairs with the unit-level test in the
|
|
7
|
+
// compiler package (`island-audit.test.ts`) — those test the
|
|
8
|
+
// underlying `auditIslands()` function directly; this one proves the
|
|
9
|
+
// MCP wiring.
|
|
10
|
+
|
|
11
|
+
// `audit_islands` walks the real repo (packages/ + examples/) — under CI's
|
|
12
|
+
// parallel test load that can exceed vitest's 5s default. 30s per call is
|
|
13
|
+
// well above empirical worst case (~2s locally) but safe headroom for
|
|
14
|
+
// concurrent runs.
|
|
15
|
+
const AUDIT_TIMEOUT_MS = 30_000
|
|
16
|
+
|
|
17
|
+
describe('MCP server — audit_islands tool', () => {
|
|
18
|
+
it(
|
|
19
|
+
'renders the audit header + summary line',
|
|
20
|
+
async () => {
|
|
21
|
+
const { client, close } = await newClient()
|
|
22
|
+
try {
|
|
23
|
+
const text = await callTool(client, 'audit_islands', {})
|
|
24
|
+
// The header always renders the file/decl/registry counts.
|
|
25
|
+
expect(text).toMatch(/^# Islands audit — \d+ files scanned/)
|
|
26
|
+
expect(text).toMatch(/`island\(\)` declarations?/)
|
|
27
|
+
expect(text).toMatch(/`hydrateIslands` registry entr/)
|
|
28
|
+
} finally {
|
|
29
|
+
await close()
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
AUDIT_TIMEOUT_MS,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
it(
|
|
36
|
+
'returns the green-light message when the audit is clean',
|
|
37
|
+
async () => {
|
|
38
|
+
const { client, close } = await newClient()
|
|
39
|
+
try {
|
|
40
|
+
const text = await callTool(client, 'audit_islands', {})
|
|
41
|
+
// Real-repo baseline: examples/islands-showcase has 6 islands
|
|
42
|
+
// and uses hydrateIslandsAuto() — no manual registry, so audit
|
|
43
|
+
// is clean. The bisect verification (planted typo + never-with-
|
|
44
|
+
// registry) lives in the unit suite. If THIS assertion fails,
|
|
45
|
+
// somebody added a real island defect to main, OR the audit
|
|
46
|
+
// started false-positive-ing.
|
|
47
|
+
expect(text).toContain('No island findings')
|
|
48
|
+
} finally {
|
|
49
|
+
await close()
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
AUDIT_TIMEOUT_MS,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
it(
|
|
56
|
+
'emits machine-readable JSON when json=true',
|
|
57
|
+
async () => {
|
|
58
|
+
const { client, close } = await newClient()
|
|
59
|
+
try {
|
|
60
|
+
const text = await callTool(client, 'audit_islands', { json: true })
|
|
61
|
+
// Should be valid JSON with the expected shape.
|
|
62
|
+
const parsed = JSON.parse(text)
|
|
63
|
+
expect(parsed).toHaveProperty('root')
|
|
64
|
+
expect(parsed).toHaveProperty('findings')
|
|
65
|
+
expect(parsed).toHaveProperty('summary')
|
|
66
|
+
expect(parsed.summary).toHaveProperty('filesScanned')
|
|
67
|
+
expect(parsed.summary).toHaveProperty('islandsDeclared')
|
|
68
|
+
expect(parsed.summary).toHaveProperty('registryEntries')
|
|
69
|
+
expect(parsed.summary).toHaveProperty('findingsByCode')
|
|
70
|
+
// The 5 finding codes always appear in the summary, with 0 counts
|
|
71
|
+
// when no findings — useful for CI gates that map code → exit
|
|
72
|
+
// status.
|
|
73
|
+
const codes = Object.keys(parsed.summary.findingsByCode).sort()
|
|
74
|
+
expect(codes).toEqual([
|
|
75
|
+
'dead-island',
|
|
76
|
+
'duplicate-name',
|
|
77
|
+
'nested-island',
|
|
78
|
+
'never-with-registry-entry',
|
|
79
|
+
'registry-mismatch',
|
|
80
|
+
])
|
|
81
|
+
} finally {
|
|
82
|
+
await close()
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
AUDIT_TIMEOUT_MS,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
it('rejects a non-boolean json arg via zod', async () => {
|
|
89
|
+
const { client, close } = await newClient()
|
|
90
|
+
try {
|
|
91
|
+
const result = (await client.callTool({
|
|
92
|
+
name: 'audit_islands',
|
|
93
|
+
arguments: { json: 'yes' },
|
|
94
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
95
|
+
expect(result.isError).toBe(true)
|
|
96
|
+
} finally {
|
|
97
|
+
await close()
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -1,39 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
3
|
-
import { createServer } from '../index'
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
4
2
|
|
|
5
3
|
// Real MCP server <-> client round-trip for the T2.5.8 get_changelog
|
|
6
4
|
// tool. Same InMemoryTransport shape as the patterns-server test —
|
|
7
5
|
// exercises tool registration, JSON-RPC framing, and the formatter
|
|
8
6
|
// response shape in one pass.
|
|
9
7
|
|
|
10
|
-
async function newClient(): Promise<{ client: Client; close: () => Promise<void> }> {
|
|
11
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
|
12
|
-
const server = createServer()
|
|
13
|
-
await server.connect(serverTransport)
|
|
14
|
-
const client = new Client({ name: 'test', version: '0.0.0' })
|
|
15
|
-
await client.connect(clientTransport)
|
|
16
|
-
return {
|
|
17
|
-
client,
|
|
18
|
-
close: async () => {
|
|
19
|
-
await client.close()
|
|
20
|
-
await server.close()
|
|
21
|
-
},
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function callTool(
|
|
26
|
-
client: Client,
|
|
27
|
-
name: string,
|
|
28
|
-
args: Record<string, unknown>,
|
|
29
|
-
): Promise<string> {
|
|
30
|
-
const result = (await client.callTool({ name, arguments: args })) as {
|
|
31
|
-
content: Array<{ type: string; text: string }>
|
|
32
|
-
}
|
|
33
|
-
expect(result.content[0]!.type).toBe('text')
|
|
34
|
-
return result.content[0]!.text
|
|
35
|
-
}
|
|
36
|
-
|
|
37
8
|
describe('MCP server — get_changelog tool', () => {
|
|
38
9
|
it('lists every package when called with no arg', async () => {
|
|
39
10
|
const { client, close } = await newClient()
|
|
@@ -168,7 +168,7 @@ describe('formatChangelog', () => {
|
|
|
168
168
|
it('respects the limit option', () => {
|
|
169
169
|
const q = findChangelog(registry, 'query')!
|
|
170
170
|
const limited = formatChangelog(q, { limit: 1 })
|
|
171
|
-
const versionHeadings = limited.split('\n').filter((l) =>
|
|
171
|
+
const versionHeadings = limited.split('\n').filter((l) => l.startsWith('## '))
|
|
172
172
|
expect(versionHeadings).toHaveLength(1)
|
|
173
173
|
})
|
|
174
174
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
|
|
3
|
+
// Real MCP server <-> client round-trip for the `diagnose` tool. The
|
|
4
|
+
// `ERROR_PATTERNS` catalog itself is unit-tested in the compiler's
|
|
5
|
+
// `react-intercept.test.ts`; this test pins the JSON-RPC wiring +
|
|
6
|
+
// the formatter that splits cause / fix / fixCode / related into
|
|
7
|
+
// markdown sections.
|
|
8
|
+
|
|
9
|
+
describe('MCP server — diagnose tool', () => {
|
|
10
|
+
it('matches a known pattern and emits cause + fix + code sections', async () => {
|
|
11
|
+
const { client, close } = await newClient()
|
|
12
|
+
try {
|
|
13
|
+
// The signal-not-callable pattern is one of the foundational
|
|
14
|
+
// entries in ERROR_PATTERNS — captures the `(name) is not a
|
|
15
|
+
// function` shape that almost every React refugee hits when they
|
|
16
|
+
// forget signals are callable.
|
|
17
|
+
const text = await callTool(client, 'diagnose', {
|
|
18
|
+
error: 'count is not a function',
|
|
19
|
+
})
|
|
20
|
+
expect(text).toContain('**Cause:**')
|
|
21
|
+
expect(text).toContain('**Fix:**')
|
|
22
|
+
// The fix-code section appears for patterns that produce one.
|
|
23
|
+
expect(text).toContain('**Code:**')
|
|
24
|
+
expect(text).toContain('count')
|
|
25
|
+
} finally {
|
|
26
|
+
await close()
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('includes the Related section when the pattern provides one', async () => {
|
|
31
|
+
const { client, close } = await newClient()
|
|
32
|
+
try {
|
|
33
|
+
// Hydration-mismatch is the canonical pattern that carries a
|
|
34
|
+
// `related` hint (typeof window guard / onMount). Verifies the
|
|
35
|
+
// formatter actually wires the `related` field through.
|
|
36
|
+
const text = await callTool(client, 'diagnose', {
|
|
37
|
+
error: 'Hydration mismatch at /about',
|
|
38
|
+
})
|
|
39
|
+
expect(text).toContain('**Cause:**')
|
|
40
|
+
expect(text).toContain('**Fix:**')
|
|
41
|
+
expect(text).toContain('**Related:**')
|
|
42
|
+
} finally {
|
|
43
|
+
await close()
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns the generic fallback when no pattern matches', async () => {
|
|
48
|
+
const { client, close } = await newClient()
|
|
49
|
+
try {
|
|
50
|
+
// Arbitrary text that doesn't match any ERROR_PATTERNS regex —
|
|
51
|
+
// exercises the fallback branch (no diagnosis → suggestions
|
|
52
|
+
// list).
|
|
53
|
+
const text = await callTool(client, 'diagnose', {
|
|
54
|
+
error: 'something completely unfamiliar that no pattern would match',
|
|
55
|
+
})
|
|
56
|
+
expect(text).toContain('Could not identify a Pyreon-specific pattern')
|
|
57
|
+
expect(text).toContain('pyreon doctor')
|
|
58
|
+
expect(text).toContain('bun run typecheck')
|
|
59
|
+
} finally {
|
|
60
|
+
await close()
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('rejects missing `error` arg via zod', async () => {
|
|
65
|
+
const { client, close } = await newClient()
|
|
66
|
+
try {
|
|
67
|
+
const result = (await client.callTool({
|
|
68
|
+
name: 'diagnose',
|
|
69
|
+
arguments: {},
|
|
70
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
71
|
+
expect(result.isError).toBe(true)
|
|
72
|
+
} finally {
|
|
73
|
+
await close()
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
|
|
3
|
+
// Real MCP server <-> client round-trip for the `get_api` tool. The
|
|
4
|
+
// unit-level coverage of `API_REFERENCE` itself lives in
|
|
5
|
+
// `api-reference.test.ts` (manifest-generation contract + structural
|
|
6
|
+
// shape). This test proves the JSON-RPC wiring: zod validates the
|
|
7
|
+
// `package` + `symbol` args, the handler looks up `API_REFERENCE[key]`
|
|
8
|
+
// against the merged manifest output, and the formatter assembles the
|
|
9
|
+
// markdown response. Pairs with the manifest-driven docs pipeline
|
|
10
|
+
// (T2.5.1) — every entry in `api-reference.ts` flows through this tool.
|
|
11
|
+
|
|
12
|
+
describe('MCP server — get_api tool', () => {
|
|
13
|
+
it('returns signature + usage for a known symbol', async () => {
|
|
14
|
+
const { client, close } = await newClient()
|
|
15
|
+
try {
|
|
16
|
+
// `signal` is one of the foundational reactivity APIs — every
|
|
17
|
+
// manifest in the pipeline must publish it, so this is a stable
|
|
18
|
+
// assertion for the JSON-RPC contract.
|
|
19
|
+
const text = await callTool(client, 'get_api', {
|
|
20
|
+
package: 'reactivity',
|
|
21
|
+
symbol: 'signal',
|
|
22
|
+
})
|
|
23
|
+
expect(text).toContain('## @pyreon/reactivity — signal')
|
|
24
|
+
expect(text).toContain('**Signature:**')
|
|
25
|
+
expect(text).toContain('```typescript')
|
|
26
|
+
expect(text).toContain('**Usage:**')
|
|
27
|
+
} finally {
|
|
28
|
+
await close()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns a not-found message with suggestions for an unknown symbol', async () => {
|
|
33
|
+
const { client, close } = await newClient()
|
|
34
|
+
try {
|
|
35
|
+
// `sig` is intentionally a substring of real entries (`signal`,
|
|
36
|
+
// `Signal`, ...) so the substring-match suggestion path fires —
|
|
37
|
+
// the handler does `allKeys.filter(k => k.includes(symbol))`.
|
|
38
|
+
const text = await callTool(client, 'get_api', {
|
|
39
|
+
package: 'reactivity',
|
|
40
|
+
symbol: 'sig',
|
|
41
|
+
})
|
|
42
|
+
expect(text).toContain("Symbol 'sig' not found")
|
|
43
|
+
expect(text).toContain('Did you mean')
|
|
44
|
+
// The matcher should surface real signal-related entries.
|
|
45
|
+
expect(text).toMatch(/reactivity\/\w*[Ss]ig/)
|
|
46
|
+
} finally {
|
|
47
|
+
await close()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('rejects missing required args via zod', async () => {
|
|
52
|
+
// Both `package` and `symbol` are required; omitting one must
|
|
53
|
+
// produce a JSON-RPC error rather than silently returning an
|
|
54
|
+
// empty response.
|
|
55
|
+
const { client, close } = await newClient()
|
|
56
|
+
try {
|
|
57
|
+
const result = (await client.callTool({
|
|
58
|
+
name: 'get_api',
|
|
59
|
+
arguments: { package: 'reactivity' },
|
|
60
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
61
|
+
expect(result.isError).toBe(true)
|
|
62
|
+
} finally {
|
|
63
|
+
await close()
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
// Real MCP server <-> client round-trip for the `get_browser_smoke_status`
|
|
6
|
+
// tool. The tool walks `<cwd>/packages` looking for browser-categorised
|
|
7
|
+
// packages from `.claude/rules/browser-packages.json` and checks each
|
|
8
|
+
// for `*.browser.test.{ts,tsx}` files. CI script `check-browser-smoke.ts`
|
|
9
|
+
// covers the same shape from a script entrypoint; this test pins the
|
|
10
|
+
// JSON-RPC handler + formatter output.
|
|
11
|
+
//
|
|
12
|
+
// vitest runs from `packages/tools/mcp` by default — walking
|
|
13
|
+
// `packages/tools/mcp/packages` would find nothing, exercising the
|
|
14
|
+
// "unknown package" branch. To exercise the COVERED + MISSING paths
|
|
15
|
+
// against real package directories, we `chdir` to the monorepo root
|
|
16
|
+
// inside one of the specs (then restore). The repo-walk-from-cwd
|
|
17
|
+
// pattern matches how the tool is intended to be used by an MCP
|
|
18
|
+
// client (CLI invocation from project root).
|
|
19
|
+
|
|
20
|
+
const _filename = fileURLToPath(import.meta.url)
|
|
21
|
+
const _dirname = path.dirname(_filename)
|
|
22
|
+
// .../packages/tools/mcp/src/tests → walk up 4 levels to the repo root.
|
|
23
|
+
const REPO_ROOT = path.resolve(_dirname, '..', '..', '..', '..', '..')
|
|
24
|
+
|
|
25
|
+
describe('MCP server — get_browser_smoke_status tool', () => {
|
|
26
|
+
it('emits the coverage header (covered / total)', async () => {
|
|
27
|
+
const { client, close } = await newClient()
|
|
28
|
+
try {
|
|
29
|
+
const text = await callTool(client, 'get_browser_smoke_status', {})
|
|
30
|
+
// Header format: `**Browser smoke coverage** (N / M):`.
|
|
31
|
+
expect(text).toMatch(/^\*\*Browser smoke coverage\*\* \(\d+ \/ \d+\):/)
|
|
32
|
+
} finally {
|
|
33
|
+
await close()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('flags packages as Covered / Missing / Unknown when run from repo root', async () => {
|
|
38
|
+
// chdir to the monorepo root so the walker finds real package
|
|
39
|
+
// directories instead of the default cwd's empty `packages/`.
|
|
40
|
+
// Restore in finally so adjacent tests aren't affected.
|
|
41
|
+
const originalCwd = process.cwd()
|
|
42
|
+
try {
|
|
43
|
+
process.chdir(REPO_ROOT)
|
|
44
|
+
const { client, close } = await newClient()
|
|
45
|
+
try {
|
|
46
|
+
const text = await callTool(client, 'get_browser_smoke_status', {})
|
|
47
|
+
// From the repo root, real browser packages are present and
|
|
48
|
+
// have shipped `*.browser.test.*` files (lint rule enforces
|
|
49
|
+
// this). So `Covered (N):` should appear with N > 0.
|
|
50
|
+
expect(text).toMatch(/✓ Covered \(\d+\):/)
|
|
51
|
+
// At least one canonical browser-package name in the body.
|
|
52
|
+
expect(text).toMatch(/- @pyreon\/(runtime-dom|router|styler)/)
|
|
53
|
+
} finally {
|
|
54
|
+
await close()
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
process.chdir(originalCwd)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('reports "not found in this repo" packages from outside the monorepo', async () => {
|
|
62
|
+
// Default cwd (`packages/tools/mcp`) means `<cwd>/packages` is
|
|
63
|
+
// missing — every browser-package becomes "unknown" (listed in
|
|
64
|
+
// the JSON catalog but absent from this directory). Exercises
|
|
65
|
+
// the unknown-package branch of the formatter.
|
|
66
|
+
const { client, close } = await newClient()
|
|
67
|
+
try {
|
|
68
|
+
const text = await callTool(client, 'get_browser_smoke_status', {})
|
|
69
|
+
expect(text).toContain('Listed in browser-packages.json but not found')
|
|
70
|
+
expect(text).toMatch(/- @pyreon\/runtime-dom/)
|
|
71
|
+
} finally {
|
|
72
|
+
await close()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
|
|
3
|
+
// Real MCP server <-> client round-trip for the `get_components` tool.
|
|
4
|
+
// Pairs with `get-routes-server.test.ts` — both flow through the
|
|
5
|
+
// shared `generateContext` walker but render different fields. Unit
|
|
6
|
+
// coverage of the scanner regex lives in
|
|
7
|
+
// `compiler/src/tests/project-scanner.test.ts`; this proves the
|
|
8
|
+
// JSON-RPC wiring + the formatter that renders each `ComponentInfo`
|
|
9
|
+
// (name, file path, `props: { ... }`, `signals: [ ... ]`).
|
|
10
|
+
|
|
11
|
+
describe('MCP server — get_components tool', () => {
|
|
12
|
+
it('emits the components header with count and at least one entry', async () => {
|
|
13
|
+
const { client, close } = await newClient()
|
|
14
|
+
try {
|
|
15
|
+
const text = await callTool(client, 'get_components', {})
|
|
16
|
+
// Header format `**Components (N):**` for a non-empty repo.
|
|
17
|
+
expect(text).toMatch(/^\*\*Components \(\d+\):\*\*/)
|
|
18
|
+
// At least one component rendered: ` Name — relative/file.ts`.
|
|
19
|
+
expect(text).toMatch(/^ {2}\w+ — \S+/m)
|
|
20
|
+
} finally {
|
|
21
|
+
await close()
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders the optional props + signals details when present', async () => {
|
|
26
|
+
const { client, close } = await newClient()
|
|
27
|
+
try {
|
|
28
|
+
const text = await callTool(client, 'get_components', {})
|
|
29
|
+
// The fixture set carries components with both `props: { ... }`
|
|
30
|
+
// and `signals: [...]` — verifying the formatter wires the
|
|
31
|
+
// detail branches. The unit scanner test covers detection
|
|
32
|
+
// accuracy; this verifies the rendered detail format reaches
|
|
33
|
+
// the consumer over JSON-RPC.
|
|
34
|
+
expect(text).toMatch(/props: \{ [^}]+ \}/)
|
|
35
|
+
expect(text).toMatch(/signals: \[[^\]]+\]/)
|
|
36
|
+
} finally {
|
|
37
|
+
await close()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('rejects unexpected args (no-args contract)', async () => {
|
|
42
|
+
const { client, close } = await newClient()
|
|
43
|
+
try {
|
|
44
|
+
const result = (await client.callTool({
|
|
45
|
+
name: 'get_components',
|
|
46
|
+
arguments: { unexpected: 'value' },
|
|
47
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
48
|
+
const text = result.content?.[0]?.text ?? ''
|
|
49
|
+
// Either zod rejects or the unknown arg is coerced into the
|
|
50
|
+
// no-args path. Both contracts are valid — assert the call
|
|
51
|
+
// didn't crash, returning at minimum the header.
|
|
52
|
+
expect(result.isError === true || text.includes('**Components')).toBe(true)
|
|
53
|
+
} finally {
|
|
54
|
+
await close()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|