#!/usr/bin/env node /** * Network-AI MCP Server — Phase 6 Part 3 * * Starts an HTTP/SSE server that exposes the full Network-AI tool suite to * any MCP-compatible AI agent (Claude Desktop, Cursor, Cline, etc.). * * Usage: * npx ts-node bin/mcp-server.ts [options] * npx network-ai-server [options] (after clawhub publish) * * Options: * --port TCP port to listen on (default: 3001) * --host Hostname to bind to (default: 0.0.0.0) * --board Named blackboard to expose (default: main) * --ceiling Initial FederatedBudget token ceiling (default: 1_000_000) * --no-budget Disable budget tools * --no-token Disable token tools * --no-extended Disable all extended tools (budget + token + audit) * --no-control Disable control-plane tools * --audit-log Path to audit log file (default: ./data/audit_log.jsonl) * --heartbeat SSE heartbeat interval in ms (default: 15000) * --help Print this help text * * Connect any MCP client to: * http://localhost:3001 (SSE stream — GET /sse) * http://localhost:3001/mcp (JSON-RPC POST) * http://localhost:3001/health (health check) * http://localhost:3001/tools (list all tools) * * @module bin/mcp-server * @version 4.0.13 */ import { createSwarmOrchestrator, FederatedBudget, McpBlackboardBridge, getConfig, setConfig, } from '../index'; import { SecureTokenManager } from '../security'; import { McpSseServer, McpCombinedBridge, McpBlackboardBridgeAdapter, } from '../lib/mcp-transport-sse'; import type { McpJsonRpcRequest } from '../lib/mcp-bridge'; import { ExtendedMcpTools } from '../lib/mcp-tools-extended'; import { ControlMcpTools } from '../lib/mcp-tools-control'; import { ContextMcpTools } from '../lib/mcp-tools-context'; // ============================================================================ // ARGUMENT PARSING // ============================================================================ interface ServerArgs { port: number; host: string; board: string; ceiling: number; noBudget: boolean; noToken: boolean; noExtended: boolean; noControl: boolean; noAudit: boolean; auditLog: string; heartbeat: number; stdio: boolean; secret: string; help: boolean; } function parseArgs(argv: string[]): ServerArgs { const args: ServerArgs = { port: 3001, host: '127.0.0.1', board: 'main', ceiling: 1_000_000, noBudget: false, noToken: false, noExtended: false, noControl: false, noAudit: false, auditLog: './data/audit_log.jsonl', heartbeat: 15000, stdio: false, // Load from env var first; CLI flag overrides secret: process.env['NETWORK_AI_MCP_SECRET'] ?? '', help: false, }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; const next = argv[i + 1]; switch (arg) { case '--port': args.port = parseInt(next ?? '3001', 10); i++; break; case '--host': args.host = next ?? '127.0.0.1'; i++; break; case '--board': args.board = next ?? 'main'; i++; break; case '--ceiling': args.ceiling = parseFloat(next ?? '1000000'); i++; break; case '--audit-log': args.auditLog = next ?? './data/audit_log.jsonl'; i++; break; case '--heartbeat': args.heartbeat = parseInt(next ?? '15000', 10); i++; break; case '--secret': args.secret = next ?? ''; i++; break; case '--no-budget': args.noBudget = true; break; case '--no-token': args.noToken = true; break; case '--no-extended': args.noExtended = true; break; case '--no-control': args.noControl = true; break; case '--no-audit': args.noAudit = true; break; case '--stdio': args.stdio = true; break; case '--help': case '-h': args.help = true; break; } } return args; } function printHelp(): void { console.log(` network-ai-server — Network-AI MCP Server v4.0.13 Usage: npx ts-node bin/mcp-server.ts [options] Options: --stdio Run in stdio mode (JSON-RPC over stdin/stdout) --port TCP port (default: 3001) --host Bind host (default: 127.0.0.1) --secret Bearer token required on all HTTP requests (env: NETWORK_AI_MCP_SECRET) --board Named blackboard to expose (default: main) --ceiling Budget token ceiling (default: 1000000) --audit-log Audit log path (default: ./data/audit_log.jsonl) --no-audit Disable audit logging entirely --heartbeat SSE heartbeat interval (default: 15000) --no-budget Disable budget tools --no-token Disable token tools --no-extended Disable extended tools (budget + token + audit) --no-control Disable control-plane tools --help Show this help Stdio mode (for MCP clients like Claude Desktop, Cursor, Glama): npx network-ai-server --stdio SSE/HTTP mode (for network clients): GET http://localhost:3001/sse SSE stream POST http://localhost:3001/mcp JSON-RPC 2.0 tool calls GET http://localhost:3001/health Health check GET http://localhost:3001/tools All available tools `); } // ============================================================================ // STDIO TRANSPORT // ============================================================================ /** * Run the MCP server in stdio mode: read newline-delimited JSON-RPC from * stdin, dispatch through `McpCombinedBridge.handleRPC()`, write responses * to stdout. This is the transport that Glama, Claude Desktop, Cursor, and * other MCP-compatible inspectors/clients use by default. */ async function runStdio(combined: McpCombinedBridge): Promise { const { createInterface } = await import('readline'); const { StdioElicitationChannel } = await import('../lib/mcp-elicitation'); // Server→client request channel (MCP elicitation). Client responses to // server-initiated requests are routed here instead of being dispatched // (incorrectly) as client requests. const elicitation = new StdioElicitationChannel((line: string) => { process.stdout.write(line + '\n'); }); const rl = createInterface({ input: process.stdin, terminal: false }); rl.on('line', async (line: string) => { const trimmed = line.trim(); if (!trimmed) return; let request: McpJsonRpcRequest; try { request = JSON.parse(trimmed); } catch (parseErr) { const detail = parseErr instanceof Error ? parseErr.message : String(parseErr); const err = { jsonrpc: '2.0' as const, id: null, error: { code: -32700, message: `Parse error: ${detail}` }, }; process.stdout.write(JSON.stringify(err) + '\n'); return; } // Response to a server-initiated request (elicitation)? Consume it. if (elicitation.handleMessage(request)) return; const response = await combined.handleRPC(request); process.stdout.write(JSON.stringify(response) + '\n'); }); rl.on('close', () => { elicitation.rejectAll('stdin closed'); process.exit(0); }); } // ============================================================================ // MAIN // ============================================================================ async function main(): Promise { const args = parseArgs(process.argv.slice(2)); if (args.help) { printHelp(); process.exit(0); } // In stdio mode, redirect console.log to stderr immediately so stdout // is reserved exclusively for JSON-RPC protocol messages. if (args.stdio) { console.log = (...a: unknown[]) => process.stderr.write(a.join(' ') + '\n'); } console.log(`\n[network-ai-server] Starting MCP Server v4.0.13`); console.log(`[network-ai-server] Board: ${args.board} | Mode: ${args.stdio ? 'stdio' : `SSE (port ${args.port})`}`); // -------------------------------------------------------------------------- // 1. Create orchestrator + blackboard // -------------------------------------------------------------------------- const orchestrator = createSwarmOrchestrator(); const blackboard = orchestrator.getBlackboard(args.board); // -------------------------------------------------------------------------- // 2. Create MCP bridge for blackboard tools (5 tools) // -------------------------------------------------------------------------- const blackboardBridge = new McpBlackboardBridge(blackboard, { name: args.board }); const blackboardAdapter = new McpBlackboardBridgeAdapter(blackboardBridge); // -------------------------------------------------------------------------- // 3. Create extended tools (budget + token + audit) — optional // -------------------------------------------------------------------------- let extendedTools: ExtendedMcpTools | null = null; if (!args.noExtended) { const budget = !args.noBudget ? new FederatedBudget({ ceiling: args.ceiling }) : undefined; const tokenManager = !args.noToken ? new SecureTokenManager() : undefined; extendedTools = new ExtendedMcpTools({ budget, tokenManager, auditLogPath: args.noAudit ? undefined : args.auditLog, }); const toolsEnabled: string[] = []; if (budget) toolsEnabled.push('budget (5 tools)'); if (tokenManager) toolsEnabled.push('token (3 tools)'); toolsEnabled.push('audit (2 tools)'); console.log(`[network-ai-server] Extended tools: ${toolsEnabled.join(', ')}`); } // -------------------------------------------------------------------------- // 4. Create control-plane tools — optional // -------------------------------------------------------------------------- let controlTools: ControlMcpTools | null = null; if (!args.noControl) { // Get the live CONFIG reference via the exported accessor const liveConfig = getConfig() as Record; // Create a proxy config object that both reads from and writes to CONFIG const configProxy = new Proxy(liveConfig, { get(_t, key: string) { return getConfig(key); }, set(_t, key: string, value: unknown) { setConfig(key, value); return true; }, ownKeys() { return Object.keys(getConfig() as object); }, getOwnPropertyDescriptor(_t, key: string) { return { value: getConfig(key), writable: true, enumerable: true, configurable: true }; }, }) as unknown as import('../lib/mcp-tools-control').IConfig; controlTools = new ControlMcpTools({ config: configProxy, blackboard: blackboard as unknown as import('../lib/mcp-tools-control').IControlBlackboard, systemToken: 'system-orchestrator-token', }); console.log(`[network-ai-server] Control tools: config (2), agent (3), fsm (1), info (1)`); } // -------------------------------------------------------------------------- // 5. Assemble combined bridge // -------------------------------------------------------------------------- const combined = new McpCombinedBridge('network-ai'); combined.register(blackboardAdapter); if (extendedTools) combined.register(extendedTools); if (controlTools) combined.register(controlTools); // Context tools: token-budgeted context packs + ranked blackboard search // (lexical ranking out of the box; embed a SemanticMemory for semantic mode) const contextTools = new ContextMcpTools({ blackboard: blackboard as unknown as import('../lib/mcp-blackboard-tools').IBlackboard, }); combined.register(contextTools); console.log(`[network-ai-server] Context tools: context_pack, blackboard_search (lexical mode)`); const totalTools = combined.allDefinitions().length; console.log(`[network-ai-server] Total tools exposed: ${totalTools}`); // -------------------------------------------------------------------------- // 6. Start server (stdio or SSE) // -------------------------------------------------------------------------- if (args.stdio) { console.log(`[network-ai-server] Stdio mode active — ${totalTools} tools ready`); await runStdio(combined); return; } // SSE/HTTP mode if (!args.secret) { process.stderr.write( '[network-ai-server] ERROR: --secret or NETWORK_AI_MCP_SECRET must be set for SSE mode.\n' + ' Without a secret every request is accepted, including cross-origin browser requests\n' + ' that can invoke all MCP tools (config_set, agent_spawn, blackboard_write, …) without credentials.\n' + ' Set a secret: --secret or export NETWORK_AI_MCP_SECRET=\n' + ' For local stdio use (Claude Desktop / Cursor / Glama) run with --stdio instead.\n' ); process.exit(1); } const server = new McpSseServer(combined, { port: args.port, host: args.host, heartbeatMs: args.heartbeat, secret: args.secret, }); await server.listen(); const localUrl = `http://localhost:${args.port}`; console.log(`\n[network-ai-server] ✓ Listening on ${localUrl}`); console.log(`[network-ai-server] SSE stream : ${localUrl}/sse`); console.log(`[network-ai-server] Tool calls : ${localUrl}/mcp (POST)`); console.log(`[network-ai-server] Health : ${localUrl}/health`); console.log(`[network-ai-server] All tools : ${localUrl}/tools\n`); console.log(`[network-ai-server] Connect any MCP client to: ${localUrl}`); console.log(`[network-ai-server] Press Ctrl+C to stop.\n`); // -------------------------------------------------------------------------- // 7. Graceful shutdown // -------------------------------------------------------------------------- const shutdown = async (signal: string): Promise => { console.log(`\n[network-ai-server] Received ${signal} — shutting down...`); await server.close(); console.log('[network-ai-server] Server stopped. Goodbye.\n'); process.exit(0); }; process.on('SIGINT', () => void shutdown('SIGINT')); process.on('SIGTERM', () => void shutdown('SIGTERM')); } main().catch(err => { console.error('\n[network-ai-server] Fatal error:', err instanceof Error ? err.message : String(err)); process.exit(1); });