Skip to main content
Glama
AgentResources.ts9.77 kB
/** * AgentResources implementation for publishing agent definitions via MCP * * Provides MCP resources for agent discovery including a list of all * available agents and individual agent definition resources. */ import type { AgentManager } from 'src/agents/AgentManager' import type { AgentDefinition } from 'src/types/AgentDefinition' import { Logger } from 'src/utils/Logger' /** * MCP resource content type for text responses */ interface McpResourceContent { [x: string]: unknown type: 'text' text: string uri: string } /** * MCP resource response format */ interface McpResourceResponse { [x: string]: unknown contents: McpResourceContent[] } /** * MCP resource definition for publication */ interface McpResource { [x: string]: unknown uri: string name: string description: string mimeType?: string } /** * AgentResources class for managing agent definition resources in MCP * * Publishes agent information as MCP resources that clients can discover * and read to understand available agents and their capabilities. */ export class AgentResources { private logger: Logger constructor(private agentManager?: AgentManager) { this.logger = new Logger('info') } /** * Get list of all published agent resources * * @returns Promise resolving to array of MCP resource definitions */ async listResources(): Promise<McpResource[]> { const startTime = Date.now() this.logger.debug('Starting resource listing', { timestamp: new Date().toISOString(), }) const resources: McpResource[] = [] // Add agent list resource resources.push({ uri: 'agents://list', name: 'Agent List', description: 'List of available Claude Code sub-agents', mimeType: 'text/plain', }) // Add individual agent resources if agent manager is available if (this.agentManager) { try { const agents = await this.agentManager.listAgents() this.logger.debug('Agents loaded for resource listing', { agentCount: agents.length, loadTime: Date.now() - startTime, }) for (const agent of agents) { resources.push({ uri: `agents://${agent.name}`, name: `Agent: ${agent.name}`, description: agent.description || 'Agent definition', mimeType: 'text/markdown', }) } } catch (error) { this.logger.error( 'Failed to load agents for resource listing', error instanceof Error ? error : undefined, { loadTime: Date.now() - startTime, } ) // If we can't load agents, just return the list resource } } else { this.logger.warn('Agent manager not available for resource listing') } this.logger.info('Resource listing completed', { resourceCount: resources.length, totalTime: Date.now() - startTime, }) return resources } /** * Read content of a specific agent resource * * @param uri - Resource URI to read * @returns Promise resolving to resource content * @throws {Error} When resource URI is invalid or not found */ async readResource(uri: string): Promise<McpResourceResponse> { const startTime = Date.now() const requestId = this.generateRequestId() this.logger.info('Resource read requested', { requestId, uri, timestamp: new Date().toISOString(), }) try { let result: McpResourceResponse if (uri === 'agents://list') { result = await this.getAgentListContent() } else { // Check for individual agent resource const agentNameMatch = uri.match(/^agents:\/\/(.+)$/) if (agentNameMatch?.[1]) { const agentName = agentNameMatch[1] result = await this.getAgentContent(agentName) } else { throw new Error(`Invalid resource URI format: ${uri}`) } } this.logger.info('Resource read completed', { requestId, uri, readTime: Date.now() - startTime, contentLength: result.contents[0]?.text?.length || 0, }) return result } catch (error) { this.logger.error('Resource read failed', error instanceof Error ? error : undefined, { requestId, uri, readTime: Date.now() - startTime, }) throw error } } /** * Get content for the agent list resource * * @private * @returns Promise resolving to agent list content */ private async getAgentListContent(): Promise<McpResourceResponse> { if (!this.agentManager) { return { contents: [ { type: 'text', text: 'Agent manager not available. No agents can be listed.', uri: 'agents://list', }, ], } } try { const agents = await this.agentManager.listAgents() if (agents.length === 0) { return { contents: [ { type: 'text', text: 'No agents available. Check agent directory configuration.', uri: 'agents://list', }, ], } } let listText = `Available Claude Code Sub-Agents (${agents.length} total):\n\n` for (const agent of agents) { listText += `## ${agent.name}\n` listText += `**Description:** ${agent.description}\n` listText += `**File:** ${agent.filePath}\n` listText += `**Last Modified:** ${agent.lastModified.toISOString()}\n` listText += `**Resource URI:** agents://${agent.name}\n\n` } return { contents: [ { type: 'text', text: listText, uri: 'agents://list', }, ], } } catch (error) { return { contents: [ { type: 'text', text: `Error loading agent list: ${error instanceof Error ? error.message : 'Unknown error'}`, uri: 'agents://list', }, ], } } } /** * Get content for a specific agent resource * * @private * @param agentName - Name of the agent to get content for * @returns Promise resolving to agent content */ private async getAgentContent(agentName: string): Promise<McpResourceResponse> { if (!this.agentManager) { return { contents: [ { type: 'text', text: 'Agent manager not available. Agent content cannot be retrieved.', uri: `agents://${agentName}`, }, ], } } try { const agent = await this.agentManager.getAgent(agentName) if (!agent) { const availableAgents = await this.getAvailableAgentNames() let errorText = `Agent '${agentName}' not found.` if (availableAgents.length > 0) { errorText += `\n\nAvailable agents:\n${availableAgents.map((name) => `- ${name}`).join('\n')}` } return { contents: [ { type: 'text', text: errorText, uri: `agents://${agentName}`, }, ], } } return { contents: [ { type: 'text', text: this.formatAgentContent(agent), uri: `agents://${agentName}`, }, ], } } catch (error) { return { contents: [ { type: 'text', text: `Error loading agent '${agentName}': ${error instanceof Error ? error.message : 'Unknown error'}`, uri: `agents://${agentName}`, }, ], } } } /** * Format agent definition content for display * * @private * @param agent - Agent definition to format * @returns Formatted content string */ private formatAgentContent(agent: AgentDefinition): string { let content = `# Agent: ${agent.name}\n\n` content += `**Description:** ${agent.description}\n` content += `**File Path:** ${agent.filePath}\n` content += `**Last Modified:** ${agent.lastModified.toISOString()}\n` content += `**Content Length:** ${agent.content.length} characters\n\n` content += '## Agent Definition\n\n' content += agent.content return content } /** * Get list of available agent names * * @private * @returns Promise resolving to array of agent names */ private async getAvailableAgentNames(): Promise<string[]> { if (!this.agentManager) { return [] } try { const agents = await this.agentManager.listAgents() return agents.map((agent) => agent.name) } catch (error) { return [] } } /** * Generate unique request ID for tracking * * @private * @returns Unique request identifier */ private generateRequestId(): string { return `resource_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } /** * Check if a resource URI is valid with enhanced validation * * @param uri - Resource URI to validate * @returns True if URI is valid */ isValidResourceUri(uri: string): boolean { if (!uri || typeof uri !== 'string') { return false } if (uri === 'agents://list') { return true } const agentNameMatch = uri.match(/^agents:\/\/(.+)$/) if (!agentNameMatch || !agentNameMatch[1]) { return false } const agentName = agentNameMatch[1] // Validate agent name format if (agentName.length === 0 || agentName.length > 100) { return false } // Check for valid characters (same as RunAgentTool validation) if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) { return false } return true } }

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