mirror of
https://github.com/ruvnet/RuView
synced 2026-08-06 19:51:43 +00:00
feat(ruview): secure community metaharness flywheel (#1467)
* feat(ruview): add secure community metaharness flywheel * fix(ruview): canonicalize manifest line endings
This commit is contained in:
@@ -17,6 +17,8 @@ import { readFileSync } from 'node:fs';
|
||||
import { listTools, runTool } from './tools.js';
|
||||
|
||||
const PROTOCOL_VERSION = '2024-11-05';
|
||||
const MAX_REQUEST_BYTES = 256 * 1024;
|
||||
const MAX_QUEUED_TOOL_CALLS = 20;
|
||||
// Single-source the version from package.json (ADR-263 O6).
|
||||
const PKG = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
||||
const SERVER_INFO = { name: 'ruview', version: PKG.version };
|
||||
@@ -28,7 +30,7 @@ function result(id, res) { send({ jsonrpc: '2.0', id, result: res }); }
|
||||
function error(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }); }
|
||||
function log(...a) { process.stderr.write('[ruview-mcp] ' + a.join(' ') + '\n'); }
|
||||
|
||||
async function handle(msg) {
|
||||
async function handle(msg, context = {}) {
|
||||
const { id, method, params } = msg;
|
||||
switch (method) {
|
||||
case 'initialize':
|
||||
@@ -40,8 +42,10 @@ async function handle(msg) {
|
||||
});
|
||||
case 'notifications/initialized':
|
||||
case 'initialized':
|
||||
case 'notifications/cancelled':
|
||||
return; // notifications — no response
|
||||
case 'notifications/cancelled':
|
||||
if (context.queuedIds?.has(params?.requestId)) context.cancelled?.add(params.requestId);
|
||||
return; // queued requests are cancelled before execution
|
||||
case 'ping':
|
||||
return result(id, {});
|
||||
case 'tools/list':
|
||||
@@ -53,7 +57,8 @@ async function handle(msg) {
|
||||
case 'tools/call': {
|
||||
const name = params?.name;
|
||||
const args = params?.arguments || {};
|
||||
const out = await runTool(name, args);
|
||||
log('audit', JSON.stringify({ event: 'tools/call', id, name }));
|
||||
const out = await runTool(name, args, context);
|
||||
// MCP content envelope: text block with the JSON, isError reflects ok=false.
|
||||
return result(id, {
|
||||
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
||||
@@ -75,19 +80,55 @@ export function startMcpServer() {
|
||||
// answer during a long tool run). `toolChain` also lets stdin-close drain the
|
||||
// in-flight call so its response is flushed instead of dropped by process.exit.
|
||||
let toolChain = Promise.resolve();
|
||||
let queuedToolCalls = 0;
|
||||
const cancelled = new Set();
|
||||
const queuedIds = new Set();
|
||||
|
||||
const dispatch = (msg) => handle(msg).catch((err) => {
|
||||
const grants = String(process.env.RUVIEW_MCP_GRANTS || '').split(',').map((v) => v.trim()).filter(Boolean);
|
||||
const dispatch = (msg) => handle(msg, { source: 'mcp', grants, cancelled, queuedIds }).catch((err) => {
|
||||
if (msg && msg.id !== undefined) error(msg.id, -32603, String(err && err.message || err));
|
||||
log('handler error:', String(err));
|
||||
});
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (Buffer.byteLength(line, 'utf8') > MAX_REQUEST_BYTES) {
|
||||
log('oversized JSON-RPC line dropped');
|
||||
return;
|
||||
}
|
||||
const s = line.trim();
|
||||
if (!s) return;
|
||||
let msg;
|
||||
try { msg = JSON.parse(s); } catch { return log('bad JSON line dropped'); }
|
||||
if (msg && msg.method === 'tools/call') {
|
||||
toolChain = toolChain.then(() => dispatch(msg)); // one tool at a time
|
||||
const validId = typeof msg.id === 'string' || (typeof msg.id === 'number' && Number.isFinite(msg.id));
|
||||
if (!validId) {
|
||||
error(msg?.id ?? null, -32600, 'tools/call requires a finite string or number id');
|
||||
return;
|
||||
}
|
||||
if (queuedIds.has(msg.id)) {
|
||||
error(msg.id, -32600, 'Duplicate in-flight request id');
|
||||
return;
|
||||
}
|
||||
if (queuedToolCalls >= MAX_QUEUED_TOOL_CALLS) {
|
||||
if (msg.id !== undefined) error(msg.id, -32000, 'Tool queue is full');
|
||||
log('tool queue full:', String(msg.id));
|
||||
return;
|
||||
}
|
||||
queuedToolCalls += 1;
|
||||
queuedIds.add(msg.id);
|
||||
toolChain = toolChain.then(async () => {
|
||||
try {
|
||||
if (cancelled.delete(msg.id)) {
|
||||
if (msg.id !== undefined) error(msg.id, -32800, 'Request cancelled');
|
||||
return;
|
||||
}
|
||||
await dispatch(msg);
|
||||
} finally {
|
||||
cancelled.delete(msg.id);
|
||||
queuedIds.delete(msg.id);
|
||||
queuedToolCalls -= 1;
|
||||
}
|
||||
}); // one tool at a time
|
||||
} else {
|
||||
dispatch(msg); // health/list/handshake answer immediately, even mid tool run
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user