jay-code
Version:
Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability
553 lines (475 loc) • 15.5 kB
text/typescript
/**
* 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;
}
}