index.js•14.1 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs/promises';
import path from 'path';
const execAsync = promisify(exec);
class GooseSubagentServer {
constructor() {
this.server = new Server(
{
name: 'goose-subagents',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.activeSubagents = new Map();
this.setupToolHandlers();
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'delegate_to_subagents',
description: 'Delegate tasks to Goose CLI subagents for autonomous development',
inputSchema: {
type: 'object',
properties: {
task: {
type: 'string',
description: 'The development task to delegate to subagents'
},
agents: {
type: 'array',
items: {
type: 'object',
properties: {
role: {
type: 'string',
description: 'Agent role (e.g., backend_developer, frontend_developer, qa_engineer)'
},
instructions: {
type: 'string',
description: 'Specific instructions for this agent'
},
recipe: {
type: 'string',
description: 'Optional recipe name to use for this agent'
}
},
required: ['role', 'instructions']
},
description: 'Array of subagents to create'
},
execution_mode: {
type: 'string',
enum: ['parallel', 'sequential'],
default: 'parallel',
description: 'How to execute the subagents'
},
working_directory: {
type: 'string',
description: 'Working directory for the subagents (defaults to current directory)'
}
},
required: ['task', 'agents']
}
},
{
name: 'create_goose_recipe',
description: 'Create a reusable Goose recipe for specialized subagents',
inputSchema: {
type: 'object',
properties: {
recipe_name: {
type: 'string',
description: 'Name of the recipe'
},
role: {
type: 'string',
description: 'Agent role (e.g., code_reviewer, security_auditor)'
},
instructions: {
type: 'string',
description: 'Detailed instructions for the agent'
},
extensions: {
type: 'array',
items: { type: 'string' },
description: 'List of Goose extensions to enable'
},
parameters: {
type: 'object',
description: 'Recipe parameters'
}
},
required: ['recipe_name', 'role', 'instructions']
}
},
{
name: 'list_active_subagents',
description: 'List currently active subagents and their status',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_subagent_results',
description: 'Get results from completed subagents',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'Session ID to get results for'
}
},
required: ['session_id']
}
}
]
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'delegate_to_subagents':
return await this.delegateToSubagents(args);
case 'create_goose_recipe':
return await this.createGooseRecipe(args);
case 'list_active_subagents':
return await this.listActiveSubagents();
case 'get_subagent_results':
return await this.getSubagentResults(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Error executing ${name}: ${error.message}`
);
}
});
}
async delegateToSubagents(args) {
const { task, agents, execution_mode = 'parallel', working_directory = process.cwd() } = args;
const sessionId = uuidv4();
// Ensure Goose alpha features are enabled
process.env.ALPHA_FEATURES = 'true';
try {
// Create the delegation prompt for Goose
const delegationPrompt = this.createDelegationPrompt(task, agents, execution_mode);
// Execute Goose with the delegation prompt
const gooseProcess = await this.executeGoose(delegationPrompt, working_directory, sessionId);
this.activeSubagents.set(sessionId, {
task,
agents,
execution_mode,
status: 'running',
startTime: new Date(),
process: gooseProcess
});
return {
content: [
{
type: 'text',
text: `Successfully delegated task to ${agents.length} subagents in ${execution_mode} mode.\n\nSession ID: ${sessionId}\nTask: ${task}\n\nSubagents created:\n${agents.map(agent => `- ${agent.role}: ${agent.instructions}`).join('\n')}\n\nUse get_subagent_results with session_id "${sessionId}" to retrieve results when complete.`
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to delegate to subagents: ${error.message}`
);
}
}
createDelegationPrompt(task, agents, execution_mode) {
const agentDescriptions = agents.map(agent => {
let description = `${agent.role} with instructions: "${agent.instructions}"`;
if (agent.recipe) {
description += ` using recipe "${agent.recipe}"`;
}
return description;
}).join(', ');
const executionPhrase = execution_mode === 'parallel'
? 'in parallel simultaneously'
: 'sequentially one after another';
return `Use ${agents.length} subagents working ${executionPhrase} to complete this task: "${task}".
Create these specialized subagents: ${agentDescriptions}.
Each subagent should work independently on their assigned part of the task. Provide a comprehensive summary of all results when complete.`;
}
async executeGoose(prompt, workingDirectory, sessionId) {
return new Promise((resolve, reject) => {
// Spawn Goose CLI process for this subagent
const gooseProcess = spawn('goose', [
'run',
'--text', prompt,
'--name', `${sessionId}`,
'--path', workingDirectory,
'--no-session'
], {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: workingDirectory,
env: { ...process.env, ALPHA_FEATURES: 'true' }
});
let output = '';
let errorOutput = '';
gooseProcess.stdout.on('data', (data) => {
output += data.toString();
});
gooseProcess.stderr.on('data', (data) => {
errorOutput += data.toString();
});
gooseProcess.on('close', (code) => {
if (this.activeSubagents.has(sessionId)) {
const session = this.activeSubagents.get(sessionId);
session.status = code === 0 ? 'completed' : 'failed';
session.output = output;
session.error = errorOutput;
session.endTime = new Date();
}
resolve({ code, output, error: errorOutput });
});
gooseProcess.on('error', (error) => {
if (this.activeSubagents.has(sessionId)) {
const session = this.activeSubagents.get(sessionId);
session.status = 'failed';
session.error = error.message;
session.endTime = new Date();
}
reject(new Error(`Failed to start Goose: ${error.message}`));
});
});
}
async createGooseRecipe(args) {
const { recipe_name, role, instructions, extensions = [], parameters = {} } = args;
const recipe = {
id: recipe_name,
version: '1.0.0',
title: `${role.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} Recipe`,
description: `Specialized subagent for ${role}`,
instructions: instructions,
activities: [
`Perform ${role} tasks`,
'Analyze and provide feedback',
'Generate deliverables'
],
extensions: extensions.map(ext => ({
type: 'builtin',
name: ext,
display_name: ext.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
timeout: 300,
bundled: true
})),
parameters: Object.entries(parameters).map(([key, value]) => ({
key,
input_type: typeof value,
requirement: 'optional',
description: `Parameter for ${key}`,
default: value
})),
prompt: instructions
};
// Create recipes directory if it doesn't exist
const recipesDir = path.join(process.cwd(), 'goose-recipes');
await fs.mkdir(recipesDir, { recursive: true });
// Write recipe file
const recipeFile = path.join(recipesDir, `${recipe_name}.yaml`);
const yamlContent = this.objectToYaml(recipe);
await fs.writeFile(recipeFile, yamlContent);
return {
content: [
{
type: 'text',
text: `Successfully created Goose recipe "${recipe_name}" at ${recipeFile}\n\nRecipe details:\n- Role: ${role}\n- Extensions: ${extensions.join(', ') || 'none'}\n- Parameters: ${Object.keys(parameters).join(', ') || 'none'}\n\nTo use this recipe, set GOOSE_RECIPE_PATH environment variable to the recipes directory or place the recipe in your working directory.`
}
]
};
}
objectToYaml(obj, indent = 0) {
let yaml = '';
const spaces = ' '.repeat(indent);
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) continue;
if (Array.isArray(value)) {
yaml += `${spaces}${key}:\n`;
for (const item of value) {
if (typeof item === 'object') {
yaml += `${spaces}- \n${this.objectToYaml(item, indent + 1).split('\n').map(line => line ? `${spaces} ${line}` : '').join('\n')}\n`;
} else {
yaml += `${spaces}- ${item}\n`;
}
}
} else if (typeof value === 'object') {
yaml += `${spaces}${key}:\n${this.objectToYaml(value, indent + 1)}`;
} else if (typeof value === 'string' && value.includes('\n')) {
yaml += `${spaces}${key}: |\n${value.split('\n').map(line => `${spaces} ${line}`).join('\n')}\n`;
} else {
yaml += `${spaces}${key}: ${value}\n`;
}
}
return yaml;
}
async listActiveSubagents() {
const sessions = Array.from(this.activeSubagents.entries()).map(([id, session]) => ({
session_id: id,
task: session.task,
agent_count: session.agents.length,
execution_mode: session.execution_mode,
status: session.status,
start_time: session.startTime,
end_time: session.endTime || null,
duration: session.endTime
? `${Math.round((session.endTime - session.startTime) / 1000)}s`
: `${Math.round((new Date() - session.startTime) / 1000)}s (ongoing)`
}));
return {
content: [
{
type: 'text',
text: sessions.length > 0
? `Active Subagent Sessions:\n\n${sessions.map(s =>
`Session: ${s.session_id}\n` +
`Task: ${s.task}\n` +
`Agents: ${s.agent_count} (${s.execution_mode})\n` +
`Status: ${s.status}\n` +
`Duration: ${s.duration}\n`
).join('\n')}`
: 'No active subagent sessions.'
}
]
};
}
async getSubagentResults(args) {
const { session_id } = args;
if (!this.activeSubagents.has(session_id)) {
throw new McpError(
ErrorCode.InvalidRequest,
`Session ${session_id} not found`
);
}
const session = this.activeSubagents.get(session_id);
return {
content: [
{
type: 'text',
text: `Subagent Session Results\n\nSession ID: ${session_id}\nTask: ${session.task}\nStatus: ${session.status}\nExecution Mode: ${session.execution_mode}\nAgents: ${session.agents.length}\n\n` +
`Start Time: ${session.startTime}\n` +
`End Time: ${session.endTime || 'Still running'}\n\n` +
`Output:\n${session.output || 'No output yet'}\n\n` +
`${session.error ? `Errors:\n${session.error}` : ''}`
}
]
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Goose Subagents MCP server running on stdio');
}
}
const server = new GooseSubagentServer();
server.run().catch(console.error);