Skip to main content
Glama
RunAgentTool.ts24 kB
/** * RunAgentTool implementation for executing Claude Code sub-agents via MCP * * Provides the run_agent tool that allows MCP clients to execute specific * agents with parameters, integrating with AgentExecutor and AgentManager * for complete agent execution workflow. */ import { randomUUID } from 'node:crypto' import type { AgentManager } from 'src/agents/AgentManager' import type { AgentExecutionResult, AgentExecutor } from 'src/execution/AgentExecutor' import { formatSessionHistory } from 'src/session/SessionHistoryFormatter' import type { SessionManager } from 'src/session/SessionManager' import type { ExecutionParams } from 'src/types/ExecutionParams' import { type LogLevel, Logger } from 'src/utils/Logger' /** * MCP tool content type for text responses */ interface McpTextContent { [x: string]: unknown type: 'text' text: string } /** * MCP tool response format */ interface McpToolResponse { [x: string]: unknown content: McpTextContent[] isError?: boolean structuredContent?: unknown _meta?: { session_id: string } } /** * MCP response data structure (ADR-0003) * This structure is used in both content[0].text (as JSON string) and structuredContent */ interface McpResponseData { result: string session_id?: string agent: string exit_code: number execution_time: number status: 'success' | 'partial' | 'error' request_id?: string } /** * Input schema for run_agent tool parameters */ interface RunAgentInputSchema { [x: string]: unknown type: 'object' properties: { [x: string]: object agent: { type: 'string' description: string } prompt: { type: 'string' description: string } cwd: { type: 'string' description: string } extra_args: { type: 'array' items: { type: 'string' } description: string } session_id: { type: 'string' description: string } } required: string[] } /** * Parameters for run_agent tool execution */ interface RunAgentParams { agent: string prompt: string cwd?: string | undefined extra_args?: string[] | undefined /** * Session ID for continuing previous conversation context (optional) * * When provided, the agent will have access to previous request/response history. * Must be alphanumeric with hyphens and underscores only (max 100 characters). */ session_id?: string | undefined } /** * RunAgentTool class implementing the run_agent MCP tool * * Provides execution of Claude Code sub-agents with parameter validation, * error handling, and proper MCP response formatting. */ export class RunAgentTool { public readonly name = 'run_agent' public readonly description = 'Delegate complex, multi-step, or specialized tasks to an autonomous agent for independent execution with dedicated context (e.g., refactoring across multiple files, fixing all test failures, systematic codebase analysis, batch operations). Returns session_id in response metadata - reuse it in subsequent calls to maintain conversation context continuity across multiple agent executions.' private logger: Logger private executionStats: Map<string, { count: number; totalTime: number; lastUsed: Date }> = new Map() public readonly inputSchema: RunAgentInputSchema = { type: 'object', properties: { agent: { type: 'string', description: 'Identifier of the specialized agent to delegate the task to', }, prompt: { type: 'string', description: 'Task description or instructions for the agent to execute. When referencing file paths, use absolute paths to ensure proper file access.', }, cwd: { type: 'string', description: 'Working directory path for agent execution context (optional)', }, extra_args: { type: 'array', items: { type: 'string' }, description: 'Additional configuration parameters for agent execution (optional)', }, session_id: { type: 'string', description: 'Session ID for continuing previous conversation context (optional). If omitted, a new session will be auto-generated and returned in response metadata. Reuse the returned session_id in subsequent calls to maintain context continuity.', }, }, required: ['agent', 'prompt'], } constructor( private agentExecutor?: AgentExecutor, private agentManager?: AgentManager, private sessionManager?: SessionManager ) { // Use LOG_LEVEL environment variable if available const logLevel = (process.env['LOG_LEVEL'] as LogLevel) || 'info' this.logger = new Logger(logLevel) } /** * Execute the run_agent tool with the provided parameters * * @param params - Tool execution parameters * @returns Promise resolving to MCP tool response * @throws {Error} When parameters are invalid or execution fails */ async execute(params: unknown): Promise<McpToolResponse> { const startTime = Date.now() const requestId = this.generateRequestId() this.logger.info('Run agent tool execution started', { requestId, timestamp: new Date().toISOString(), }) // Best-effort cleanup: old session files (ADR-0002) // Non-blocking execution - does not affect main processing flow if (this.sessionManager) { Promise.resolve() .then(() => this.sessionManager!.cleanupOldSessions()) .catch((error) => { this.logger.warn('Session cleanup failed (best-effort)', { requestId, error: error instanceof Error ? error.message : String(error), }) }) } try { // Validate parameters with enhanced validation const validatedParams = this.validateParams(params) // Auto-generate session_id if not provided and SessionManager is available const sessionId = validatedParams.session_id || (this.sessionManager ? randomUUID() : undefined) this.logger.debug('Parameters validated successfully', { requestId, agent: validatedParams.agent, promptLength: validatedParams.prompt.length, cwd: validatedParams.cwd, extraArgsCount: validatedParams.extra_args?.length || 0, sessionId: sessionId, sessionIdGenerated: !validatedParams.session_id && !!sessionId, }) // Check if agent exists if (this.agentManager) { const agent = await this.agentManager.getAgent(validatedParams.agent) if (!agent) { this.logger.warn('Agent not found', { requestId, requestedAgent: validatedParams.agent, }) return this.createErrorResponse( `Agent '${validatedParams.agent}' not found`, await this.getAvailableAgentsList() ) } this.logger.debug('Agent found and validated', { requestId, agentName: agent.name, agentDescription: agent.description, }) } // Execute agent if executor is available if (this.agentExecutor) { // Report progress: Starting agent execution // Get agent definition content if available let agentContext = validatedParams.agent if (this.agentManager) { const agent = await this.agentManager.getAgent(validatedParams.agent) if (agent?.content) { // Include full agent definition content as system context agentContext = agent.content } } // Load session history if session_id is provided and SessionManager is available let promptWithHistory = validatedParams.prompt if (sessionId && this.sessionManager) { try { // CRITICAL: Pass agent_type to enforce sub-agent isolation const sessionData = await this.sessionManager.loadSession( sessionId, validatedParams.agent ) if (sessionData && sessionData.history.length > 0) { // Convert session history to Markdown format for token efficiency and LLM comprehension const historyMarkdown = formatSessionHistory(sessionData) promptWithHistory = `Previous conversation history:\n\n${historyMarkdown}\n\n---\n\nCurrent request:\n${validatedParams.prompt}` this.logger.info('Session history loaded and merged', { requestId, sessionId: sessionId, historyEntries: sessionData.history.length, }) } else { this.logger.debug('No session history found', { requestId, sessionId: sessionId, }) } } catch (error) { // Log error but continue - session loading failure should not break main flow this.logger.warn('Failed to load session history', { requestId, sessionId: sessionId, error: error instanceof Error ? error.message : String(error), }) } } const executionParams: ExecutionParams = { agent: agentContext, prompt: promptWithHistory, ...(validatedParams.cwd !== undefined && { cwd: validatedParams.cwd }), ...(validatedParams.extra_args !== undefined && { extra_args: validatedParams.extra_args, }), } // Report progress: Executing agent // Execute agent (this has its own timeout: MCP -> AI) const result = await this.agentExecutor.executeAgent(executionParams) // Report progress: Execution completed // Update execution statistics this.updateExecutionStats(validatedParams.agent, result.executionTime) this.logger.info('Agent execution completed successfully', { requestId, agent: validatedParams.agent, exitCode: result.exitCode, executionTime: result.executionTime, totalTime: Date.now() - startTime, }) // Save session if session_id is available and SessionManager is available if (sessionId && this.sessionManager) { try { // Build request object with only defined properties const sessionRequest: { agent: string prompt: string cwd?: string extra_args?: string[] } = { agent: validatedParams.agent, prompt: validatedParams.prompt, } if (validatedParams.cwd !== undefined) { sessionRequest.cwd = validatedParams.cwd } if (validatedParams.extra_args !== undefined) { sessionRequest.extra_args = validatedParams.extra_args } await this.sessionManager.saveSession(sessionId, sessionRequest, { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, executionTime: result.executionTime, }) this.logger.info('Session saved successfully', { requestId, sessionId: sessionId, }) } catch (error) { // Log error but continue - session save failure should not break main flow this.logger.warn('Failed to save session', { requestId, sessionId: sessionId, error: error instanceof Error ? error.message : String(error), }) } } // Mark MCP request as completed return this.formatExecutionResponse(result, validatedParams.agent, requestId, sessionId) } // Fallback response if executor is not available this.logger.warn('Agent executor not available', { requestId }) return { content: [ { type: 'text', text: `Agent execution request received for '${validatedParams.agent}' with prompt: "${validatedParams.prompt}"\n\nNote: Agent executor not initialized.`, }, ], } } catch (error) { const totalTime = Date.now() - startTime this.logger.error('Agent execution failed', error instanceof Error ? error : undefined, { requestId, totalTime, errorType: error instanceof Error ? error.constructor.name : 'Unknown', }) return this.createErrorResponse( `Agent execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, null ) } } /** * Validate and type-check the input parameters with comprehensive validation * * @private * @param params - Raw parameters to validate * @returns Validated parameters * @throws {Error} When parameters are invalid */ private validateParams(params: unknown): RunAgentParams { if (!params || typeof params !== 'object') { throw new Error('Invalid parameters: expected object') } const p = params as Record<string, unknown> // Validate required agent parameter with enhanced checks if (!p['agent'] || typeof p['agent'] !== 'string') { throw new Error('Agent parameter is required and must be a string') } const agentName = p['agent'].trim() if (agentName === '') { throw new Error('Invalid agent parameter: cannot be empty') } // Enhanced agent name validation if (agentName.length > 100) { throw new Error('Agent name too long (max 100 characters)') } if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) { throw new Error( 'Agent name contains invalid characters (only alphanumeric, underscore, and dash allowed)' ) } // Validate required prompt parameter with enhanced checks if (!p['prompt'] || typeof p['prompt'] !== 'string') { throw new Error('Prompt parameter is required and must be a string') } const prompt = p['prompt'].trim() if (prompt === '') { throw new Error('Invalid prompt parameter: cannot be empty') } if (prompt.length > 50000) { throw new Error('Prompt too long (max 50,000 characters)') } // Validate optional cwd parameter with path validation if (p['cwd'] !== undefined) { if (typeof p['cwd'] !== 'string') { throw new Error('CWD parameter must be a string if provided') } if (p['cwd'].length > 1000) { throw new Error('Working directory path too long (max 1000 characters)') } // Basic path security check - prevent obvious malicious paths if (p['cwd'].includes('..') || p['cwd'].includes('\0')) { throw new Error('Invalid working directory path') } } // Validate optional extra_args parameter with enhanced checks if (p['extra_args'] !== undefined) { if (!Array.isArray(p['extra_args'])) { throw new Error('Extra args parameter must be an array if provided') } if (p['extra_args'].length > 20) { throw new Error('Too many extra arguments (max 20 allowed)') } for (const [index, arg] of p['extra_args'].entries()) { if (typeof arg !== 'string') { throw new Error(`Extra argument at index ${index} must be a string`) } if (arg.length > 1000) { throw new Error(`Extra argument at index ${index} too long (max 1000 characters)`) } } } // Validate optional session_id parameter if (p['session_id'] !== undefined) { if (typeof p['session_id'] !== 'string') { throw new Error('Session ID parameter must be a string if provided') } const sessionId = p['session_id'].trim() if (sessionId === '') { throw new Error('Invalid session ID parameter: cannot be empty') } if (sessionId.length > 100) { throw new Error('Session ID too long (max 100 characters)') } // Validate session ID format (alphanumeric, hyphens, underscores only) if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) { throw new Error( 'Session ID contains invalid characters (only alphanumeric, underscore, and dash allowed)' ) } } return { agent: agentName, prompt: prompt, cwd: p['cwd'] as string | undefined, extra_args: p['extra_args'] as string[] | undefined, session_id: p['session_id'] as string | undefined, } } /** * Type guard to check if a value is a Record (plain object) * * @private * @param value - Value to check * @returns True if value is a Record */ private isRecord(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value) } /** * Type guard to check if a value is a string * * @private * @param value - Value to check * @returns True if value is a string */ private isStringField(value: unknown): value is string { return typeof value === 'string' } /** * Extract content from agent response JSON * * Implements information extraction layer to hide agent implementation details. * Supports both cursor-agent and claude code response formats. * * @private * @param resultJson - Parsed agent response JSON (unknown type for safety) * @param isError - Whether this is an error response * @param stdout - Raw stdout as fallback * @param stderr - Raw stderr as fallback * @returns Extracted content string */ private extractAgentContent( resultJson: unknown, isError: boolean, stdout: string, stderr: string ): string { // Type guard: Check if resultJson is a valid record if (!this.isRecord(resultJson)) { return stdout || stderr || 'No output' } // Priority field differs between success and error cases const primaryField = isError ? 'error' : 'result' // Extract primary field (result or error) if (this.isStringField(resultJson[primaryField])) { return resultJson[primaryField] } // Fallback to content field (claude code may use this) if (this.isStringField(resultJson['content'])) { return resultJson['content'] } // Final fallback to raw stdout/stderr return stdout || stderr || 'No output' } /** * Determine if agent response indicates an error * * Checks is_error flag first (agent-level error), then exitCode (process-level error). * * @private * @param resultJson - Parsed agent response JSON * @param exitCode - Process exit code * @returns True if response indicates an error */ private isAgentError(resultJson: unknown, exitCode: number): boolean { // Priority 1: Check agent's is_error flag if (this.isRecord(resultJson) && resultJson['is_error'] === true) { return true } // Priority 2: Check process exit code (excluding special cases) // 143: SIGTERM (normal termination) // 124: Timeout (may have partial result) return exitCode !== 0 && exitCode !== 143 && exitCode !== 124 } /** * Format agent execution response (ADR-0003) * * Returns MCP 2025-06-18 compliant response with: * - content[0].text: JSON string (readable by current clients) * - structuredContent: structured data (MCP 2025-06-18 standard) * - _meta.session_id: session tracking (ADR-0002) * * @private * @param result - Agent execution result * @param agentName - Name of the executed agent * @param requestId - Request tracking ID * @param sessionId - Session ID if session management is used * @returns Formatted MCP response */ private formatExecutionResponse( result: AgentExecutionResult, agentName: string, requestId?: string, sessionId?: string ): McpToolResponse { // Determine if response indicates an error (agent-level or process-level) const isError = this.isAgentError(result.resultJson, result.exitCode) // Extract actual content from agent response const contentText = this.extractAgentContent( result.resultJson, isError, result.stdout, result.stderr ) // Determine detailed status const isSuccess = result.exitCode === 0 || // Normal completion (result.exitCode === 143 && result.hasResult === true) // SIGTERM with result const isPartialSuccess = result.exitCode === 124 && result.hasResult === true // Timeout with partial result // Build response data structure (ADR-0003) // This object is used in both content[0].text and structuredContent const responseData: McpResponseData = { result: contentText, agent: agentName, exit_code: result.exitCode, execution_time: result.executionTime, status: isSuccess ? 'success' : isPartialSuccess ? 'partial' : 'error', ...(sessionId && { session_id: sessionId }), ...(requestId && { request_id: requestId }), } const response: McpToolResponse = { content: [ { type: 'text', // JSON string format for current MCP clients (Cursor, Claude Code, etc.) text: JSON.stringify(responseData, null, 2), }, ], isError: isError, // Structured data format (MCP 2025-06-18 standard) structuredContent: responseData, } // Add session_id to response metadata (ADR-0002) if (sessionId) { response._meta = { session_id: sessionId, } } return response } /** * Create error response with optional available agents list (ADR-0003) * * @private * @param errorMessage - Error message to display * @param availableAgents - Optional list of available agents * @returns Error response in MCP format */ private createErrorResponse( errorMessage: string, availableAgents: string[] | null ): McpToolResponse { // Build error response data const errorData: Record<string, unknown> = { status: 'error', error: errorMessage, ...(availableAgents && { available_agents: availableAgents }), } return { content: [ { type: 'text', // JSON string format for current MCP clients text: JSON.stringify(errorData, null, 2), }, ], isError: true, // Structured data format (MCP 2025-06-18 standard) structuredContent: errorData, } } /** * Generate unique request ID for tracking * * @private * @returns Unique request identifier */ private generateRequestId(): string { return `run_agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } /** * Update execution statistics * * @private * @param agentName - Name of the executed agent * @param executionTime - Time taken for execution */ private updateExecutionStats(agentName: string, executionTime: number): void { const existing = this.executionStats.get(agentName) if (existing) { existing.count += 1 existing.totalTime += executionTime existing.lastUsed = new Date() } else { this.executionStats.set(agentName, { count: 1, totalTime: executionTime, lastUsed: new Date(), }) } } /** * Get execution statistics for monitoring * * @returns Map of agent execution statistics */ getExecutionStats(): Map<string, { count: number; totalTime: number; lastUsed: Date }> { return new Map(this.executionStats) } /** * Get list of available agent names * * @private * @returns Promise resolving to array of agent names */ private async getAvailableAgentsList(): Promise<string[] | null> { if (!this.agentManager) { return null } try { const agents = await this.agentManager.listAgents() return agents.map((agent) => agent.name) } catch (error) { this.logger.warn('Failed to get available agents list', { error: error instanceof Error ? error.message : 'Unknown error', }) return null } } }

Implementation Reference

Latest Blog Posts

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/shinpr/sub-agents-mcp'

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