| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- '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);
- });
|