Skip to main content
Glama

MCP Goose Subagents Server

by lordstyled55
index.js14.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);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lordstyled55/goose-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server