'use strict'; // Validate the portable MCP entry shipped with a training package. The helper // speaks newline-delimited JSON-RPC over stdio and only reports protocol metadata/counts. const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); } function readMessage(stream, timeoutMs = 10000) { return new Promise((resolve, reject) => { let buffer = ''; let timer = setTimeout(() => finish(new Error('MCP response timeout')), timeoutMs); const cleanup = () => { clearTimeout(timer); stream.off('data', onData); stream.off('error', onError); stream.off('end', onEnd); }; const finish = (error, value) => { cleanup(); if (error) reject(error); else resolve(value); }; const onError = error => finish(error); const onEnd = () => finish(new Error('MCP process closed before response')); const onData = chunk => { buffer += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk); const newline = buffer.indexOf('\n'); if (newline < 0) return; const line = buffer.slice(0, newline).replace(/\r$/, ''); buffer = buffer.slice(newline + 1); if (!line.trim()) return; try { finish(null, JSON.parse(line)); } catch (error) { finish(new Error(`Invalid MCP JSON: ${error.message}`)); } }; stream.on('data', onData); stream.on('error', onError); stream.on('end', onEnd); }); } function sendMessage(stream, message) { stream.write(`${JSON.stringify(message)}\n`); } async function verify(packageDir) { const root = path.resolve(packageDir); const onboardingAssets = ['driver.min.js', 'driver.css', 'onboarding-guide.js', 'onboarding.css']; for (const asset of onboardingAssets) { const assetPath = path.join(root, 'web', asset); if (!fs.existsSync(assetPath) || fs.statSync(assetPath).size === 0) { throw new Error(`缺少新手引导前端资源:web/${asset}`); } } const configPath = path.join(root, '.mcp.json'); if (!fs.existsSync(configPath)) throw new Error('缺少 .mcp.json'); const config = readJson(configPath); const entry = config?.mcpServers?.['qiwei-assistant']; if (!entry) throw new Error('.mcp.json 未注册 qiwei-assistant'); if (!Array.isArray(entry.args) || !entry.args.includes('mcp')) { throw new Error('qiwei-assistant 未配置 mcp 子命令'); } const command = String(entry.command || '').trim(); const executable = path.resolve(root, command.replace(/^\.([\\/])/, '')); if (!fs.existsSync(executable)) throw new Error(`MCP 可执行文件不存在:${path.basename(executable)}`); const child = spawn(executable, ['mcp'], { cwd: root, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, QIWEI_PACKAGE_ROOT: root, QIWEI_WORKSPACE_ROOT: root, QIWEI_OUTPUTS_DIR: path.join(root, 'outputs'), CLAUDE_CODE_WORKDIR: root, QIWEI_RUNTIME_CONFIG: path.join(root, 'qiwei.runtime.config.mjs'), }, }); let stderr = ''; child.stderr.setEncoding('utf8'); child.stderr.on('data', chunk => { stderr = `${stderr}${chunk}`.slice(-4000); }); try { sendMessage(child.stdin, { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'training-package-verifier', version: '1.0.0' }, }, }); const initialized = await readMessage(child.stdout); if (initialized?.error) throw new Error(`MCP initialize failed: ${initialized.error.message || 'unknown error'}`); sendMessage(child.stdin, { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }); sendMessage(child.stdin, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }); const listed = await readMessage(child.stdout); if (listed?.error) throw new Error(`MCP tools/list failed: ${listed.error.message || 'unknown error'}`); const tools = Array.isArray(listed?.result?.tools) ? listed.result.tools : []; if (!tools.length) throw new Error('MCP 工具列表为空'); return { status: 'ok', server: initialized?.result?.serverInfo?.name || '', toolCount: tools.length, qiweiTools: tools.filter(tool => String(tool?.name || '').startsWith('qiwei_')).length, onboardingAssets, }; } catch (error) { const detail = stderr.replace(/(?:r:|sk-)[A-Za-z0-9._:-]+/g, '[redacted]').trim(); throw new Error(`${error.message}${detail ? ` (${detail})` : ''}`); } finally { child.kill(); if (!child.killed) child.kill('SIGTERM'); } } async function main() { const packageDir = process.argv[2]; if (!packageDir) throw new Error('用法:node scripts/verify-training-package-mcp.js <培训包目录>'); process.stdout.write(`${JSON.stringify(await verify(packageDir), null, 2)}\n`); } main().catch(error => { process.stderr.write(`${error.message}\n`); process.exit(1); });