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 })