/ scripts / mcp-conformance-check.ts
mcp-conformance-check.ts
 1  #!/usr/bin/env node
 2  import { loadMcpServers } from '../src/lib/server/storage'
 3  import { runMcpConformanceCheck } from '../src/lib/server/mcp-conformance'
 4  import type { McpServerConfig } from '../src/types'
 5  
 6  function parseBool(value: string | undefined, fallback: boolean): boolean {
 7    if (!value) return fallback
 8    const normalized = value.trim().toLowerCase()
 9    if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
10    if (['0', 'false', 'no', 'off'].includes(normalized)) return false
11    return fallback
12  }
13  
14  function parseIntWithBounds(value: string | undefined, fallback: number, min: number, max: number): number {
15    const parsed = value ? Number.parseInt(value, 10) : Number.NaN
16    if (!Number.isFinite(parsed)) return fallback
17    return Math.max(min, Math.min(max, Math.trunc(parsed)))
18  }
19  
20  async function main() {
21    const required = parseBool(process.env.SWARMCLAW_MCP_CONFORMANCE_REQUIRED, false)
22    const failOnWarning = parseBool(process.env.SWARMCLAW_MCP_CONFORMANCE_FAIL_ON_WARNING, false)
23    const timeoutMs = parseIntWithBounds(process.env.SWARMCLAW_MCP_CONFORMANCE_TIMEOUT_MS, 12_000, 1_000, 120_000)
24    const smokeToolName = process.env.SWARMCLAW_MCP_CONFORMANCE_SMOKE_TOOL || undefined
25  
26    const servers = Object.values(loadMcpServers())
27    if (servers.length === 0) {
28      const message = '[mcp-conformance] No MCP servers configured.'
29      if (required) {
30        console.error(`${message} Set SWARMCLAW_MCP_CONFORMANCE_REQUIRED=0 to allow empty config.`)
31        process.exitCode = 1
32        return
33      }
34      console.log(`${message} Skipping.`)
35      return
36    }
37  
38    let totalErrors = 0
39    let totalWarnings = 0
40  
41    for (const server of servers) {
42      const result = await runMcpConformanceCheck(server as McpServerConfig, {
43        timeoutMs,
44        smokeToolName,
45      })
46      const errors = result.issues.filter((issue) => issue.level === 'error')
47      const warnings = result.issues.filter((issue) => issue.level === 'warning')
48      totalErrors += errors.length
49      totalWarnings += warnings.length
50  
51      console.log(`[mcp-conformance] ${result.serverName} (${result.serverId}) -> ${result.ok ? 'PASS' : 'FAIL'} | tools=${result.toolsCount} | smoke=${result.smokeToolName || 'none'}`)
52      if (result.issues.length > 0) {
53        for (const issue of result.issues) {
54          const scope = issue.toolName ? ` (${issue.toolName})` : ''
55          console.log(`  - ${issue.level.toUpperCase()} [${issue.code}]${scope}: ${issue.message}`)
56        }
57      }
58    }
59  
60    console.log(`[mcp-conformance] Summary: errors=${totalErrors}, warnings=${totalWarnings}`)
61    if (totalErrors > 0 || (failOnWarning && totalWarnings > 0)) {
62      process.exitCode = 1
63    }
64  }
65  
66  main().catch((err) => {
67    console.error('[mcp-conformance] Fatal error:', err instanceof Error ? err.message : String(err))
68    process.exitCode = 1
69  })