UNPKG

@web-interact-mcp/client

Version:

A production-ready TypeScript library that transforms web applications into MCP (Model Context Protocol) servers with robust two-way communication via SignalR

381 lines 14.2 kB
"use strict"; /** * @fileoverview Tool Registry for managing Web Interact MCP tool configurations * @description Manages the collection of available tools with validation and discovery capabilities * @version 1.0.0 * @author Vijay Nirmal */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ToolRegistry = void 0; /** * Manages the collection of all available Tools. * Provides functionality to load configurations from various sources and discover tools based on context. */ class ToolRegistry { /** * Creates a new ToolRegistry instance * @param enableLogging - Whether to enable console logging (default: false) */ constructor(enableLogging = false) { this.tools = new Map(); this.cachedToolsJson = '[]'; this.logger = enableLogging ? console : this.createSilentLogger(); } /** * Loads tool configurations from a source * @param source - Array of configurations or URL to a JSON file * @throws {Error} When source is invalid or tools fail validation */ async loadTools(source) { let configurations = []; try { if (Array.isArray(source)) { configurations = source; this.logger.log(`Loading ${configurations.length} tools from array`); } else if (typeof source === 'string') { this.logger.log(`Loading tools from URL: ${source}`); configurations = await this.fetchToolsFromUrl(source); } else { throw new Error('Invalid source type. Expected array or string (URL).'); } const loadResults = this.validateAndLoadConfigurations(configurations); this.logger.log(`Successfully loaded ${loadResults.success} of ${configurations.length} tools`); if (loadResults.errors.length > 0) { this.logger.warn(`${loadResults.errors.length} tools failed validation:`, loadResults.errors); } } catch (error) { this.logger.error('Error loading tools:', error); throw error; } } /** * Gets a specific tool by its ID * @param toolId - The ID of the tool to retrieve * @returns The tool configuration or undefined if not found */ getToolById(toolId) { return this.tools.get(toolId); } /** * Returns a map of all tools available for a given page URL * @param url - The URL of the current page * @returns Map of available tools for the page */ getAvailableTools(url) { const availableTools = new Map(); for (const [toolId, tool] of this.tools) { if (this.isToolAvailableForPage(tool, url)) { availableTools.set(toolId, tool); } } return availableTools; } /** * Returns a map of all global tools (tools without pageMatcher) * @returns Map of all global tools */ getGlobalTools() { const globalTools = new Map(); for (const [toolId, tool] of this.tools) { if (!tool.pageMatcher) { globalTools.set(toolId, tool); } } return globalTools; } /** * Returns all available tools * @returns Map of all tools */ getAllTools() { return new Map(this.tools); } /** * Gets tools that have parameter schemas defined * @returns Map of tools that have parameter schemas */ getToolsWithParameterSchemas() { const toolsWithSchemas = new Map(); for (const [toolId, tool] of this.tools) { if (tool.parameterSchema && Object.keys(tool.parameterSchema.parameters).length > 0) { toolsWithSchemas.set(toolId, tool); } } return toolsWithSchemas; } /** * Gets parameter schema for a specific tool * @param toolId - The ID of the tool * @returns The parameter schema or undefined if not found */ getParameterSchema(toolId) { const tool = this.tools.get(toolId); return tool?.parameterSchema; } /** * Gets all tools as a JSON string for MCP server communication * @returns JSON string representation of all tool configurations */ getToolsAsJson() { return this.cachedToolsJson; } /** * Gets a summary of all tools with their parameter information for MCP server discovery * @returns Array of tool summaries including parameter information */ getToolsSummaryForMCP() { const summary = []; for (const [, tool] of this.tools) { const hasParameters = tool.parameterSchema !== undefined; const parameterCount = hasParameters && tool.parameterSchema ? Object.keys(tool.parameterSchema.parameters).length : 0; const toolSummary = { toolId: tool.toolId, title: tool.title, description: tool.description, mode: tool.mode, hasParameters, parameterCount }; if (tool.parameterSchema) { toolSummary.parameterSchema = tool.parameterSchema; } if (tool.destructive !== undefined) { toolSummary.destructive = tool.destructive; } if (tool.idempotent !== undefined) { toolSummary.idempotent = tool.idempotent; } if (tool.openWorld !== undefined) { toolSummary.openWorld = tool.openWorld; } if (tool.readOnly !== undefined) { toolSummary.readOnly = tool.readOnly; } summary.push(toolSummary); } return summary; } /** * Gets the total number of registered tools * @returns The count of registered tools */ getToolCount() { return this.tools.size; } /** * Checks if a tool exists by ID * @param toolId - The ID of the tool to check * @returns True if the tool exists, false otherwise */ hasToolId(toolId) { return this.tools.has(toolId); } /** * Removes a tool from the registry * @param toolId - The ID of the tool to remove * @returns True if the tool was removed, false if it didn't exist */ removeTool(toolId) { return this.tools.delete(toolId); } /** * Clears all tools from the registry */ clearTools() { this.tools.clear(); this.cachedToolsJson = '[]'; } /** * Fetches tools from a URL * @private */ async fetchToolsFromUrl(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch tools from ${url}: ${response.statusText}`); } const text = await response.text(); try { const configurations = JSON.parse(text); if (!Array.isArray(configurations)) { throw new Error('Expected JSON array of tool configurations'); } return configurations; } catch (parseError) { throw new Error(`Failed to parse JSON from ${url}: ${parseError instanceof Error ? parseError.message : String(parseError)}`); } } /** * Validates and loads multiple configurations * @private */ validateAndLoadConfigurations(configurations) { const results = { success: 0, errors: [] }; for (const config of configurations) { try { this.validateToolConfiguration(config); this.tools.set(config.toolId, config); results.success++; this.logger.log(`Successfully loaded tool: ${config.toolId}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); results.errors.push({ toolId: config.toolId || 'unknown', error: errorMessage }); this.logger.error(`Failed to validate tool: ${config.toolId}`, error); } } // Pre-calculate JSON representation for MCP server communication this.cacheToolsJson(); return results; } /** * Pre-calculates and caches the JSON representation of all tools * @private */ cacheToolsJson() { try { const toolsArray = Array.from(this.tools.values()); this.cachedToolsJson = JSON.stringify(toolsArray, null, 0); this.logger.log(`Cached JSON representation of ${toolsArray.length} tools`); } catch (error) { this.logger.error('Error caching tools JSON:', error); this.cachedToolsJson = '[]'; } } /** * Validates a tool configuration * @private * @throws {Error} When configuration is invalid */ validateToolConfiguration(config) { // Basic required fields if (!config.toolId) { throw new Error('Tool configuration must have a toolId'); } if (!config.title) { throw new Error(`Tool ${config.toolId} must have a title`); } if (!config.description) { throw new Error(`Tool ${config.toolId} must have a description`); } // Validate mode const validModes = ['normal', 'buttonless', 'silent']; if (!validModes.includes(config.mode)) { throw new Error(`Tool ${config.toolId} has invalid mode: ${config.mode}. Must be one of: ${validModes.join(', ')}`); } // Validate steps if (!Array.isArray(config.steps) || config.steps.length === 0) { throw new Error(`Tool ${config.toolId} must have at least one step`); } // Validate each step config.steps.forEach((step, index) => { this.validateToolStep(config.toolId, step, index, config.mode); }); // Validate parameter schema if present if (config.parameterSchema) { this.validateParameterSchema(config.toolId, config.parameterSchema); } } /** * Validates a single tool step * @private */ validateToolStep(toolId, step, index, mode) { const stepObj = step; if (!stepObj['targetElement'] || typeof stepObj['targetElement'] !== 'string') { throw new Error(`Tool ${toolId}, step ${index}: targetElement is required and must be a string`); } if (mode !== 'silent' && (!stepObj['content'] || typeof stepObj['content'] !== 'string')) { throw new Error(`Tool ${toolId}, step ${index}: content is required for ${mode} mode`); } if (mode === 'silent' && !stepObj['action']) { throw new Error(`Tool ${toolId}, step ${index}: action is required for silent mode`); } // Validate action if present if (stepObj['action']) { this.validateToolAction(toolId, stepObj['action'], index); } } /** * Validates a tool action * @private */ validateToolAction(toolId, action, stepIndex) { const validActionTypes = ['click', 'fillInput', 'navigate', 'selectOption', 'executeFunction']; if (!action['type'] || !validActionTypes.includes(action['type'])) { throw new Error(`Tool ${toolId}, step ${stepIndex}: invalid action type '${action['type']}'. Must be one of: ${validActionTypes.join(', ')}`); } if (!action['element'] || typeof action['element'] !== 'string') { throw new Error(`Tool ${toolId}, step ${stepIndex}: action must have a valid element selector`); } // Additional validation for executeFunction if (action['type'] === 'executeFunction') { if (!action['function'] && !action['functionName']) { throw new Error(`Tool ${toolId}, step ${stepIndex}: executeFunction action requires either 'function' or 'functionName'`); } } } /** * Validates a parameter schema * @private */ validateParameterSchema(toolId, schema) { if (!schema.parameters || typeof schema.parameters !== 'object') { throw new Error(`Tool ${toolId}: parameterSchema must have a parameters object`); } for (const [paramName, paramDef] of Object.entries(schema.parameters)) { this.validateParameterDefinition(toolId, paramName, paramDef); } } /** * Validates a parameter definition * @private */ validateParameterDefinition(toolId, paramName, paramDef) { const def = paramDef; const validTypes = ['string', 'number', 'boolean', 'array', 'object']; if (!def['type'] || !validTypes.includes(def['type'])) { throw new Error(`Tool ${toolId}, parameter '${paramName}': invalid type '${def['type']}'. Must be one of: ${validTypes.join(', ')}`); } if (!def['description'] || typeof def['description'] !== 'string') { throw new Error(`Tool ${toolId}, parameter '${paramName}': description is required and must be a string`); } } /** * Checks if a tool is available for a specific page URL * @private */ isToolAvailableForPage(tool, url) { if (!tool.pageMatcher) { return true; // No matcher means available everywhere (global tool) } if (typeof tool.pageMatcher === 'string') { return url.includes(tool.pageMatcher); } else if (tool.pageMatcher instanceof RegExp) { return tool.pageMatcher.test(url); } return false; } /** * Creates a silent logger that doesn't output anything * @private */ createSilentLogger() { const silentFunction = () => { }; return { log: silentFunction, error: silentFunction, warn: silentFunction, info: silentFunction, debug: silentFunction, }; } } exports.ToolRegistry = ToolRegistry; //# sourceMappingURL=tool-registry.js.map