UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

335 lines (334 loc) 11.7 kB
/** * Tool Integration with Elicitation Context * * Provides integration between MCP tools and the elicitation protocol, * enabling tools to request interactive user input during execution. * * @module mcp/toolIntegration * @since 8.39.0 */ import { ElicitationManager } from "./elicitation/elicitationManager.js"; import { ErrorFactory, withTimeout } from "../utils/errorHandling.js"; import { logger } from "../utils/logger.js"; /** * Create elicitation context for a tool */ export function createElicitationContext(toolName, serverId, manager) { return { confirm: async (message, options) => { return manager.confirm(message, { toolName, serverId, confirmLabel: options?.confirmLabel, cancelLabel: options?.cancelLabel, }); }, getText: async (message, options) => { return manager.getText(message, { toolName, placeholder: options?.placeholder, defaultValue: options?.defaultValue, }); }, select: async (message, options) => { return manager.select(message, options, { toolName }); }, multiSelect: async (message, options) => { return manager.multiSelect(message, options, { toolName }); }, form: async (message, fields) => { return manager.form(message, fields, { toolName, serverId }); }, request: async (elicitation) => { return manager.request({ ...elicitation, toolName, serverId, }); }, }; } /** * Wrap a tool with elicitation support */ export function wrapToolWithElicitation(tool, options = {}) { const { elicitationManager, autoConfirmDestructive = false, elicitationTimeout = 60000, enableLogging = true, } = options; const manager = elicitationManager ?? new ElicitationManager({ defaultTimeout: elicitationTimeout }); return { ...tool, execute: async (params, context) => { const config = context?.config; const serverId = config?.serverId; // Create elicitation context const elicitationContext = createElicitationContext(tool.name, serverId, manager); // Check if tool requires confirmation const needsConfirmation = tool.annotations?.requiresConfirmation || tool.annotations?.destructiveHint; if (needsConfirmation && !autoConfirmDestructive) { if (enableLogging) { logger.debug(`[ToolIntegration] Tool '${tool.name}' requires confirmation`); } const confirmed = await elicitationContext.confirm(`This operation (${tool.name}) ${tool.annotations?.destructiveHint ? "is destructive and " : ""}requires confirmation. Do you want to proceed?`, { confirmLabel: "Yes, proceed", cancelLabel: "Cancel", }); if (!confirmed) { return { success: false, error: "Operation cancelled by user", metadata: { toolName: tool.name, cancelled: true, }, }; } } // Create enhanced context const enhancedContext = { ...context, elicitation: elicitationContext, toolMeta: { name: tool.name, serverId, annotations: tool.annotations, }, }; // Execute the tool return tool.execute(params, enhancedContext); }, }; } /** * Batch wrap tools with elicitation support */ export function wrapToolsWithElicitation(tools, options = {}) { return tools.map((tool) => wrapToolWithElicitation(tool, options)); } /** * Create a middleware chain for tool execution */ export function createToolMiddlewareChain(middlewares) { return async (tool, params, context, next) => { const dispatch = async (index) => { if (index >= middlewares.length) { return next(); } const middleware = middlewares[index]; return middleware(tool, params, context, () => dispatch(index + 1)); }; return dispatch(0); }; } /** * Built-in middleware: Logging */ export const loggingMiddleware = async (tool, params, context, next) => { const startTime = Date.now(); logger.debug(`[ToolMiddleware] Executing tool '${tool.name}'`); try { const result = await next(); const duration = Date.now() - startTime; logger.debug(`[ToolMiddleware] Tool '${tool.name}' completed in ${duration}ms`); return result; } catch (error) { const duration = Date.now() - startTime; logger.error(`[ToolMiddleware] Tool '${tool.name}' failed after ${duration}ms:`, error); throw error; } }; /** * Built-in middleware: Confirmation for destructive operations */ export const confirmationMiddleware = async (tool, params, context, next) => { // Skip confirmation if elicitation context is not available if (!context.elicitation?.confirm) { return next(); } if (tool.annotations?.destructiveHint || tool.annotations?.requiresConfirmation) { const confirmed = await context.elicitation.confirm(`Confirm execution of ${tool.name}?`); if (!confirmed) { return { success: false, error: "Operation cancelled by user", metadata: { cancelled: true }, }; } } return next(); }; /** * Built-in middleware: Timeout */ export function createTimeoutMiddleware(timeoutMs) { return async (tool, params, context, next) => { return withTimeout(next(), timeoutMs, ErrorFactory.toolTimeout(tool.name, timeoutMs)); }; } /** * Built-in middleware: Retry */ export function createRetryMiddleware(maxRetries, delayMs = 1000) { return async (tool, params, context, next) => { // Only retry idempotent or read-only tools const canRetry = tool.annotations?.idempotentHint || tool.annotations?.readOnlyHint; if (!canRetry) { return next(); } const retries = Math.max(0, maxRetries); let lastError; for (let attempt = 0; attempt <= retries; attempt++) { try { return await next(); } catch (error) { lastError = error instanceof Error ? error : ErrorFactory.toolExecutionFailed(tool.name, new Error(String(error))); if (attempt < retries) { logger.warn(`[ToolMiddleware] Tool '${tool.name}' failed, retrying (${attempt + 1}/${retries})`); await new Promise((resolve) => setTimeout(resolve, delayMs * (attempt + 1))); } } } throw (lastError ?? ErrorFactory.toolExecutionFailed(tool.name, new Error("Retry middleware exhausted without captured error"))); }; } /** * Built-in middleware: Parameter validation */ export const validationMiddleware = async (tool, params, context, next) => { if (!tool.inputSchema) { return next(); } const schema = tool.inputSchema; const required = schema.required ?? []; const properties = schema.properties ?? {}; // Validate required parameters const paramObj = (params ?? {}); const missing = []; for (const req of required) { if (paramObj[req] === undefined) { missing.push(req); } } if (missing.length > 0) { // Try to elicit missing parameters const formFields = missing.map((name) => { const prop = properties[name]; return { name, label: name, type: prop?.type ?? "text", required: true, description: prop?.description, }; }); const formResult = await context.elicitation.form(`Missing required parameters for ${tool.name}`, formFields); if (!formResult) { return { success: false, error: `Missing required parameters: ${missing.join(", ")}`, metadata: { missingParams: missing }, }; } // Merge elicited values with params Object.assign(paramObj, formResult); } return next(); }; /** * Tool Integration Manager * * Manages tool execution with middleware and elicitation support. */ export class ToolIntegrationManager { elicitationManager; middlewares = []; wrappedTools = new Map(); constructor(elicitationManager) { this.elicitationManager = elicitationManager ?? new ElicitationManager(); } /** * Set the elicitation handler */ setElicitationHandler(handler) { this.elicitationManager.setHandler(handler); } /** * Add middleware */ use(middleware) { this.middlewares.push(middleware); return this; } /** * Register a tool with integration */ registerTool(tool) { const wrapped = wrapToolWithElicitation(tool, { elicitationManager: this.elicitationManager, }); this.wrappedTools.set(tool.name, wrapped); return wrapped; } /** * Execute a tool with full middleware chain */ async executeTool(toolName, params, context) { const tool = this.wrappedTools.get(toolName); if (!tool) { throw ErrorFactory.toolNotFound(toolName, Array.from(this.wrappedTools.keys())); } // Normalize params to a mutable object so middleware (e.g. validationMiddleware) // can merge elicited values and have them forwarded to tool.execute const normalizedParams = params && typeof params === "object" ? params : {}; const config = context?.config; const serverId = config?.serverId; // Create enhanced context const elicitationContext = createElicitationContext(toolName, serverId, this.elicitationManager); const enhancedContext = { ...context, elicitation: elicitationContext, toolMeta: { name: toolName, serverId, annotations: tool.annotations, }, }; // Create middleware chain if (this.middlewares.length === 0) { return tool.execute(normalizedParams, enhancedContext); } const chain = createToolMiddlewareChain(this.middlewares); return chain(tool, normalizedParams, enhancedContext, () => tool.execute(normalizedParams, enhancedContext)); } /** * Get registered tool */ getTool(name) { return this.wrappedTools.get(name); } /** * Get all registered tools */ getAllTools() { return Array.from(this.wrappedTools.values()); } /** * Get the elicitation manager */ getElicitationManager() { return this.elicitationManager; } } /** * Module-level singleton ToolIntegrationManager. * Note: The default ElicitationManager has no handler set. Consumers must call * setElicitationHandler() before using elicitation methods. */ export const globalToolIntegrationManager = new ToolIntegrationManager();