138 lines
4.8 KiB
JavaScript
138 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable */
|
|
// Generic stdio MCP smoke runner.
|
|
//
|
|
// Usage:
|
|
// node scripts/smoke-mcp.js <server-name> [--call toolName] [--args '{"foo":"bar"}']
|
|
//
|
|
// Server names map to dist/mcp/<name>-server.js. Dummy env vars are injected so
|
|
// the server boots without needing real credentials — we're exercising the
|
|
// protocol surface, not the upstream API.
|
|
|
|
const { spawn } = require('child_process');
|
|
const path = require('path');
|
|
|
|
const SERVER_DUMMY_ENV = {
|
|
coolify: { COOLIFY_API_URL: 'https://smoke.example', COOLIFY_API_TOKEN: 'smoke-token' },
|
|
gitea: { GITEA_API_URL: 'https://smoke.example', GITEA_API_TOKEN: 'smoke-token', GITEA_USERNAME: 'smoke' },
|
|
workspace: {
|
|
WORKSPACE_ROOT: path.resolve(__dirname, '..'),
|
|
GITEA_API_URL: 'https://smoke.example',
|
|
GITEA_API_TOKEN: 'smoke-token',
|
|
GITEA_USERNAME: 'smoke',
|
|
},
|
|
'vibn-platform': {
|
|
SESSION_KEY: 'smoke-session',
|
|
GITEA_API_URL: 'https://smoke.example',
|
|
GITEA_API_TOKEN: 'smoke-token',
|
|
},
|
|
agent: { AGENT_RUNNER_URL: 'http://127.0.0.1:65535' },
|
|
};
|
|
|
|
const DEFAULT_CALL = {
|
|
coolify: { name: 'coolify_list_projects', args: {} },
|
|
gitea: { name: 'list_repos', args: {} },
|
|
workspace: { name: 'list_directory', args: { path: '.' } },
|
|
'vibn-platform': {
|
|
name: 'save_memory',
|
|
args: { key: 'smoke_probe', type: 'note', value: 'hello from smoke test' },
|
|
},
|
|
agent: { name: 'get_job_status', args: { job_id: 'smoke-nonexistent' } },
|
|
};
|
|
|
|
const args = process.argv.slice(2);
|
|
const serverName = args[0];
|
|
if (!serverName) {
|
|
console.error('usage: node scripts/smoke-mcp.js <coolify|gitea|...> [--call name] [--args json]');
|
|
process.exit(2);
|
|
}
|
|
|
|
let callName = DEFAULT_CALL[serverName]?.name;
|
|
let callArgs = DEFAULT_CALL[serverName]?.args || {};
|
|
for (let i = 1; i < args.length; i++) {
|
|
if (args[i] === '--call') callName = args[++i];
|
|
else if (args[i] === '--args') callArgs = JSON.parse(args[++i]);
|
|
}
|
|
|
|
const serverPath = path.resolve(__dirname, '..', 'dist', 'mcp', `${serverName}-server.js`);
|
|
const env = { ...process.env, ...(SERVER_DUMMY_ENV[serverName] || {}) };
|
|
// Only inject dummies for keys that aren't already set by the real environment.
|
|
for (const [k, v] of Object.entries(SERVER_DUMMY_ENV[serverName] || {})) {
|
|
if (!process.env[k]) env[k] = v;
|
|
}
|
|
|
|
const child = spawn(process.execPath, [serverPath], { stdio: ['pipe', 'pipe', 'inherit'], env });
|
|
|
|
let buf = '';
|
|
const pending = new Map();
|
|
let nextId = 1;
|
|
|
|
function send(method, params) {
|
|
const id = nextId++;
|
|
return new Promise((resolve, reject) => {
|
|
pending.set(id, { resolve, reject });
|
|
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
});
|
|
}
|
|
|
|
function notify(method, params) {
|
|
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
|
|
}
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
buf += chunk.toString('utf8');
|
|
let idx;
|
|
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
const line = buf.slice(0, idx).trim();
|
|
buf = buf.slice(idx + 1);
|
|
if (!line) continue;
|
|
try {
|
|
const msg = JSON.parse(line);
|
|
if (msg.id && pending.has(msg.id)) {
|
|
const { resolve, reject } = pending.get(msg.id);
|
|
pending.delete(msg.id);
|
|
if (msg.error) reject(new Error(msg.error.message));
|
|
else resolve(msg.result);
|
|
}
|
|
} catch {
|
|
console.error('[smoke] non-JSON line:', line);
|
|
}
|
|
}
|
|
});
|
|
|
|
child.on('error', (err) => { console.error('[smoke] spawn error:', err); process.exit(2); });
|
|
|
|
(async () => {
|
|
try {
|
|
const init = await send('initialize', {
|
|
protocolVersion: '2024-11-05',
|
|
capabilities: {},
|
|
clientInfo: { name: 'vibn-smoke', version: '0.0.0' },
|
|
});
|
|
console.log('initialize.serverInfo:', JSON.stringify(init.serverInfo));
|
|
|
|
notify('notifications/initialized', {});
|
|
|
|
const tools = await send('tools/list', {});
|
|
console.log(`\ntools/list returned ${tools.tools.length} tools:`);
|
|
for (const t of tools.tools) {
|
|
const required = t.inputSchema?.required ?? [];
|
|
console.log(` - ${t.name}${required.length ? ` (required: ${required.join(', ')})` : ''}`);
|
|
}
|
|
|
|
if (callName) {
|
|
const callResult = await send('tools/call', { name: callName, arguments: callArgs });
|
|
console.log(`\ntools/call ${callName}(${JSON.stringify(callArgs)}):`);
|
|
console.log(JSON.stringify(callResult, null, 2).slice(0, 500));
|
|
}
|
|
|
|
console.log('\n[smoke] ✓ MCP stdio round-trip successful');
|
|
child.kill();
|
|
process.exit(0);
|
|
} catch (err) {
|
|
console.error('[smoke] FAILED:', err.message);
|
|
child.kill();
|
|
process.exit(1);
|
|
}
|
|
})();
|