@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
JavaScript
/**
* 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();