@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 • 11.8 kB
JavaScript
/**
* 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();
//# sourceMappingURL=elicitationManager.js.map