#!/usr/bin/env node /* Mission Control MCP Server (stdio transport) - Zero dependencies (Node.js built-ins only) - JSON-RPC 2.0 over stdin/stdout - Wraps Mission Control REST API as MCP tools - Add with: claude mcp add mission-control -- node /path/to/mc-mcp-server.cjs Environment: MC_URL Base URL (default: http://127.0.0.1:3000) MC_API_KEY API key for auth MC_COOKIE Session cookie (alternative auth) */ const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- function loadConfig() { // Try profile first, then env vars const profilePath = path.join(os.homedir(), '.mission-control', 'profiles', 'default.json'); let profile = {}; try { profile = JSON.parse(fs.readFileSync(profilePath, 'utf8')); } catch { /* no profile */ } return { baseUrl: (process.env.MC_URL || profile.url || 'http://127.0.0.1:3000').replace(/\/+$/, ''), apiKey: process.env.MC_API_KEY || profile.apiKey || '', cookie: process.env.MC_COOKIE || profile.cookie || '', }; } // --------------------------------------------------------------------------- // HTTP client (same pattern as mc-cli.cjs) // --------------------------------------------------------------------------- async function api(method, route, body) { const config = loadConfig(); const headers = { 'Accept': 'application/json' }; if (config.apiKey) headers['x-api-key'] = config.apiKey; if (config.cookie) headers['Cookie'] = config.cookie; let payload; if (body !== undefined) { headers['Content-Type'] = 'application/json'; payload = JSON.stringify(body); } const url = `${config.baseUrl}${route.startsWith('/') ? route : `/${route}`}`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 30000); try { const res = await fetch(url, { method, headers, body: payload, signal: controller.signal }); clearTimeout(timer); const text = await res.text(); let data; try { data = JSON.parse(text); } catch { data = { raw: text }; } if (!res.ok) throw new Error(data.error || data.message || `HTTP ${res.status}: ${text.slice(0, 200)}`); return data; } catch (err) { clearTimeout(timer); if (err?.name === 'AbortError') throw new Error('Request timeout (30s)'); throw err; } } // --------------------------------------------------------------------------- // Tool definitions // --------------------------------------------------------------------------- const TOOLS = [ // --- Agents --- { name: 'mc_list_agents', description: 'List all agents registered in Mission Control', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/agents'), }, { name: 'mc_get_agent', description: 'Get details of a specific agent by ID', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('GET', `/api/agents/${id}`), }, { name: 'mc_heartbeat', description: 'Send a heartbeat for an agent to indicate it is alive', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('POST', `/api/agents/${id}/heartbeat`), }, { name: 'mc_wake_agent', description: 'Wake a sleeping agent', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('POST', `/api/agents/${id}/wake`), }, { name: 'mc_agent_diagnostics', description: 'Get diagnostics info for an agent (health, config, recent activity)', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('GET', `/api/agents/${id}/diagnostics`), }, { name: 'mc_agent_attribution', description: 'Get cost attribution, audit trail, and mutation history for an agent', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' }, hours: { type: 'number', description: 'Lookback window in hours (default 24)' }, section: { type: 'string', description: 'Comma-separated sections: identity,audit,mutations,cost' }, }, required: ['id'], }, handler: async ({ id, hours, section }) => { let qs = `?hours=${hours || 24}`; if (section) qs += `§ion=${encodeURIComponent(section)}`; return api('GET', `/api/agents/${id}/attribution${qs}`); }, }, // --- Agent Memory --- { name: 'mc_read_memory', description: 'Read an agent\'s working memory', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('GET', `/api/agents/${id}/memory`), }, { name: 'mc_write_memory', description: 'Write or append to an agent\'s working memory', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' }, working_memory: { type: 'string', description: 'Memory content to write' }, append: { type: 'boolean', description: 'Append to existing memory instead of replacing (default false)' }, }, required: ['id', 'working_memory'], }, handler: async ({ id, working_memory, append }) => api('PUT', `/api/agents/${id}/memory`, { working_memory, append: append || false }), }, { name: 'mc_clear_memory', description: 'Clear an agent\'s working memory', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('DELETE', `/api/agents/${id}/memory`), }, // --- Agent Soul --- { name: 'mc_read_soul', description: 'Read an agent\'s SOUL (System of Unified Logic) content — the agent\'s identity and behavioral directives', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' } }, required: ['id'], }, handler: async ({ id }) => api('GET', `/api/agents/${id}/soul`), }, { name: 'mc_write_soul', description: 'Write an agent\'s SOUL content, or apply a named template', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' }, soul_content: { type: 'string', description: 'SOUL content to write (omit if using template_name)' }, template_name: { type: 'string', description: 'Name of a SOUL template to apply (omit if providing soul_content)' }, }, required: ['id'], }, handler: async ({ id, soul_content, template_name }) => { const body = {}; if (template_name) body.template_name = template_name; else if (soul_content) body.soul_content = soul_content; return api('PUT', `/api/agents/${id}/soul`, body); }, }, { name: 'mc_list_soul_templates', description: 'List available SOUL templates, or retrieve a specific template\'s content', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Agent ID' }, template: { type: 'string', description: 'Template name to retrieve (omit to list all)' }, }, required: ['id'], }, handler: async ({ id, template }) => { const qs = template ? `?template=${encodeURIComponent(template)}` : ''; return api('PATCH', `/api/agents/${id}/soul${qs}`); }, }, // --- Tasks --- { name: 'mc_list_tasks', description: 'List all tasks in Mission Control', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/tasks'), }, { name: 'mc_get_task', description: 'Get a specific task by ID', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Task ID' } }, required: ['id'], }, handler: async ({ id }) => api('GET', `/api/tasks/${id}`), }, { name: 'mc_create_task', description: 'Create a new task', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Task title' }, description: { type: 'string', description: 'Task description' }, priority: { type: 'string', description: 'Priority: low, medium, high, critical' }, assigned_to: { type: 'string', description: 'Agent name to assign to' }, }, required: ['title'], }, handler: async (args) => api('POST', '/api/tasks', args), }, { name: 'mc_update_task', description: 'Update an existing task (status, priority, assigned_to, title, description, etc.)', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Task ID' }, status: { type: 'string', description: 'New status' }, priority: { type: 'string', description: 'New priority' }, assigned_to: { type: 'string', description: 'New assignee agent name' }, title: { type: 'string', description: 'New title' }, description: { type: 'string', description: 'New description' }, }, required: ['id'], }, handler: async ({ id, ...fields }) => api('PUT', `/api/tasks/${id}`, fields), }, { name: 'mc_poll_task_queue', description: 'Poll the task queue for an agent — returns the next available task(s) to work on', inputSchema: { type: 'object', properties: { agent: { type: 'string', description: 'Agent name to poll for' }, max_capacity: { type: 'number', description: 'Max tasks to return (default 1)' }, }, required: ['agent'], }, handler: async ({ agent, max_capacity }) => { let qs = `?agent=${encodeURIComponent(agent)}`; if (max_capacity) qs += `&max_capacity=${max_capacity}`; return api('GET', `/api/tasks/queue${qs}`); }, }, { name: 'mc_broadcast_task', description: 'Broadcast a message to all subscribers of a task', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Task ID' }, message: { type: 'string', description: 'Message to broadcast' }, }, required: ['id', 'message'], }, handler: async ({ id, message }) => api('POST', `/api/tasks/${id}/broadcast`, { message }), }, // --- Task Comments --- { name: 'mc_list_comments', description: 'List comments on a task', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Task ID' } }, required: ['id'], }, handler: async ({ id }) => api('GET', `/api/tasks/${id}/comments`), }, { name: 'mc_add_comment', description: 'Add a comment to a task', inputSchema: { type: 'object', properties: { id: { type: ['string', 'number'], description: 'Task ID' }, content: { type: 'string', description: 'Comment text (supports @mentions)' }, parent_id: { type: 'number', description: 'Parent comment ID for threaded replies' }, }, required: ['id', 'content'], }, handler: async ({ id, content, parent_id }) => { const body = { content }; if (parent_id) body.parent_id = parent_id; return api('POST', `/api/tasks/${id}/comments`, body); }, }, // --- Sessions --- { name: 'mc_list_sessions', description: 'List all active sessions', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/sessions'), }, { name: 'mc_control_session', description: 'Control a session (monitor, pause, or terminate)', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Session ID' }, action: { type: 'string', description: 'Action: monitor, pause, or terminate' }, }, required: ['id', 'action'], }, handler: async ({ id, action }) => api('POST', `/api/sessions/${id}/control`, { action }), }, { name: 'mc_continue_session', description: 'Send a follow-up prompt to an existing session', inputSchema: { type: 'object', properties: { kind: { type: 'string', description: 'Session kind: claude-code, codex-cli, hermes' }, id: { type: 'string', description: 'Session ID' }, prompt: { type: 'string', description: 'Follow-up prompt to send' }, }, required: ['kind', 'id', 'prompt'], }, handler: async ({ kind, id, prompt }) => api('POST', '/api/sessions/continue', { kind, id, prompt }), }, { name: 'mc_session_transcript', description: 'Get the transcript of a session (messages, tool calls, reasoning)', inputSchema: { type: 'object', properties: { kind: { type: 'string', description: 'Session kind: claude-code, codex-cli, hermes' }, id: { type: 'string', description: 'Session ID' }, limit: { type: 'number', description: 'Max messages to return (default 40, max 200)' }, }, required: ['kind', 'id'], }, handler: async ({ kind, id, limit }) => { let qs = `?kind=${encodeURIComponent(kind)}&id=${encodeURIComponent(id)}`; if (limit) qs += `&limit=${limit}`; return api('GET', `/api/sessions/transcript${qs}`); }, }, // --- Connections --- { name: 'mc_list_connections', description: 'List active agent connections (tool registrations)', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/connect'), }, { name: 'mc_register_connection', description: 'Register a tool connection for an agent', inputSchema: { type: 'object', properties: { tool_name: { type: 'string', description: 'Tool name to register' }, agent_name: { type: 'string', description: 'Agent name to connect' }, }, required: ['tool_name', 'agent_name'], }, handler: async (args) => api('POST', '/api/connect', args), }, // --- Tokens & Costs --- { name: 'mc_token_stats', description: 'Get aggregate token usage statistics (total tokens, cost, request count, per-model breakdown)', inputSchema: { type: 'object', properties: { timeframe: { type: 'string', description: 'Timeframe: hour, day, week, month, all (default: all)' }, }, required: [], }, handler: async ({ timeframe }) => { let qs = '?action=stats'; if (timeframe) qs += `&timeframe=${encodeURIComponent(timeframe)}`; return api('GET', `/api/tokens${qs}`); }, }, { name: 'mc_agent_costs', description: 'Get per-agent cost breakdown with timeline and model details', inputSchema: { type: 'object', properties: { timeframe: { type: 'string', description: 'Timeframe: hour, day, week, month, all' }, }, required: [], }, handler: async ({ timeframe }) => { let qs = '?action=agent-costs'; if (timeframe) qs += `&timeframe=${encodeURIComponent(timeframe)}`; return api('GET', `/api/tokens${qs}`); }, }, { name: 'mc_costs_by_agent', description: 'Get per-agent cost summary over a number of days', inputSchema: { type: 'object', properties: { days: { type: 'number', description: 'Lookback in days (default 30, max 365)' }, }, required: [], }, handler: async ({ days }) => api('GET', `/api/tokens/by-agent?days=${days || 30}`), }, // --- Skills --- { name: 'mc_list_skills', description: 'List all skills available in the system', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/skills'), }, { name: 'mc_read_skill', description: 'Read the content of a specific skill', inputSchema: { type: 'object', properties: { source: { type: 'string', description: 'Skill source (e.g. workspace, system)' }, name: { type: 'string', description: 'Skill name' }, }, required: ['source', 'name'], }, handler: async ({ source, name }) => api('GET', `/api/skills?mode=content&source=${encodeURIComponent(source)}&name=${encodeURIComponent(name)}`), }, // --- Cron --- { name: 'mc_list_cron', description: 'List all cron jobs', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/cron'), }, // --- Status --- { name: 'mc_health', description: 'Check Mission Control health status (no auth required)', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/status?action=health'), }, { name: 'mc_dashboard', description: 'Get a dashboard summary of the entire Mission Control system (agents, tasks, sessions, costs)', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/status?action=dashboard'), }, { name: 'mc_status', description: 'Get system status overview (uptime, memory, disk, sessions, processes)', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => api('GET', '/api/status?action=overview'), }, ]; // Build lookup map const toolMap = new Map(); for (const tool of TOOLS) { toolMap.set(tool.name, tool); } // --------------------------------------------------------------------------- // JSON-RPC 2.0 / MCP protocol handler // --------------------------------------------------------------------------- const SERVER_INFO = { name: 'mission-control', version: '2.0.1', }; const CAPABILITIES = { tools: {}, }; function makeResponse(id, result) { return { jsonrpc: '2.0', id, result }; } function makeError(id, code, message, data) { return { jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } }; } async function handleMessage(msg) { const { id, method, params } = msg; // Notifications (no id) — just acknowledge if (id === undefined) { if (method === 'notifications/initialized') return null; // no response needed return null; } switch (method) { case 'initialize': return makeResponse(id, { protocolVersion: '2024-11-05', serverInfo: SERVER_INFO, capabilities: CAPABILITIES, }); case 'tools/list': return makeResponse(id, { tools: TOOLS.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema, })), }); case 'tools/call': { const toolName = params?.name; const args = params?.arguments || {}; const tool = toolMap.get(toolName); if (!tool) { return makeResponse(id, { content: [{ type: 'text', text: `Unknown tool: ${toolName}` }], isError: true, }); } try { const result = await tool.handler(args); return makeResponse(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }); } catch (err) { return makeResponse(id, { content: [{ type: 'text', text: `Error: ${err?.message || String(err)}` }], isError: true, }); } } case 'ping': return makeResponse(id, {}); default: return makeError(id, -32601, `Method not found: ${method}`); } } // --------------------------------------------------------------------------- // Stdio transport // --------------------------------------------------------------------------- function send(msg) { if (!msg) return; const json = JSON.stringify(msg); process.stdout.write(json + '\n'); } async function main() { // Disable stdout buffering for interactive use if (process.stdout._handle && process.stdout._handle.setBlocking) { process.stdout._handle.setBlocking(true); } const readline = require('node:readline'); const rl = readline.createInterface({ input: process.stdin, terminal: false }); rl.on('line', async (line) => { const trimmed = line.trim(); if (!trimmed) return; try { const msg = JSON.parse(trimmed); const response = await handleMessage(msg); send(response); } catch (err) { send(makeError(null, -32700, `Parse error: ${err?.message || 'invalid JSON'}`)); } }); rl.on('close', () => { process.exit(0); }); // Keep process alive process.stdin.resume(); } main();