UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

553 lines (475 loc) 15.5 kB
/** * Enhanced Tool registry for MCP with capability negotiation and discovery */ import type { MCPTool, MCPCapabilities, MCPProtocolVersion } from '../utils/types.js'; import type { ILogger } from '../core/logger.js'; import { MCPError } from '../utils/errors.js'; import { EventEmitter } from 'node:events'; export interface ToolCapability { name: string; version: string; description: string; category: string; tags: string[]; requiredPermissions?: string[]; supportedProtocolVersions: MCPProtocolVersion[]; dependencies?: string[]; deprecated?: boolean; deprecationMessage?: string; } export interface ToolMetrics { name: string; totalInvocations: number; successfulInvocations: number; failedInvocations: number; averageExecutionTime: number; lastInvoked?: Date; totalExecutionTime: number; } export interface ToolDiscoveryQuery { category?: string; tags?: string[]; capabilities?: string[]; protocolVersion?: MCPProtocolVersion; includeDeprecated?: boolean; permissions?: string[]; } /** * Enhanced Tool registry implementation with capability negotiation */ export class ToolRegistry extends EventEmitter { private tools = new Map<string, MCPTool>(); private capabilities = new Map<string, ToolCapability>(); private metrics = new Map<string, ToolMetrics>(); private categories = new Set<string>(); private tags = new Set<string>(); constructor(private logger: ILogger) { super(); } /** * Registers a new tool with enhanced capability information */ register(tool: MCPTool, capability?: ToolCapability): void { if (this.tools.has(tool.name)) { throw new MCPError(`Tool already registered: ${tool.name}`); } // Validate tool schema this.validateTool(tool); // Register tool this.tools.set(tool.name, tool); // Register capability if provided if (capability) { this.registerCapability(tool.name, capability); } else { // Create default capability const defaultCapability: ToolCapability = { name: tool.name, version: '1.0.0', description: tool.description, category: this.extractCategory(tool.name), tags: this.extractTags(tool), supportedProtocolVersions: [{ major: 2024, minor: 11, patch: 5 }], }; this.registerCapability(tool.name, defaultCapability); } // Initialize metrics this.metrics.set(tool.name, { name: tool.name, totalInvocations: 0, successfulInvocations: 0, failedInvocations: 0, averageExecutionTime: 0, totalExecutionTime: 0, }); this.logger.debug('Tool registered', { name: tool.name }); this.emit('toolRegistered', { name: tool.name, capability }); } /** * Unregisters a tool */ unregister(name: string): void { if (!this.tools.has(name)) { throw new MCPError(`Tool not found: ${name}`); } this.tools.delete(name); this.logger.debug('Tool unregistered', { name }); } /** * Gets a tool by name */ getTool(name: string): MCPTool | undefined { return this.tools.get(name); } /** * Lists all registered tools */ listTools(): Array<{ name: string; description: string }> { return Array.from(this.tools.values()).map((tool) => ({ name: tool.name, description: tool.description, })); } /** * Gets the number of registered tools */ getToolCount(): number { return this.tools.size; } /** * Executes a tool with metrics tracking */ async executeTool(name: string, input: unknown, context?: any): Promise<unknown> { const tool = this.tools.get(name); if (!tool) { throw new MCPError(`Tool not found: ${name}`); } const startTime = Date.now(); const metrics = this.metrics.get(name); this.logger.debug('Executing tool', { name, input }); try { // Validate input against schema this.validateInput(tool, input); // Check tool capabilities and permissions await this.checkToolCapabilities(name, context); // Execute tool handler const result = await tool.handler(input, context); // Update success metrics if (metrics) { const executionTime = Date.now() - startTime; metrics.totalInvocations++; metrics.successfulInvocations++; metrics.totalExecutionTime += executionTime; metrics.averageExecutionTime = metrics.totalExecutionTime / metrics.totalInvocations; metrics.lastInvoked = new Date(); } this.logger.debug('Tool executed successfully', { name, executionTime: Date.now() - startTime, }); this.emit('toolExecuted', { name, success: true, executionTime: Date.now() - startTime }); return result; } catch (error) { // Update failure metrics if (metrics) { const executionTime = Date.now() - startTime; metrics.totalInvocations++; metrics.failedInvocations++; metrics.totalExecutionTime += executionTime; metrics.averageExecutionTime = metrics.totalExecutionTime / metrics.totalInvocations; metrics.lastInvoked = new Date(); } this.logger.error('Tool execution failed', { name, error, executionTime: Date.now() - startTime, }); this.emit('toolExecuted', { name, success: false, error, executionTime: Date.now() - startTime, }); throw error; } } /** * Validates tool definition */ private validateTool(tool: MCPTool): void { if (!tool.name || typeof tool.name !== 'string') { throw new MCPError('Tool name must be a non-empty string'); } if (!tool.description || typeof tool.description !== 'string') { throw new MCPError('Tool description must be a non-empty string'); } if (typeof tool.handler !== 'function') { throw new MCPError('Tool handler must be a function'); } if (!tool.inputSchema || typeof tool.inputSchema !== 'object') { throw new MCPError('Tool inputSchema must be an object'); } // Validate tool name format (namespace/name) if (!tool.name.includes('/')) { throw new MCPError('Tool name must be in format: namespace/name'); } } /** * Validates input against tool schema */ private validateInput(tool: MCPTool, input: unknown): void { // Simple validation - in production, use a JSON Schema validator const schema = tool.inputSchema as any; if (schema.type === 'object' && schema.properties) { if (typeof input !== 'object' || input === null) { throw new MCPError('Input must be an object'); } const inputObj = input as Record<string, unknown>; // Check required properties if (schema.required && Array.isArray(schema.required)) { for (const prop of schema.required) { if (!(prop in inputObj)) { throw new MCPError(`Missing required property: ${prop}`); } } } // Check property types for (const [prop, propSchema] of Object.entries(schema.properties)) { if (prop in inputObj) { const value = inputObj[prop]; const expectedType = (propSchema as any).type; if (expectedType && !this.checkType(value, expectedType)) { throw new MCPError(`Invalid type for property ${prop}: expected ${expectedType}`); } } } } } /** * Checks if a value matches a JSON Schema type */ private checkType(value: unknown, type: string): boolean { switch (type) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number'; case 'boolean': return typeof value === 'boolean'; case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value); case 'array': return Array.isArray(value); case 'null': return value === null; default: return true; } } /** * Register tool capability information */ private registerCapability(toolName: string, capability: ToolCapability): void { this.capabilities.set(toolName, capability); this.categories.add(capability.category); capability.tags.forEach((tag) => this.tags.add(tag)); } /** * Extract category from tool name */ private extractCategory(toolName: string): string { const parts = toolName.split('/'); return parts.length > 1 ? parts[0] : 'general'; } /** * Extract tags from tool definition */ private extractTags(tool: MCPTool): string[] { const tags: string[] = []; // Extract from description if (tool.description.toLowerCase().includes('file')) tags.push('filesystem'); if (tool.description.toLowerCase().includes('search')) tags.push('search'); if (tool.description.toLowerCase().includes('memory')) tags.push('memory'); if (tool.description.toLowerCase().includes('swarm')) tags.push('swarm'); if (tool.description.toLowerCase().includes('task')) tags.push('orchestration'); return tags.length > 0 ? tags : ['general']; } /** * Check tool capabilities and permissions */ private async checkToolCapabilities(toolName: string, context?: any): Promise<void> { const capability = this.capabilities.get(toolName); if (!capability) { return; // No capability checks needed } // Check if tool is deprecated if (capability.deprecated) { this.logger.warn('Using deprecated tool', { name: toolName, message: capability.deprecationMessage, }); } // Check required permissions if (capability.requiredPermissions && context?.permissions) { const hasAllPermissions = capability.requiredPermissions.every((permission) => context.permissions.includes(permission), ); if (!hasAllPermissions) { throw new MCPError( `Insufficient permissions for tool ${toolName}. Required: ${capability.requiredPermissions.join(', ')}`, ); } } // Check protocol version compatibility if (context?.protocolVersion) { const isCompatible = capability.supportedProtocolVersions.some((version) => this.isProtocolVersionCompatible(context.protocolVersion, version), ); if (!isCompatible) { throw new MCPError( `Tool ${toolName} is not compatible with protocol version ${context.protocolVersion.major}.${context.protocolVersion.minor}.${context.protocolVersion.patch}`, ); } } } /** * Check protocol version compatibility */ private isProtocolVersionCompatible( client: MCPProtocolVersion, supported: MCPProtocolVersion, ): boolean { if (client.major !== supported.major) { return false; } if (client.minor > supported.minor) { return false; } return true; } /** * Discover tools based on query criteria */ discoverTools( query: ToolDiscoveryQuery = {}, ): Array<{ tool: MCPTool; capability: ToolCapability }> { const results: Array<{ tool: MCPTool; capability: ToolCapability }> = []; for (const [name, tool] of this.tools) { const capability = this.capabilities.get(name); if (!capability) continue; // Filter by category if (query.category && capability.category !== query.category) { continue; } // Filter by tags if (query.tags && !query.tags.some((tag) => capability.tags.includes(tag))) { continue; } // Filter by capabilities if (query.capabilities && !query.capabilities.every((cap) => capability.tags.includes(cap))) { continue; } // Filter by protocol version if (query.protocolVersion) { const isCompatible = capability.supportedProtocolVersions.some((version) => this.isProtocolVersionCompatible(query.protocolVersion!, version), ); if (!isCompatible) continue; } // Filter deprecated tools if (!query.includeDeprecated && capability.deprecated) { continue; } // Filter by permissions if (query.permissions && capability.requiredPermissions) { const hasAllPermissions = capability.requiredPermissions.every((permission) => query.permissions!.includes(permission), ); if (!hasAllPermissions) continue; } results.push({ tool, capability }); } return results; } /** * Get tool capability information */ getToolCapability(name: string): ToolCapability | undefined { return this.capabilities.get(name); } /** * Get tool metrics */ getToolMetrics(name?: string): ToolMetrics | ToolMetrics[] { if (name) { const metrics = this.metrics.get(name); if (!metrics) { throw new MCPError(`Metrics not found for tool: ${name}`); } return metrics; } return Array.from(this.metrics.values()); } /** * Get all available categories */ getCategories(): string[] { return Array.from(this.categories); } /** * Get all available tags */ getTags(): string[] { return Array.from(this.tags); } /** * Reset metrics for a tool or all tools */ resetMetrics(toolName?: string): void { if (toolName) { const metrics = this.metrics.get(toolName); if (metrics) { Object.assign(metrics, { totalInvocations: 0, successfulInvocations: 0, failedInvocations: 0, averageExecutionTime: 0, totalExecutionTime: 0, lastInvoked: undefined, }); } } else { for (const metrics of this.metrics.values()) { Object.assign(metrics, { totalInvocations: 0, successfulInvocations: 0, failedInvocations: 0, averageExecutionTime: 0, totalExecutionTime: 0, lastInvoked: undefined, }); } } this.emit('metricsReset', { toolName }); } /** * Get comprehensive registry statistics */ getRegistryStats(): { totalTools: number; toolsByCategory: Record<string, number>; toolsByTag: Record<string, number>; totalInvocations: number; successRate: number; averageExecutionTime: number; } { const stats = { totalTools: this.tools.size, toolsByCategory: {} as Record<string, number>, toolsByTag: {} as Record<string, number>, totalInvocations: 0, successRate: 0, averageExecutionTime: 0, }; // Count by category for (const capability of this.capabilities.values()) { stats.toolsByCategory[capability.category] = (stats.toolsByCategory[capability.category] || 0) + 1; for (const tag of capability.tags) { stats.toolsByTag[tag] = (stats.toolsByTag[tag] || 0) + 1; } } // Calculate execution stats let totalExecutionTime = 0; let totalSuccessful = 0; for (const metrics of this.metrics.values()) { stats.totalInvocations += metrics.totalInvocations; totalSuccessful += metrics.successfulInvocations; totalExecutionTime += metrics.totalExecutionTime; } if (stats.totalInvocations > 0) { stats.successRate = (totalSuccessful / stats.totalInvocations) * 100; stats.averageExecutionTime = totalExecutionTime / stats.totalInvocations; } return stats; } }