@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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
|
|
3
|
+
// Real MCP server <-> client round-trip for the `get_routes` tool. The
|
|
4
|
+
// project-scanner regex itself is unit-tested in the compiler's
|
|
5
|
+
// `project-scanner.test.ts`; this test proves the JSON-RPC wiring +
|
|
6
|
+
// the formatter that turns each `RouteInfo` into a markdown line
|
|
7
|
+
// (`/path (loader, guard, params: ..., name: "...")`).
|
|
8
|
+
//
|
|
9
|
+
// Real-repo fixture: vitest runs from `packages/tools/mcp`. The
|
|
10
|
+
// scanner walks src/ and picks up route literals from the
|
|
11
|
+
// `project-scanner.test.ts` fixture strings AND the api-reference's
|
|
12
|
+
// example code blocks. Both surfaces serve as canonical "the repo has
|
|
13
|
+
// routes" inputs, so assertions target the structural shape of the
|
|
14
|
+
// response (header + at least one route line with flag formatting),
|
|
15
|
+
// not exact path counts.
|
|
16
|
+
|
|
17
|
+
describe('MCP server — get_routes tool', () => {
|
|
18
|
+
it('emits the routes header with a count and at least one route line', async () => {
|
|
19
|
+
const { client, close } = await newClient()
|
|
20
|
+
try {
|
|
21
|
+
const text = await callTool(client, 'get_routes', {})
|
|
22
|
+
// Header format `**Routes (N):**` for a non-empty repo.
|
|
23
|
+
expect(text).toMatch(/^\*\*Routes \(\d+\):\*\*/)
|
|
24
|
+
// At least one route rendered with the leading " /…" indent.
|
|
25
|
+
expect(text).toMatch(/^ {2}\//m)
|
|
26
|
+
} finally {
|
|
27
|
+
await close()
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders route flags inline (loader / guard / params / name)', async () => {
|
|
32
|
+
const { client, close } = await newClient()
|
|
33
|
+
try {
|
|
34
|
+
const text = await callTool(client, 'get_routes', {})
|
|
35
|
+
// The fixture routes carry these flags — verifying the formatter
|
|
36
|
+
// actually wires `hasLoader` / `hasGuard` / `params` / `name`
|
|
37
|
+
// through to the rendered line. Loosely matched because the
|
|
38
|
+
// fixture set may shift; what matters is that the formatter's
|
|
39
|
+
// flag-rendering branches all fire across the result.
|
|
40
|
+
expect(text).toMatch(/\(loader[^)]*\)/)
|
|
41
|
+
expect(text).toMatch(/\(.*guard.*\)/)
|
|
42
|
+
expect(text).toMatch(/params: \w+/)
|
|
43
|
+
} finally {
|
|
44
|
+
await close()
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('rejects unexpected args via zod (no-args contract)', async () => {
|
|
49
|
+
// `get_routes` declares `{}` (no args). Unknown args should be
|
|
50
|
+
// rejected by zod's strict input validation rather than silently
|
|
51
|
+
// ignored.
|
|
52
|
+
const { client, close } = await newClient()
|
|
53
|
+
try {
|
|
54
|
+
const result = (await client.callTool({
|
|
55
|
+
name: 'get_routes',
|
|
56
|
+
arguments: { unexpected: 'value' },
|
|
57
|
+
})) as { isError?: boolean; content: Array<{ type: string; text: string }> }
|
|
58
|
+
// Either rejected via zod (isError: true) OR coerced into the
|
|
59
|
+
// empty-args path (no isError). Both are valid contracts —
|
|
60
|
+
// assert the call DIDN'T crash, returning at minimum the header.
|
|
61
|
+
const text = result.content?.[0]?.text ?? ''
|
|
62
|
+
expect(result.isError === true || text.includes('**Routes')).toBe(true)
|
|
63
|
+
} finally {
|
|
64
|
+
await close()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test helpers for the MCP server <-> client round-trip suites.
|
|
3
|
+
*
|
|
4
|
+
* Every `*-server.test.ts` file in this directory needs the same two
|
|
5
|
+
* primitives: spin up an `InMemoryTransport`-wired client + server pair,
|
|
6
|
+
* and call a tool through the client returning the first text block.
|
|
7
|
+
* Keeping them here means a future change to the JSON-RPC handshake
|
|
8
|
+
* (e.g. server name/version, transport flavor) is a one-file edit.
|
|
9
|
+
*/
|
|
10
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
11
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
12
|
+
import { expect } from 'vitest'
|
|
13
|
+
import { createServer } from '../index'
|
|
14
|
+
|
|
15
|
+
export interface McpTestClient {
|
|
16
|
+
client: Client
|
|
17
|
+
close: () => Promise<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Construct a paired in-memory MCP client + server. The caller is
|
|
22
|
+
* responsible for calling `close()` to dispose both ends.
|
|
23
|
+
*/
|
|
24
|
+
export async function newClient(): Promise<McpTestClient> {
|
|
25
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
|
26
|
+
const server = createServer()
|
|
27
|
+
await server.connect(serverTransport)
|
|
28
|
+
const client = new Client({ name: 'test', version: '0.0.0' })
|
|
29
|
+
await client.connect(clientTransport)
|
|
30
|
+
return {
|
|
31
|
+
client,
|
|
32
|
+
close: async () => {
|
|
33
|
+
await client.close()
|
|
34
|
+
await server.close()
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ToolCallResult {
|
|
40
|
+
content: Array<{ type: string; text: string }>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Invoke an MCP tool and return the first text content block's payload.
|
|
45
|
+
* Asserts the block is `type: 'text'` so tool handlers that switch to a
|
|
46
|
+
* different content shape fail loudly at the assertion, not silently in
|
|
47
|
+
* the returned string.
|
|
48
|
+
*/
|
|
49
|
+
export async function callTool(
|
|
50
|
+
client: Client,
|
|
51
|
+
name: string,
|
|
52
|
+
args: Record<string, unknown>,
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
const result = (await client.callTool({ name, arguments: args })) as ToolCallResult
|
|
55
|
+
expect(result.content[0]!.type).toBe('text')
|
|
56
|
+
return result.content[0]!.text
|
|
57
|
+
}
|
|
@@ -20,6 +20,7 @@ describe('gen-docs — mcp snapshot', () => {
|
|
|
20
20
|
it('renders MCP api-reference entries for every api[] item', () => {
|
|
21
21
|
const record = renderApiReferenceEntries(manifest)
|
|
22
22
|
expect(Object.keys(record).sort()).toEqual([
|
|
23
|
+
'mcp/audit_islands',
|
|
23
24
|
'mcp/audit_test_environment',
|
|
24
25
|
'mcp/diagnose',
|
|
25
26
|
'mcp/get_anti_patterns',
|
|
@@ -29,6 +30,7 @@ describe('gen-docs — mcp snapshot', () => {
|
|
|
29
30
|
'mcp/get_components',
|
|
30
31
|
'mcp/get_pattern',
|
|
31
32
|
'mcp/get_routes',
|
|
33
|
+
'mcp/mcp_overview',
|
|
32
34
|
'mcp/migrate_react',
|
|
33
35
|
'mcp/validate',
|
|
34
36
|
])
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
2
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
3
|
+
import { createServer } from '../index'
|
|
4
|
+
import manifest from '../manifest'
|
|
5
|
+
|
|
6
|
+
// Real MCP server <-> client round-trip via InMemoryTransport, mirroring
|
|
7
|
+
// `server-integration.test.ts`. Locks the discoverability contract end-to-end:
|
|
8
|
+
// `tools/list` exposes `mcp_overview`, calling it returns a row per
|
|
9
|
+
// registered tool with summary-derived "when to use" + example-first-line.
|
|
10
|
+
|
|
11
|
+
async function newConnectedClient(): Promise<{ client: Client; close: () => Promise<void> }> {
|
|
12
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
|
13
|
+
|
|
14
|
+
const server = createServer()
|
|
15
|
+
await server.connect(serverTransport)
|
|
16
|
+
|
|
17
|
+
const client = new Client({ name: 'test', version: '0.0.0' })
|
|
18
|
+
await client.connect(clientTransport)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
client,
|
|
22
|
+
close: async () => {
|
|
23
|
+
await client.close()
|
|
24
|
+
await server.close()
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('MCP server — mcp_overview tool over real JSON-RPC transport', () => {
|
|
30
|
+
it('lists mcp_overview alongside every other registered tool', async () => {
|
|
31
|
+
const { client, close } = await newConnectedClient()
|
|
32
|
+
try {
|
|
33
|
+
const list = await client.listTools()
|
|
34
|
+
const names = list.tools.map((t) => t.name).sort()
|
|
35
|
+
expect(names).toContain('mcp_overview')
|
|
36
|
+
// The manifest is the source of truth for the tool catalog. Every tool
|
|
37
|
+
// entry there ('tool: ' signature prefix) must be reachable via tools/list.
|
|
38
|
+
const expected = manifest.api
|
|
39
|
+
.filter((e) => e.signature.startsWith('tool: '))
|
|
40
|
+
.map((e) => e.name)
|
|
41
|
+
for (const tool of expected) {
|
|
42
|
+
expect(names).toContain(tool)
|
|
43
|
+
}
|
|
44
|
+
} finally {
|
|
45
|
+
await close()
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns a markdown table with one row per registered tool', async () => {
|
|
50
|
+
const { client, close } = await newConnectedClient()
|
|
51
|
+
try {
|
|
52
|
+
const result = (await client.callTool({
|
|
53
|
+
name: 'mcp_overview',
|
|
54
|
+
arguments: {},
|
|
55
|
+
})) as { content: Array<{ type: string; text: string }> }
|
|
56
|
+
|
|
57
|
+
expect(result.content).toHaveLength(1)
|
|
58
|
+
expect(result.content[0]!.type).toBe('text')
|
|
59
|
+
const text = result.content[0]!.text
|
|
60
|
+
|
|
61
|
+
// Header line carries the count.
|
|
62
|
+
const expectedTools = manifest.api
|
|
63
|
+
.filter((e) => e.signature.startsWith('tool: '))
|
|
64
|
+
.map((e) => e.name)
|
|
65
|
+
expect(text).toMatch(new RegExp(`MCP Tools \\(${expectedTools.length}\\):`))
|
|
66
|
+
|
|
67
|
+
// Markdown table delimiter + header row.
|
|
68
|
+
expect(text).toContain('| Tool | When to use | Example |')
|
|
69
|
+
expect(text).toContain('|---|---|---|')
|
|
70
|
+
|
|
71
|
+
// Every manifest tool name appears as a backticked cell.
|
|
72
|
+
for (const tool of expectedTools) {
|
|
73
|
+
expect(text).toContain('`' + tool + '`')
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
await close()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('derives `when to use` from the manifest summary first sentence', async () => {
|
|
81
|
+
const { client, close } = await newConnectedClient()
|
|
82
|
+
try {
|
|
83
|
+
const result = (await client.callTool({
|
|
84
|
+
name: 'mcp_overview',
|
|
85
|
+
arguments: {},
|
|
86
|
+
})) as { content: Array<{ type: string; text: string }> }
|
|
87
|
+
const text = result.content[0]!.text
|
|
88
|
+
|
|
89
|
+
// Split into the data rows (skip header + delimiter).
|
|
90
|
+
const lines = text.split('\n').filter((l) => l.startsWith('| `'))
|
|
91
|
+
expect(lines.length).toBeGreaterThanOrEqual(13) // current count, grows over time
|
|
92
|
+
|
|
93
|
+
// Every row has exactly 3 pipe-delimited columns (4 pipes including the
|
|
94
|
+
// outer two). Empty cells are still cells — table shape must be uniform.
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const columnCount = (line.match(/\|/g) ?? []).length
|
|
97
|
+
expect(columnCount).toBe(4)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// mcp_overview row's "when to use" is the first sentence of its own
|
|
101
|
+
// manifest summary — locks in the derivation contract.
|
|
102
|
+
const ownEntry = manifest.api.find((e) => e.name === 'mcp_overview')!
|
|
103
|
+
const firstSentence = ownEntry.summary.split(/(?<=[.!?])\s+/)[0]!.trim()
|
|
104
|
+
const ownRow = lines.find((l) => l.startsWith('| `mcp_overview` |'))!
|
|
105
|
+
expect(ownRow).toContain(firstSentence)
|
|
106
|
+
} finally {
|
|
107
|
+
await close()
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { callTool, newClient } from './helpers'
|
|
2
|
+
|
|
3
|
+
// Real MCP server <-> client round-trip for the `migrate_react` tool.
|
|
4
|
+
// Unit-level coverage of the migration engine itself lives in the
|
|
5
|
+
// compiler package (`migrate.test.ts`); this test pins the JSON-RPC
|
|
6
|
+
// wiring: zod accepts the `code` arg, the handler calls
|
|
7
|
+
// `migrateReactCode`, and the formatter assembles the
|
|
8
|
+
// transformed-code-plus-changelog response.
|
|
9
|
+
|
|
10
|
+
describe('MCP server — migrate_react tool', () => {
|
|
11
|
+
it('rewrites className → class and reports the change', async () => {
|
|
12
|
+
const { client, close } = await newClient()
|
|
13
|
+
try {
|
|
14
|
+
// `className` is the canonical auto-fixable React-ism — the
|
|
15
|
+
// migrator drops the n and reports it in the changes list.
|
|
16
|
+
const text = await callTool(client, 'migrate_react', {
|
|
17
|
+
code: 'const X = () => <div className="x" />',
|
|
18
|
+
})
|
|
19
|
+
expect(text).toContain('## Migrated Code')
|
|
20
|
+
// The migrated code body uses `class=`, not `className=`. We
|
|
21
|
+
// can't assert text.not.toContain('className') because the
|
|
22
|
+
// change description echoes the original name ("className → class").
|
|
23
|
+
expect(text).toMatch(/<div class="x"\s*\/>/)
|
|
24
|
+
expect(text).toContain('**Changes applied')
|
|
25
|
+
// Each change is rendered as a `- Line N: ...` bullet.
|
|
26
|
+
expect(text).toMatch(/- Line \d+: className/)
|
|
27
|
+
} finally {
|
|
28
|
+
await close()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('reports "No changes needed" for already-Pyreon code', async () => {
|
|
33
|
+
const { client, close } = await newClient()
|
|
34
|
+
try {
|
|
35
|
+
// Idiomatic Pyreon snippet — the migrator should find nothing
|
|
36
|
+
// to auto-fix, and the formatter must still emit the section
|
|
37
|
+
// headers + the no-changes message.
|
|
38
|
+
const text = await callTool(client, 'migrate_react', {
|
|
39
|
+
code: `
|
|
40
|
+
import { signal } from '@pyreon/reactivity'
|
|
41
|
+
const X = () => <div class="x" />
|
|
42
|
+
`,
|
|
43
|
+
})
|
|
44
|
+
expect(text).toContain('## Migrated Code')
|
|
45
|
+
expect(text).toContain('**Changes applied (0):**')
|
|
46
|
+
expect(text).toContain('No changes needed.')
|
|
47
|
+
} finally {
|
|
48
|
+
await close()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('surfaces non-fixable diagnostics under "Remaining issues"', async () => {
|
|
53
|
+
const { client, close } = await newClient()
|
|
54
|
+
try {
|
|
55
|
+
// `useReducer` is detected but NOT auto-fixable — the migrator
|
|
56
|
+
// can't synthesize the reducer's `signal().update()` shape from
|
|
57
|
+
// arbitrary user reducer logic. The handler must surface it
|
|
58
|
+
// under "Remaining issues (manual fix needed)" rather than
|
|
59
|
+
// silently dropping it. (Other unfixable codes: `dot-value-signal`,
|
|
60
|
+
// `array-map-jsx`.)
|
|
61
|
+
const text = await callTool(client, 'migrate_react', {
|
|
62
|
+
code: `
|
|
63
|
+
import { useReducer } from 'react'
|
|
64
|
+
const X = () => {
|
|
65
|
+
const [s, dispatch] = useReducer(reducer, 0)
|
|
66
|
+
return <div>{s}</div>
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
})
|
|
70
|
+
expect(text).toContain('## Migrated Code')
|
|
71
|
+
expect(text).toContain('**Remaining issues (manual fix needed):**')
|
|
72
|
+
expect(text).toMatch(/Line \d+: useReducer/)
|
|
73
|
+
} finally {
|
|
74
|
+
await close()
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -40,6 +40,10 @@ const KNOWN_DETECTOR_CODES = new Set([
|
|
|
40
40
|
'raw-remove-event-listener',
|
|
41
41
|
'date-math-random-id',
|
|
42
42
|
'on-click-undefined',
|
|
43
|
+
'signal-write-as-call',
|
|
44
|
+
'static-return-null-conditional',
|
|
45
|
+
'as-unknown-as-vnodechild',
|
|
46
|
+
'island-never-with-registry-entry',
|
|
43
47
|
])
|
|
44
48
|
|
|
45
49
|
describe('patterns content — structural shape', () => {
|
|
@@ -49,7 +53,7 @@ describe('patterns content — structural shape', () => {
|
|
|
49
53
|
'%s has exactly one top-level # heading',
|
|
50
54
|
(_name, path) => {
|
|
51
55
|
const body = readFileSync(path, 'utf8')
|
|
52
|
-
const h1Count = body.split('\n').filter((l) =>
|
|
56
|
+
const h1Count = body.split('\n').filter((l) => l.startsWith('# ')).length
|
|
53
57
|
expect(h1Count).toBe(1)
|
|
54
58
|
},
|
|
55
59
|
)
|
|
@@ -1,6 +1,4 @@
|
|
|
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.3 (`get_pattern`)
|
|
6
4
|
// and T2.5.4 (`get_anti_patterns`) tools. Same setup as the validate
|
|
@@ -8,33 +6,6 @@ import { createServer } from '../index'
|
|
|
8
6
|
// so we exercise tool registration, JSON-RPC framing, and the formatter
|
|
9
7
|
// response shape in one pass.
|
|
10
8
|
|
|
11
|
-
async function newClient(): Promise<{ client: Client; close: () => Promise<void> }> {
|
|
12
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
|
13
|
-
const server = createServer()
|
|
14
|
-
await server.connect(serverTransport)
|
|
15
|
-
const client = new Client({ name: 'test', version: '0.0.0' })
|
|
16
|
-
await client.connect(clientTransport)
|
|
17
|
-
return {
|
|
18
|
-
client,
|
|
19
|
-
close: async () => {
|
|
20
|
-
await client.close()
|
|
21
|
-
await server.close()
|
|
22
|
-
},
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function callTool(
|
|
27
|
-
client: Client,
|
|
28
|
-
name: string,
|
|
29
|
-
args: Record<string, unknown>,
|
|
30
|
-
): Promise<string> {
|
|
31
|
-
const result = (await client.callTool({ name, arguments: args })) as {
|
|
32
|
-
content: Array<{ type: string; text: string }>
|
|
33
|
-
}
|
|
34
|
-
expect(result.content[0]!.type).toBe('text')
|
|
35
|
-
return result.content[0]!.text
|
|
36
|
-
}
|
|
37
|
-
|
|
38
9
|
describe('MCP server — get_pattern tool', () => {
|
|
39
10
|
it('lists available patterns when called with no arg', async () => {
|
|
40
11
|
const { client, close } = await newClient()
|
|
@@ -22,7 +22,7 @@ describe('loadPatternRegistry — real repo docs/patterns', () => {
|
|
|
22
22
|
expect(registry.root).toContain('docs/patterns')
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
it('loads all
|
|
25
|
+
it('loads all 15 seeded patterns', () => {
|
|
26
26
|
const names = registry.patterns.map((p) => p.name).sort()
|
|
27
27
|
expect(names).toEqual([
|
|
28
28
|
'controllable-state',
|
|
@@ -32,6 +32,7 @@ describe('loadPatternRegistry — real repo docs/patterns', () => {
|
|
|
32
32
|
'event-listeners',
|
|
33
33
|
'form-fields',
|
|
34
34
|
'imperative-toasts',
|
|
35
|
+
'islands',
|
|
35
36
|
'keyed-lists',
|
|
36
37
|
'reactive-context',
|
|
37
38
|
'routing-setup',
|
|
@@ -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
|
// MCP server <-> client round-trip for the T2.5.7 audit_test_environment
|
|
6
4
|
// tool. Same InMemoryTransport shape as the other tool tests so we
|
|
7
5
|
// exercise registration, JSON-RPC framing, and the formatter in
|
|
8
6
|
// 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 — audit_test_environment tool', () => {
|
|
38
9
|
it('returns a risk-grouped audit with defaults (minRisk=medium)', async () => {
|
|
39
10
|
const { client, close } = await newClient()
|