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 11.1 kB
/** * MCP Server Base Class * * Abstract base class for creating custom MCP servers with consistent patterns * for tool registration, execution, and lifecycle management. * * Implements MCPServerBase features including: * - Tool annotation support (readOnlyHint, destructiveHint, idempotentHint) * - Lifecycle hooks (onInit, onStart, onStop) * - Event emission for tool operations * - Conversion to MCPServerInfo format * * @module mcp/mcpServerBase * @since 8.39.0 */ import { EventEmitter } from "events"; import { withTimeout } from "../utils/async/withTimeout.js"; import { ErrorFactory } from "../utils/errorHandling.js"; /** * MCPServerBase configuration */ /** * Abstract base class for MCP servers * * Provides a foundation for creating custom MCP servers with consistent * patterns for tool registration, execution, and lifecycle management. * * @example * ```typescript * class MyCustomServer extends MCPServerBase { * constructor() { * super({ * id: "my-custom-server", * name: "My Custom Server", * description: "Provides custom functionality", * category: "custom", * }); * * // Register tools in constructor or init * this.registerTool({ * name: "myTool", * description: "Does something useful", * annotations: { * readOnlyHint: true, * idempotentHint: true, * }, * execute: async (params, context) => { * return { success: true, data: "result" }; * }, * }); * } * } * ``` */ export class MCPServerBase extends EventEmitter { config; tools = new Map(); isInitialized = false; isRunning = false; constructor(config) { super(); // Apply defaults this.config = { id: config.id, name: config.name, description: config.description ?? "", version: config.version ?? "1.0.0", category: config.category ?? "custom", transport: config.transport ?? "stdio", metadata: config.metadata ?? {}, defaultTimeoutMs: config.defaultTimeoutMs ?? 30_000, defaultAnnotations: config.defaultAnnotations ?? {}, }; } /** * Initialize the server * Override in subclasses for async initialization */ async init() { if (this.isInitialized) { return; } await this.onInit(); this.isInitialized = true; this.emit("serverReady", { tools: Array.from(this.tools.keys()), }); } /** * Hook for subclass initialization * Override to perform async setup */ async onInit() { // Override in subclasses } /** * Start the server */ async start() { if (!this.isInitialized) { await this.init(); } if (this.isRunning) { return; } await this.onStart(); this.isRunning = true; } /** * Hook for subclass start logic */ async onStart() { // Override in subclasses } /** * Stop the server */ async stop(reason) { if (!this.isRunning) { return; } await this.onStop(); this.isRunning = false; this.emit("serverStopped", { reason }); } /** * Hook for subclass stop logic */ async onStop() { // Override in subclasses } /** * Register a tool with the server */ registerTool(tool) { // Validate tool this.validateTool(tool); // Merge with default annotations const mergedTool = { ...tool, annotations: { ...this.config.defaultAnnotations, ...tool.annotations, }, }; this.tools.set(tool.name, mergedTool); this.emit("toolRegistered", { toolName: tool.name, tool: mergedTool, }); return this; } /** * Register multiple tools at once */ registerTools(tools) { for (const tool of tools) { this.registerTool(tool); } return this; } /** * Validate tool configuration */ validateTool(tool) { if (!tool.name || typeof tool.name !== "string") { throw ErrorFactory.invalidConfiguration("tool.name", "Tool name is required and must be a string"); } if (tool.name.length > 64) { throw ErrorFactory.invalidConfiguration("tool.name", "Tool name must be 64 characters or less", { toolName: tool.name }); } if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(tool.name)) { throw ErrorFactory.invalidConfiguration("tool.name", "Tool name must start with a letter or underscore and contain only alphanumeric characters, underscores, and hyphens", { toolName: tool.name }); } if (!tool.description || typeof tool.description !== "string") { throw ErrorFactory.invalidConfiguration("tool.description", "Tool description is required and must be a string", { toolName: tool.name }); } if (typeof tool.execute !== "function") { throw ErrorFactory.invalidConfiguration("tool.execute", "Tool execute function is required", { toolName: tool.name }); } if (this.tools.has(tool.name)) { throw ErrorFactory.invalidConfiguration("tool.name", `Tool '${tool.name}' is already registered`, { toolName: tool.name }); } } /** * Execute a tool by name */ async executeTool(toolName, params, context) { const tool = this.tools.get(toolName); if (!tool) { return { success: false, error: `Tool '${toolName}' not found on server '${this.config.id}'`, metadata: { toolName, serverId: this.config.id, }, }; } const startTime = Date.now(); const toolTimeoutMs = this.config.defaultTimeoutMs ?? 30_000; try { const result = await withTimeout(tool.execute(params, context ?? {}), toolTimeoutMs, ErrorFactory.toolTimeout(toolName, toolTimeoutMs).message); const duration = Date.now() - startTime; this.emit("toolExecuted", { toolName, duration, success: true, }); // Ensure result conforms to ToolResult if (this.isToolResult(result)) { return { ...result, metadata: { ...result.metadata, toolName, serverId: this.config.id, executionTime: duration, }, }; } return { success: true, data: result, metadata: { toolName, serverId: this.config.id, executionTime: duration, }, }; } catch (error) { const duration = Date.now() - startTime; this.emit("toolError", { toolName, error: error instanceof Error ? error : new Error(String(error)), }); this.emit("toolExecuted", { toolName, duration, success: false, }); return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { toolName, serverId: this.config.id, executionTime: duration, }, }; } } /** * Type guard to check if result is a ToolResult */ isToolResult(result) { return (result !== null && typeof result === "object" && "success" in result && typeof result.success === "boolean"); } /** * Get all registered tools */ getTools() { return Array.from(this.tools.values()); } /** * Get a specific tool by name */ getTool(name) { return this.tools.get(name); } /** * Check if a tool exists */ hasTool(name) { return this.tools.has(name); } /** * Remove a tool */ removeTool(name) { return this.tools.delete(name); } /** * Get server info in MCPServerInfo format */ toServerInfo() { return { id: this.config.id, name: this.config.name, description: this.config.description, transport: this.config.transport, status: this.isRunning ? "connected" : "stopped", tools: this.getTools().map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, execute: (params, context) => this.executeTool(tool.name, params, context), })), metadata: { ...this.config.metadata, category: this.config.category, version: this.config.version, }, }; } /** * Get tools filtered by annotations */ getToolsByAnnotation(annotation, value) { return this.getTools().filter((tool) => { const annotationValue = tool.annotations?.[annotation]; if (Array.isArray(value) && Array.isArray(annotationValue)) { return value.some((v) => annotationValue.includes(v)); } return annotationValue === value; }); } /** * Get read-only tools */ getReadOnlyTools() { return this.getToolsByAnnotation("readOnlyHint", true); } /** * Get destructive tools */ getDestructiveTools() { return this.getToolsByAnnotation("destructiveHint", true); } /** * Get idempotent tools */ getIdempotentTools() { return this.getToolsByAnnotation("idempotentHint", true); } /** * Get tools that require confirmation */ getToolsRequiringConfirmation() { return this.getToolsByAnnotation("requiresConfirmation", true); } /** * Server identification */ get id() { return this.config.id; } get name() { return this.config.name; } get description() { return this.config.description; } get version() { return this.config.version; } get category() { return this.config.category; } /** * Check if server is initialized */ get initialized() { return this.isInitialized; } /** * Check if server is running */ get running() { return this.isRunning; } } //# sourceMappingURL=mcpServerBase.js.map