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

377 lines (376 loc) 11.8 kB
/** * Elicitation Manager * * Manager for handling elicitation requests during tool execution. * Enables MCP tools to request interactive user input mid-execution. * * @module mcp/elicitation/elicitationManager * @since 8.39.0 */ import { EventEmitter } from "events"; import { randomUUID } from "crypto"; import { logger } from "../../utils/logger.js"; /** * Manager for handling elicitation requests during tool execution * * The elicitation protocol allows MCP tools to request interactive user input * mid-execution. This is useful for: * - Confirming destructive operations * - Requesting missing information * - Getting user preferences * - Handling authentication challenges * * @example * ```typescript * const elicitationManager = new ElicitationManager({ * defaultTimeout: 60000, * handler: async (request) => { * // Implement UI prompt based on request type * if (request.type === "confirmation") { * const confirmed = await showConfirmDialog(request.message); * return { * requestId: request.id, * responded: true, * value: confirmed, * timestamp: Date.now(), * }; * } * // Handle other types... * }, * }); * * // Use in a tool * const response = await elicitationManager.request({ * type: "confirmation", * message: "Are you sure you want to delete this file?", * toolName: "deleteFile", * }); * * if (response.value === true) { * // Proceed with deletion * } * ``` */ export class ElicitationManager extends EventEmitter { config; pendingRequests = new Map(); constructor(config = {}) { super(); this.config = { defaultTimeout: config.defaultTimeout ?? 60000, enabled: config.enabled ?? true, handler: config.handler ?? this.defaultHandler.bind(this), fallbackBehavior: config.fallbackBehavior ?? "timeout", }; } /** * Set the elicitation handler */ setHandler(handler) { this.config.handler = handler; } /** * Enable or disable elicitation */ setEnabled(enabled) { this.config.enabled = enabled; if (!enabled) { // Resolve all pending requests with timeout/default for (const [requestId, pending] of this.pendingRequests) { if (pending.settled) { continue; } pending.settled = true; this.handleDisabled(pending.request, pending.resolve); if (pending.timer) { clearTimeout(pending.timer); } this.pendingRequests.delete(requestId); } } } /** * Check if elicitation is enabled */ isEnabled() { return this.config.enabled; } /** * Request user input */ async request(elicitation) { const request = { ...elicitation, id: elicitation.id ?? randomUUID(), }; // If disabled, handle according to fallback behavior if (!this.config.enabled) { return this.handleDisabledRequest(request); } const timeout = request.timeout ?? this.config.defaultTimeout; return new Promise((resolve) => { // Set up timeout const timer = setTimeout(() => { const pending = this.pendingRequests.get(request.id); if (!pending || pending.settled) { return; } pending.settled = true; this.handleTimeout(request, resolve); }, timeout); // Store pending request with shared settled flag this.pendingRequests.set(request.id, { request, resolve, timer, settled: false, }); // Emit request event this.emit("elicitationRequested", request); // Call handler (wrapped to catch synchronous throws) Promise.resolve() .then(() => this.config.handler(request)) .then((response) => { const pending = this.pendingRequests.get(request.id); if (!pending || pending.settled) { return; } pending.settled = true; clearTimeout(timer); this.pendingRequests.delete(request.id); this.emit("elicitationResponded", response); resolve(response); }) .catch((error) => { const pending = this.pendingRequests.get(request.id); if (!pending || pending.settled) { return; } pending.settled = true; clearTimeout(timer); this.pendingRequests.delete(request.id); const errorResponse = { requestId: request.id, responded: false, error: error instanceof Error ? error.message : String(error), timestamp: Date.now(), }; this.emit("elicitationError", { request, error }); resolve(errorResponse); }); }); } /** * Convenience method for confirmation requests */ async confirm(message, options) { const request = { type: "confirmation", message, toolName: options?.toolName ?? "unknown", serverId: options?.serverId, confirmLabel: options?.confirmLabel, cancelLabel: options?.cancelLabel, timeout: options?.timeout, }; const response = await this.request(request); return response.value === true; } /** * Convenience method for text input */ async getText(message, options) { const request = { type: "text", message, toolName: options?.toolName ?? "unknown", placeholder: options?.placeholder, defaultValue: options?.defaultValue, timeout: options?.timeout, }; const response = await this.request(request); return response.value; } /** * Convenience method for selection */ async select(message, options, config) { const request = { type: "select", message, toolName: config?.toolName ?? "unknown", options: options, timeout: config?.timeout, }; const response = await this.request(request); return response.value; } /** * Convenience method for multiple selection */ async multiSelect(message, options, config) { const request = { type: "multiselect", message, toolName: config?.toolName ?? "unknown", options: options, timeout: config?.timeout, minSelections: config?.minSelections, maxSelections: config?.maxSelections, }; const response = await this.request(request); return response.value; } /** * Convenience method for form input */ async form(message, fields, config) { const request = { type: "form", message, toolName: config?.toolName ?? "unknown", serverId: config?.serverId, fields, submitLabel: config?.submitLabel, timeout: config?.timeout, }; const response = await this.request(request); return response.value; } /** * Convenience method for secret input */ async getSecret(message, options) { const request = { type: "secret", message, toolName: options?.toolName ?? "unknown", hint: options?.hint, timeout: options?.timeout, }; const response = await this.request(request); return response.value; } /** * Cancel a pending request */ cancel(requestId, reason) { const pending = this.pendingRequests.get(requestId); if (pending) { if (pending.settled) { return; } pending.settled = true; if (pending.timer) { clearTimeout(pending.timer); } const response = { requestId, responded: false, cancelled: true, error: reason, timestamp: Date.now(), }; pending.resolve(response); this.pendingRequests.delete(requestId); this.emit("elicitationCancelled", { requestId, reason }); } } /** * Default handler when none is provided */ async defaultHandler(request) { logger.warn(`[ElicitationManager] No handler for elicitation request: ${request.id}`); // If there's a default value, use it if (request.defaultValue !== undefined) { return { requestId: request.id, responded: true, value: request.defaultValue, timestamp: Date.now(), }; } // Otherwise, return not responded return { requestId: request.id, responded: false, error: "No elicitation handler configured", timestamp: Date.now(), }; } /** * Handle timeout */ handleTimeout(request, resolve) { this.pendingRequests.delete(request.id); const response = { requestId: request.id, responded: false, timedOut: true, value: request.defaultValue, timestamp: Date.now(), }; this.emit("elicitationTimeout", { request }); resolve(response); } /** * Handle disabled elicitation */ handleDisabled(request, resolve) { resolve(this.handleDisabledRequest(request)); } /** * Handle disabled request based on fallback behavior */ handleDisabledRequest(request) { switch (this.config.fallbackBehavior) { case "default": return { requestId: request.id, responded: request.defaultValue !== undefined, value: request.defaultValue, timestamp: Date.now(), }; case "error": return { requestId: request.id, responded: false, error: "Elicitation is disabled", timestamp: Date.now(), }; case "timeout": default: return { requestId: request.id, responded: false, timedOut: true, value: request.defaultValue, timestamp: Date.now(), }; } } /** * Get pending request count */ getPendingCount() { return this.pendingRequests.size; } /** * Get all pending requests */ getPendingRequests() { return Array.from(this.pendingRequests.values()).map((p) => p.request); } /** * Clear all pending requests */ clearPending(reason) { for (const [requestId] of this.pendingRequests) { this.cancel(requestId, reason ?? "Cleared"); } } } /** * Global elicitation manager instance */ export const globalElicitationManager = new ElicitationManager();