feat(mcp): add Mission Control MCP server for agent-native tool access
Zero-dependency MCP server (stdio transport, JSON-RPC 2.0) that wraps the Mission Control REST API as 35 MCP tools. Agents can add it with: claude mcp add mission-control -- node scripts/mc-mcp-server.cjs Tools cover the full agent workflow: - Agent lifecycle: list, get, heartbeat, wake, diagnostics, attribution - Memory & Soul: read/write/clear working memory, read/write SOUL, list/retrieve SOUL templates - Tasks: CRUD, queue polling, broadcast, comments (list/add) - Sessions: list, control, continue, transcript - Connections: list, register - Tokens & Costs: stats, agent-costs, costs-by-agent - Skills: list, read content - Cron: list jobs - Status: health, dashboard, overview Auth uses the same profile system as the CLI (~/.mission-control/profiles/) or MC_URL/MC_API_KEY/MC_COOKIE environment variables. Also adds `mc` and `mc:mcp` package.json scripts.
This commit is contained in:
parent
f2747b5330
commit
59f7f0d720
|
|
@ -6,6 +6,8 @@
|
||||||
"verify:node": "node scripts/check-node-version.mjs",
|
"verify:node": "node scripts/check-node-version.mjs",
|
||||||
"api:parity": "node scripts/check-api-contract-parity.mjs --root . --openapi openapi.json --ignore-file scripts/api-contract-parity.ignore",
|
"api:parity": "node scripts/check-api-contract-parity.mjs --root . --openapi openapi.json --ignore-file scripts/api-contract-parity.ignore",
|
||||||
"api:parity:json": "node scripts/check-api-contract-parity.mjs --root . --openapi openapi.json --ignore-file scripts/api-contract-parity.ignore --json",
|
"api:parity:json": "node scripts/check-api-contract-parity.mjs --root . --openapi openapi.json --ignore-file scripts/api-contract-parity.ignore --json",
|
||||||
|
"mc": "node scripts/mc-cli.cjs",
|
||||||
|
"mc:mcp": "node scripts/mc-mcp-server.cjs",
|
||||||
"dev": "pnpm run verify:node && next dev --hostname 127.0.0.1 --port ${PORT:-3000}",
|
"dev": "pnpm run verify:node && next dev --hostname 127.0.0.1 --port ${PORT:-3000}",
|
||||||
"build": "pnpm run verify:node && next build",
|
"build": "pnpm run verify:node && next build",
|
||||||
"start": "pnpm run verify:node && next start --hostname 0.0.0.0 --port ${PORT:-3000}",
|
"start": "pnpm run verify:node && next start --hostname 0.0.0.0 --port ${PORT:-3000}",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,637 @@
|
||||||
|
#!/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();
|
||||||
Loading…
Reference in New Issue