@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.
- 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 +2 -2
- 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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
2
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
3
|
+
import { createServer } from '../index'
|
|
4
|
+
|
|
5
|
+
// MCP server <-> client round-trip for the T2.5.7 audit_test_environment
|
|
6
|
+
// tool. Same InMemoryTransport shape as the other tool tests so we
|
|
7
|
+
// exercise registration, JSON-RPC framing, and the formatter in
|
|
8
|
+
// one pass.
|
|
9
|
+
|
|
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
|
+
describe('MCP server — audit_test_environment tool', () => {
|
|
38
|
+
it('returns a risk-grouped audit with defaults (minRisk=medium)', async () => {
|
|
39
|
+
const { client, close } = await newClient()
|
|
40
|
+
try {
|
|
41
|
+
const text = await callTool(client, 'audit_test_environment', {})
|
|
42
|
+
expect(text).toMatch(/^# Test environment audit — \d+ test files scanned/)
|
|
43
|
+
// Label is markdown-bolded as `**Mock-vnode exposure**:`, so
|
|
44
|
+
// match the substring rather than the literal label.
|
|
45
|
+
expect(text).toMatch(/Mock-vnode exposure.*\d+ \/ \d+/)
|
|
46
|
+
expect(text).toMatch(/Risk counts.*\d+ high/)
|
|
47
|
+
// At default minRisk='medium', either at least one HIGH/MEDIUM
|
|
48
|
+
// group surfaces, OR the "no files at risk level …" sentinel
|
|
49
|
+
// appears (the case after the T1.2 cleanup brought both counts
|
|
50
|
+
// to zero). The HIGH/MEDIUM section path is covered by the
|
|
51
|
+
// synthetic-fixture tests in the compiler package.
|
|
52
|
+
expect(text).toMatch(/^## (HIGH|MEDIUM)|No files at risk level/m)
|
|
53
|
+
} finally {
|
|
54
|
+
await close()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('honours minRisk="high" — hides MEDIUM entries', async () => {
|
|
59
|
+
const { client, close } = await newClient()
|
|
60
|
+
try {
|
|
61
|
+
const text = await callTool(client, 'audit_test_environment', { minRisk: 'high' })
|
|
62
|
+
expect(text).not.toMatch(/^## MEDIUM/m)
|
|
63
|
+
} finally {
|
|
64
|
+
await close()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('honours minRisk="low" — surfaces every risk tier present', async () => {
|
|
69
|
+
const { client, close } = await newClient()
|
|
70
|
+
try {
|
|
71
|
+
const text = await callTool(client, 'audit_test_environment', { minRisk: 'low', limit: 3 })
|
|
72
|
+
// With minRisk=low, every present risk tier surfaces. The repo
|
|
73
|
+
// currently has MEDIUM + LOW (HIGH count is 0 after the T1.2
|
|
74
|
+
// cleanup), so just verify LOW renders. The HIGH section
|
|
75
|
+
// appears when at least one HIGH file exists — covered by the
|
|
76
|
+
// synthetic-fixture tests in the compiler package.
|
|
77
|
+
expect(text).toMatch(/^## LOW/m)
|
|
78
|
+
} finally {
|
|
79
|
+
await close()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('honours limit per risk group', async () => {
|
|
84
|
+
const { client, close } = await newClient()
|
|
85
|
+
try {
|
|
86
|
+
const text = await callTool(client, 'audit_test_environment', { limit: 2 })
|
|
87
|
+
// Any risk group that exceeds 2 entries must show the "showing 2" marker.
|
|
88
|
+
const groups = text.split(/^## /m).slice(1) // drop preamble
|
|
89
|
+
for (const group of groups) {
|
|
90
|
+
const header = group.split('\n')[0]!
|
|
91
|
+
const totalMatch = /—\s+(\d+)\s+files?/.exec(header)
|
|
92
|
+
if (!totalMatch) continue
|
|
93
|
+
const total = Number(totalMatch[1])
|
|
94
|
+
if (total > 2) {
|
|
95
|
+
expect(header).toContain('showing 2')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await close()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rejects an unknown minRisk via zod', async () => {
|
|
104
|
+
const { client, close } = await newClient()
|
|
105
|
+
try {
|
|
106
|
+
const result = (await client.callTool({
|
|
107
|
+
name: 'audit_test_environment',
|
|
108
|
+
arguments: { minRisk: 'bogus' },
|
|
109
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
110
|
+
expect(result.isError).toBe(true)
|
|
111
|
+
} finally {
|
|
112
|
+
await close()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('rejects a negative limit via zod', async () => {
|
|
117
|
+
const { client, close } = await newClient()
|
|
118
|
+
try {
|
|
119
|
+
const result = (await client.callTool({
|
|
120
|
+
name: 'audit_test_environment',
|
|
121
|
+
arguments: { limit: -1 },
|
|
122
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
123
|
+
expect(result.isError).toBe(true)
|
|
124
|
+
} finally {
|
|
125
|
+
await close()
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { detectPyreonPatterns, detectReactPatterns } from '@pyreon/compiler'
|
|
2
|
+
|
|
3
|
+
// The MCP `validate` tool handler lives in index.ts and simply merges
|
|
4
|
+
// the results of both detectors. The handler cannot be exercised in-
|
|
5
|
+
// process without standing up an MCP transport, so this test locks
|
|
6
|
+
// down the merge contract: for a snippet that carries BOTH React
|
|
7
|
+
// patterns (coming-from-React mistakes) AND Pyreon patterns (using-
|
|
8
|
+
// Pyreon-wrong mistakes), both detectors must fire and the union must
|
|
9
|
+
// be what the handler returns. This is the regression test for the
|
|
10
|
+
// T2.5.2 extension that added the Pyreon detector alongside the React
|
|
11
|
+
// detector.
|
|
12
|
+
|
|
13
|
+
describe('MCP validate — merged detector surface', () => {
|
|
14
|
+
it('returns BOTH React and Pyreon diagnostics on a mixed snippet', () => {
|
|
15
|
+
const code = `
|
|
16
|
+
import { useState } from 'react'
|
|
17
|
+
|
|
18
|
+
const Counter = ({ count }: { count: number }) => {
|
|
19
|
+
const [local, setLocal] = useState(count)
|
|
20
|
+
return <For each={items}>{(i) => <li className="x" />}</For>
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
const react = detectReactPatterns(code)
|
|
24
|
+
const pyreon = detectPyreonPatterns(code)
|
|
25
|
+
|
|
26
|
+
const reactCodes = new Set(react.map((d) => d.code))
|
|
27
|
+
const pyreonCodes = new Set(pyreon.map((d) => d.code))
|
|
28
|
+
|
|
29
|
+
// React detector catches: useState, react import, className
|
|
30
|
+
expect(reactCodes.has('use-state')).toBe(true)
|
|
31
|
+
expect(reactCodes.has('react-import')).toBe(true)
|
|
32
|
+
expect(reactCodes.has('class-name-prop')).toBe(true)
|
|
33
|
+
|
|
34
|
+
// Pyreon detector catches: destructured props, missing `by`
|
|
35
|
+
expect(pyreonCodes.has('props-destructured')).toBe(true)
|
|
36
|
+
expect(pyreonCodes.has('for-missing-by')).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('does not double-flag anything between the two detectors', () => {
|
|
40
|
+
// Both detectors know about className — but it belongs to the
|
|
41
|
+
// React detector (coming-from-React mistake). The Pyreon detector
|
|
42
|
+
// must NOT duplicate it.
|
|
43
|
+
const code = `<div className="x" />`
|
|
44
|
+
const reactCodes = new Set(detectReactPatterns(code).map((d) => d.code))
|
|
45
|
+
const pyreonCodes = new Set(detectPyreonPatterns(code).map((d) => d.code))
|
|
46
|
+
expect(reactCodes.has('class-name-prop')).toBe(true)
|
|
47
|
+
// The Pyreon detector does NOT claim className ownership.
|
|
48
|
+
expect(pyreonCodes.has('class-name-prop' as never)).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns zero diagnostics across both detectors for idiomatic Pyreon code', () => {
|
|
52
|
+
const code = `
|
|
53
|
+
import { signal, effect } from '@pyreon/reactivity'
|
|
54
|
+
import { For } from '@pyreon/core'
|
|
55
|
+
|
|
56
|
+
const List = (props: { items: Array<{ id: string; name: string }> }) => {
|
|
57
|
+
const query = signal('')
|
|
58
|
+
effect(() => console.log('query changed', query()))
|
|
59
|
+
return (
|
|
60
|
+
<For each={props.items} by={(i) => i.id}>
|
|
61
|
+
{(i) => <li>{i.name}</li>}
|
|
62
|
+
</For>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
`
|
|
66
|
+
expect(detectReactPatterns(code)).toEqual([])
|
|
67
|
+
expect(detectPyreonPatterns(code)).toEqual([])
|
|
68
|
+
})
|
|
69
|
+
})
|