Skip to main content
Glama
AgentManager.ts6.87 kB
import fs from 'node:fs' import path from 'node:path' import type { ServerConfig } from 'src/config/ServerConfig' import type { AgentDefinition } from 'src/types/AgentDefinition' import { type Logger, Logger as LoggerClass } from 'src/utils/Logger' /** * AgentManager class for discovering, loading, parsing, and caching agent definitions. * * Provides automatic detection of .md/.txt files in configured directory, * parsing of Claude Code sub-agent format, and efficient caching with * file change detection. */ export class AgentManager { private logger: Logger constructor(private config: ServerConfig) { this.logger = new LoggerClass(config.logLevel) } /** * Retrieves a specific agent definition by name. * * @param name - The name of the agent to retrieve * @returns Promise resolving to the agent definition or undefined if not found * @throws {Error} When agent name is invalid */ async getAgent(name: string): Promise<AgentDefinition | undefined> { // Input validation for security if (!name || typeof name !== 'string') { throw new Error('Invalid agent name: agent name is required') } if (name.trim().length === 0) { throw new Error('Invalid agent name: empty agent name not allowed') } if (name.length > 255) { throw new Error('Invalid agent name: too long agent name') } // Check for invalid characters that could be used for path traversal or injection const invalidChars = /[<>:"/\\|?*;`$()&|\s]/ if (invalidChars.test(name)) { throw new Error('Invalid agent name: forbidden characters detected') } // Check for control characters using char code inspection for (let i = 0; i < name.length; i++) { const charCode = name.charCodeAt(i) if ((charCode >= 0 && charCode <= 31) || charCode === 127) { throw new Error('Invalid agent name: forbidden characters detected') } } // Check for path traversal attempts if (name.includes('..') || name.includes('./') || name.includes('.\\')) { throw new Error('Invalid agent name: path traversal attempt detected') } const agents = await this.loadAgentsFromDirectory() return agents.get(name) } /** * Lists all available agent definitions. * * @returns Promise resolving to an array of all agent definitions */ async listAgents(): Promise<AgentDefinition[]> { const agents = await this.loadAgentsFromDirectory() return Array.from(agents.values()) } /** * Refreshes the agents by re-scanning the agents directory. * Forces reload of all agent definitions from disk. * * @returns Promise resolving when refresh is complete */ async refreshAgents(): Promise<void> { await this.loadAgentsFromDirectory() } /** * Loads all agent definitions from the configured directory. * Scans for .md and .txt files and parses them as agent definitions. * * @returns Map of agent name to agent definition */ private async loadAgentsFromDirectory(): Promise<Map<string, AgentDefinition>> { try { const agentsDir = path.resolve(this.config.agentsDir) this.logger.info('Starting agent discovery', { directory: agentsDir }) const files = await fs.promises.readdir(agentsDir) const agentFiles = files.filter((file) => file.endsWith('.md') || file.endsWith('.txt')) this.logger.info('Agent definition files discovered', { totalFiles: files.length, agentFiles: agentFiles.length, files: agentFiles, }) const agents = new Map<string, AgentDefinition>() for (const file of agentFiles) { const filePath = path.join(agentsDir, file) try { const agent = await this.loadAgentFromFile(filePath) if (agent) { agents.set(agent.name, agent) this.logger.debug('Agent definition loaded successfully', { name: agent.name, filePath: agent.filePath, description: agent.description, }) } } catch (error) { this.logger.error( 'Failed to load agent definition from file', error instanceof Error ? error : undefined, { filePath } ) } } this.logger.info('Agent discovery completed', { loadedAgents: agents.size, timestamp: new Date().toISOString(), }) return agents } catch (error) { this.logger.error( 'Failed to scan agents directory', error instanceof Error ? error : undefined, { directory: this.config.agentsDir } ) throw new Error(`Failed to load agents from directory: ${this.config.agentsDir}`) } } /** * Loads and parses a single agent definition from a file. * * @param filePath - Absolute path to the agent definition file * @returns Promise resolving to the parsed agent definition or undefined */ private async loadAgentFromFile(filePath: string): Promise<AgentDefinition | undefined> { try { this.logger.debug('Loading agent definition from file', { filePath }) const content = await fs.promises.readFile(filePath, 'utf-8') const stats = await fs.promises.stat(filePath) // Extract agent name from filename (without extension) const fileName = path.basename(filePath) const agentName = fileName.replace(/\.(md|txt)$/, '') // Parse description from content (first line or first heading) const description = this.extractDescription(content) const agentDefinition: AgentDefinition = { name: agentName, description, content, filePath, lastModified: stats.mtime, } this.logger.debug('Agent definition parsed successfully', { name: agentName, description, contentLength: content.length, lastModified: stats.mtime?.toISOString() ?? 'unknown', }) return agentDefinition } catch (error) { this.logger.error( 'Error reading agent definition file', error instanceof Error ? error : undefined, { filePath } ) return undefined } } /** * Extracts description from agent file content. * Looks for first heading or first line as description. * * @param content - The file content to parse * @returns Extracted description or default message */ private extractDescription(content: string): string { const lines = content.split('\n').filter((line) => line.trim()) // Look for first markdown heading for (const line of lines) { if (line.startsWith('#')) { return line.replace(/^#+\s*/, '').trim() } } // Fall back to first non-empty line if (lines.length > 0 && lines[0]) { return lines[0].trim() } return 'Agent definition' } }

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