/**
* MCP Server for multi-agent orchestration.
*
* Exposes tools for:
* - planner: Decompose tasks and delegate to subagents
* - subagent: Run a specific agent role directly
* - session_log: Read the session scratchpad
* - session_clear: Clear the session scratchpad
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import path from 'path';
import { AgentOrchestrator } from './agents/orchestrator.js';
import { CONFIG } from './config.js';
import { LocalHybridMemoryStore } from './memory/localMemoryStore.js';
import { createDefaultProvider } from './llm/provider.js';
import { AgentRole } from './types.js';
/**
* Format a result payload for MCP tool response.
*/
function formatResult(payload: unknown) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(payload, null, 2),
},
],
};
}
/**
* Main entry point for the MCP server.
*/
async function main(): Promise<void> {
// Initialize memory store
const memory = new LocalHybridMemoryStore(path.join(CONFIG.dataDir, 'sessions'));
// Create MCP server
const server = new McpServer(
{
name: 'mcp-subagents',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
instructions:
'Planner plus recursive code/analysis subagents. Provide a session_id to preserve scratchpad between calls. Uses client-sampled LLM (no direct API key required).',
},
);
// Initialize LLM provider using MCP sampling
const provider = createDefaultProvider(server, {
model: CONFIG.model,
temperature: CONFIG.temperature,
});
// Initialize orchestrator with defaults
const orchestrator = new AgentOrchestrator({
llm: (messages, options) => provider.complete(messages, options),
memory,
defaults: {
model: CONFIG.model,
temperature: CONFIG.temperature,
maxDepth: CONFIG.defaultMaxDepth,
},
});
// ─────────────────────────────────────────────────────────────────────────────
// Tool: planner
// ─────────────────────────────────────────────────────────────────────────────
const plannerSchema = z.object({
task: z.string().describe('The task to plan and execute'),
context: z.string().optional().describe('Additional context for the planner'),
session_id: z.string().optional().describe('Session ID for scratchpad persistence'),
max_depth: z.number().int().min(1).max(8).optional().describe('Maximum delegation depth (1-8)'),
openai_api_key: z
.string()
.min(1)
.optional()
.describe('Optional OpenAI API key for fallback when sampling is unavailable'),
});
server.registerTool(
'planner',
{
title: 'Planner',
description: 'Decompose a task, delegate to subagents, and return a concise plan with results.',
inputSchema: plannerSchema,
},
async ({ task, context, session_id, max_depth, openai_api_key }) => {
const result = await orchestrator.run({
role: 'planner',
objective: task,
context,
apiKey: openai_api_key,
sessionId: session_id,
maxDepth: max_depth,
});
return formatResult(result);
},
);
// ─────────────────────────────────────────────────────────────────────────────
// Tool: subagent
// ─────────────────────────────────────────────────────────────────────────────
const subagentSchema = z.object({
role: z
.union([z.literal('planner'), z.literal('code'), z.literal('analysis')])
.describe('Agent role to execute'),
objective: z.string().describe('Objective for the agent'),
context: z.string().optional().describe('Additional context'),
session_id: z.string().optional().describe('Session ID for scratchpad persistence'),
max_depth: z.number().int().min(1).max(8).optional().describe('Maximum delegation depth (1-8)'),
openai_api_key: z
.string()
.min(1)
.optional()
.describe('Optional OpenAI API key for fallback when sampling is unavailable'),
});
server.registerTool(
'subagent',
{
title: 'Subagent',
description: 'Run a specific agent role with optional recursive delegation.',
inputSchema: subagentSchema,
},
async ({ role, objective, context, session_id, max_depth, openai_api_key }) => {
const result = await orchestrator.run({
role: role as AgentRole,
objective,
context,
apiKey: openai_api_key,
sessionId: session_id,
maxDepth: max_depth,
});
return formatResult(result);
},
);
// ─────────────────────────────────────────────────────────────────────────────
// Tool: session_log
// ─────────────────────────────────────────────────────────────────────────────
const sessionSchema = z.object({
session_id: z.string().optional().describe('Session ID (defaults to "default")'),
});
server.registerTool(
'session_log',
{
title: 'Session log',
description: 'Return the persisted scratchpad for a session.',
inputSchema: sessionSchema,
},
async ({ session_id }) => {
const log = await memory.read(session_id ?? 'default');
return formatResult({ entries: log });
},
);
// ─────────────────────────────────────────────────────────────────────────────
// Tool: session_clear
// ─────────────────────────────────────────────────────────────────────────────
server.registerTool(
'session_clear',
{
title: 'Session clear',
description: 'Clear persisted scratchpad for a session.',
inputSchema: sessionSchema,
},
async ({ session_id }) => {
await memory.clear(session_id ?? 'default');
return formatResult({ cleared: session_id ?? 'default' });
},
);
// ─────────────────────────────────────────────────────────────────────────────
// Tool: env_status
// ─────────────────────────────────────────────────────────────────────────────
const envStatusSchema = z.object({});
server.registerTool(
'env_status',
{
title: 'Env status',
description:
'Report whether expected environment variables are present (no secrets).',
inputSchema: envStatusSchema,
},
async () => {
return formatResult({
envPath: path.join(CONFIG.projectRoot, '.env'),
hasOpenAiKey: Boolean(process.env.OPENAI_API_KEY?.trim()),
hasExtraCa: Boolean(process.env.NODE_EXTRA_CA_CERTS?.trim()),
extraCaPath: process.env.NODE_EXTRA_CA_CERTS ?? null,
});
},
);
// ─────────────────────────────────────────────────────────────────────────────
// Start server
// ─────────────────────────────────────────────────────────────────────────────
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('mcp-subagents server listening on stdio');
}
// Entry point
main().catch((error) => {
console.error('Failed to start MCP server', error);
process.exit(1);
});