mcp-ai-agent-guidelines
Version:
A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices
232 lines • 7.8 kB
JavaScript
/**
* Tool Registry for Agent-to-Agent (A2A) Orchestration
*
* Provides centralized tool management with:
* - Tool registration and discovery
* - Input/output schema validation
* - Invocation permission management (allowlist)
* - Tool capability matrix for orchestration planning
*/
import { ToolInvocationNotAllowedError, ToolNotFoundError, } from "./a2a-errors.js";
import { logger } from "./logger.js";
/**
* Tool Registry singleton for managing tool registration and invocation
*/
export class ToolRegistry {
tools = new Map();
handlers = new Map();
activeCalls = new Map();
/**
* Register a tool with its handler
*
* @param tool - Tool descriptor
* @param handler - Tool handler function
* @throws Error if tool name is already registered
*/
register(tool, handler) {
if (this.tools.has(tool.name)) {
throw new Error(`Tool '${tool.name}' is already registered`);
}
this.tools.set(tool.name, tool);
this.handlers.set(tool.name, handler);
this.activeCalls.set(tool.name, 0);
logger.debug(`Registered tool: ${tool.name}`, {
canInvoke: tool.canInvoke,
tags: tool.tags,
});
}
/**
* Check if a tool is registered
*
* @param toolName - Tool name to check
* @returns true if tool is registered
*/
has(toolName) {
return this.tools.has(toolName);
}
/**
* Get tool descriptor
*
* @param toolName - Tool name
* @returns Tool descriptor
* @throws ToolNotFoundError if tool not found
*/
getDescriptor(toolName) {
const tool = this.tools.get(toolName);
if (!tool) {
throw new ToolNotFoundError(toolName);
}
return tool;
}
/**
* Invoke a tool with the given arguments and context
*
* @param toolName - Tool to invoke
* @param args - Tool arguments
* @param context - A2A context (optional for top-level invocations)
* @returns Tool result
* @throws ToolNotFoundError if tool not found
* @throws ToolInvocationNotAllowedError if caller not allowed to invoke tool
* @throws Error if concurrency limit exceeded
*/
async invoke(toolName, args, context) {
const tool = this.getDescriptor(toolName);
const handler = this.handlers.get(toolName);
if (!handler) {
throw new ToolNotFoundError(toolName);
}
// Check invocation permission if this is a nested call
if (context?.parentToolName) {
this.checkInvocationPermission(context.parentToolName, toolName);
}
// Check concurrency limit
if (tool.maxConcurrency) {
const activeCalls = this.activeCalls.get(toolName) || 0;
if (activeCalls >= tool.maxConcurrency) {
throw new Error(`Tool '${toolName}' has reached maximum concurrency (${tool.maxConcurrency})`);
}
}
// Validate input
try {
tool.inputSchema.parse(args);
}
catch (error) {
logger.error(`Input validation failed for tool '${toolName}'`, {
error: error instanceof Error ? error.message : String(error),
});
return {
success: false,
error: `Input validation failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
// Track active calls
this.activeCalls.set(toolName, (this.activeCalls.get(toolName) || 0) + 1);
const startTime = Date.now();
try {
const result = await handler(args, context);
// Validate output if schema provided
if (tool.outputSchema && result.data) {
try {
tool.outputSchema.parse(result.data);
}
catch (error) {
logger.warn(`Output validation failed for tool '${toolName}'`, {
error: error instanceof Error ? error.message : String(error),
});
}
}
return {
...result,
metadata: {
toolName,
durationMs: Date.now() - startTime,
timestamp: new Date(),
},
};
}
finally {
// Decrement active calls
this.activeCalls.set(toolName, (this.activeCalls.get(toolName) || 1) - 1);
}
}
/**
* Check if a tool is allowed to invoke another tool
*
* @param callerTool - Calling tool name
* @param targetTool - Target tool name
* @throws ToolInvocationNotAllowedError if not allowed
*/
checkInvocationPermission(callerTool, targetTool) {
const caller = this.getDescriptor(callerTool);
// Check if target tool is in the allowlist
// Empty array means no tools can be invoked
// ['*'] means all tools can be invoked
if (caller.canInvoke.length === 0 ||
(!caller.canInvoke.includes("*") &&
!caller.canInvoke.includes(targetTool))) {
throw new ToolInvocationNotAllowedError(callerTool, targetTool, {
allowedTools: caller.canInvoke,
});
}
}
/**
* List all registered tools with optional filtering
*
* @param filter - Optional filter criteria
* @returns Array of tool descriptors
*/
listTools(filter) {
let tools = Array.from(this.tools.values());
if (filter?.tags && filter.tags.length > 0) {
tools = tools.filter((tool) => tool.tags?.some((tag) => filter.tags?.includes(tag)));
}
if (filter?.namePattern) {
const regex = new RegExp(filter.namePattern);
tools = tools.filter((tool) => regex.test(tool.name));
}
if (filter?.canInvoke) {
tools = tools.filter((tool) => tool.canInvoke.includes("*") ||
tool.canInvoke.includes(filter.canInvoke));
}
return tools;
}
/**
* Get capability matrix showing which tools can invoke which
*
* @returns Map of tool name to array of invokeable tools
*/
getCapabilityMatrix() {
const matrix = new Map();
for (const [name, tool] of this.tools.entries()) {
// Expand '*' to all tool names
if (tool.canInvoke.includes("*")) {
matrix.set(name, Array.from(this.tools.keys()));
}
else {
matrix.set(name, tool.canInvoke);
}
}
return matrix;
}
/**
* Get tools that can be invoked by a specific tool
*
* @param toolName - Tool name
* @returns Array of invokeable tool names
*/
getInvokeableTools(toolName) {
const tool = this.getDescriptor(toolName);
if (tool.canInvoke.includes("*")) {
return Array.from(this.tools.keys());
}
return tool.canInvoke;
}
/**
* Get current active invocations count for a tool
*
* @param toolName - Tool name
* @returns Number of active invocations
*/
getActiveInvocations(toolName) {
return this.activeCalls.get(toolName) || 0;
}
/**
* Clear all registered tools (primarily for testing)
*/
clear() {
this.tools.clear();
this.handlers.clear();
this.activeCalls.clear();
}
/**
* Get total number of registered tools
*/
get size() {
return this.tools.size;
}
}
/**
* Singleton instance of the tool registry
*/
export const toolRegistry = new ToolRegistry();
//# sourceMappingURL=tool-registry.js.map