UNPKG

openai-assistants-mcp

Version:

OpenAI Assistants MCP Server - A Model Context Protocol server providing OpenAI Assistants API tools and resources. Features comprehensive assistant management, thread operations, message handling, and run execution with seamless stdio transport for Claud

503 lines 19.7 kB
"use strict"; /** * CommonJS version of BaseMCPHandler for NPM package compatibility * * This file provides a CommonJS-compatible version of the BaseMCPHandler * that can be used by the NPM package while maintaining the same functionality * as the TypeScript version. */ const { OpenAIService } = require('../services/openai-service.cjs'); const { getAllResources, getResource, getResourceContent } = require('../resources/resources.cjs'); const { setupHandlerSystem, generateToolDefinitions } = require('./handlers/index.cjs'); const { createPromptHandlers } = require('./handlers/prompt-handlers.cjs'); const { createCompletionHandlers } = require('./handlers/completion-handlers.cjs'); const { paginateArray, validatePaginationParams, createPaginationMetadata, PAGINATION_DEFAULTS } = require('./pagination-utils.cjs'); // Error handling utilities const ErrorCodes = { PARSE_ERROR: -32700, INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, INVALID_PARAMS: -32602, INTERNAL_ERROR: -32603 }; const LegacyErrorCodes = { NOT_FOUND: -32001, VALIDATION_ERROR: -32002, RATE_LIMIT: -32003 }; class MCPError extends Error { constructor(code, message, data) { super(message); this.name = 'MCPError'; this.code = code; this.data = data; } } function createEnhancedError(code, message, data) { return new MCPError(code, message, data); } function createStandardErrorResponse(requestId, code, message, data) { return { jsonrpc: '2.0', id: requestId, error: { code, message, data } }; } /** * Base MCP Handler class that consolidates all deployment targets * * This class implements the core MCP protocol logic and can be extended * or adapted for different deployment environments through the adapter pattern. */ class BaseMCPHandler { constructor(config, transportAdapter) { // Set default configuration this.config = { serverName: 'openai-assistants-mcp', serverVersion: '1.0.0', debug: false, capabilities: { tools: { listChanged: false }, resources: { subscribe: false, listChanged: false }, prompts: { listChanged: false }, completions: {}, }, ...config, }; this.transportAdapter = transportAdapter; this.openaiService = new OpenAIService(config.apiKey); this.promptHandlers = {}; this.completionHandlers = {}; this.isInitialized = false; // Initialize the handler system once (performance optimization) this.initializeHandlerSystem(); this.initializePromptHandlers(); this.initializeCompletionHandlers(); } /** * Initialize the handler system with performance optimizations */ initializeHandlerSystem() { const context = { openaiService: this.openaiService, toolName: '', requestId: null }; console.log('[BaseMCPHandler] DEBUG: Starting handler system initialization...'); this.log('Initializing handler system...'); try { this.toolRegistry = setupHandlerSystem(context); console.log('[BaseMCPHandler] DEBUG: Handler system setup completed'); // Validate tool count const registeredTools = this.toolRegistry.getRegisteredTools(); console.log(`[BaseMCPHandler] DEBUG: Registry returned ${registeredTools.length} tools`); this.log(`Registered ${registeredTools.length} tools:`, registeredTools); if (registeredTools.length !== 22) { console.error(`[BaseMCPHandler] ERROR: Expected 22 tools, got ${registeredTools.length}`); console.error('[BaseMCPHandler] Registry stats:', this.toolRegistry.getStats()); console.error('[BaseMCPHandler] Missing tools analysis:'); const expectedTools = [ 'assistant-create', 'assistant-list', 'assistant-get', 'assistant-update', 'assistant-delete', 'thread-create', 'thread-get', 'thread-update', 'thread-delete', 'message-create', 'message-list', 'message-get', 'message-update', 'message-delete', 'run-create', 'run-list', 'run-get', 'run-update', 'run-cancel', 'run-submit-tool-outputs', 'run-step-list', 'run-step-get' ]; const missingTools = expectedTools.filter(tool => !registeredTools.includes(tool)); const extraTools = registeredTools.filter(tool => !expectedTools.includes(tool)); console.error('[BaseMCPHandler] Missing tools:', missingTools); console.error('[BaseMCPHandler] Extra tools:', extraTools); } else { console.log('[BaseMCPHandler] SUCCESS: All 22 tools registered correctly'); } } catch (error) { console.error('[BaseMCPHandler] FATAL ERROR during handler system initialization:', error); throw error; } } /** * Initialize the prompt handlers system */ initializePromptHandlers() { const context = { requestId: null }; this.log('Initializing prompt handlers...'); this.promptHandlers = createPromptHandlers(context); const handlerCount = Object.keys(this.promptHandlers).length; this.log(`Registered ${handlerCount} prompt handlers:`, Object.keys(this.promptHandlers)); } /** * Initialize the completion handlers system */ initializeCompletionHandlers() { const context = { requestId: null }; this.log('Initializing completion handlers...'); this.completionHandlers = createCompletionHandlers(context); const handlerCount = Object.keys(this.completionHandlers).length; this.log(`Registered ${handlerCount} completion handlers:`, Object.keys(this.completionHandlers)); } /** * Main request handler - entry point for all MCP requests */ async handleRequest(request) { try { // Transport-specific preprocessing if (this.transportAdapter && this.transportAdapter.preprocessRequest) { request = await this.transportAdapter.preprocessRequest(request); } // Route to appropriate handler let response; switch (request.method) { case 'initialize': response = await this.handleInitialize(request); break; case 'tools/list': response = await this.handleToolsList(request); break; case 'tools/call': response = await this.handleToolsCall(request); break; case 'resources/list': response = await this.handleResourcesList(request); break; case 'resources/read': response = await this.handleResourcesRead(request); break; case 'prompts/list': response = await this.handlePromptsList(request); break; case 'prompts/get': response = await this.handlePromptsGet(request); break; case 'completion/complete': response = await this.handleCompletion(request); break; default: throw new MCPError(ErrorCodes.METHOD_NOT_FOUND, `Method not found: ${request.method}`); } // Transport-specific postprocessing if (this.transportAdapter && this.transportAdapter.postprocessResponse) { response = await this.transportAdapter.postprocessResponse(response); } return response; } catch (error) { return this.handleError(error, request.id); } } /** * Handle initialize requests */ async handleInitialize(request) { this.log('Handling initialize request'); this.isInitialized = true; return { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: this.config.capabilities.tools || { listChanged: false }, resources: this.config.capabilities.resources, prompts: this.config.capabilities.prompts || { listChanged: false }, completions: this.config.capabilities.completions || {}, }, serverInfo: { name: this.config.serverName, version: this.config.serverVersion, }, }, }; } /** * Handle tools list requests with pagination support */ async handleToolsList(request) { this.log('Generating tool definitions with pagination...'); // Use the shared tool definition generator for consistency const allTools = generateToolDefinitions(this.toolRegistry); this.log(`Generated ${allTools.length} tool definitions`); if (allTools.length !== 22) { console.error(`[BaseMCPHandler] ERROR: Expected 22 tool definitions, got ${allTools.length}`); } // Apply pagination - for tools/list, return all tools by default unless limit is specified const paginationParams = { cursor: request.params && request.params.cursor, limit: (request.params && request.params.limit) || allTools.length // Return all tools by default }; const paginationResult = paginateArray(allTools, paginationParams); // Log pagination metadata const metadata = createPaginationMetadata(paginationParams, paginationResult); this.log('Tools pagination:', metadata); return { jsonrpc: '2.0', id: request.id, result: { tools: paginationResult.items, nextCursor: paginationResult.nextCursor, }, }; } /** * Handle tools call requests with optimized registry usage */ async handleToolsCall(request) { const { name, arguments: args } = request.params; try { this.log(`Executing tool: ${name}`); // Update context for this specific request (no registry recreation) const currentContext = { openaiService: this.openaiService, toolName: name, requestId: request.id }; // Update the existing registry's context instead of recreating it this.updateRegistryContext(currentContext); // Execute the tool using the existing registry const result = await this.toolRegistry.execute(name, args); return { jsonrpc: '2.0', id: request.id, result: { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }, }; } catch (error) { return { jsonrpc: '2.0', id: request.id, result: { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }, }; } } /** * Handle resources list requests with pagination support */ async handleResourcesList(request) { this.log('Listing resources with pagination...'); const allResources = getAllResources(); this.log(`Found ${allResources.length} resources`); // Apply pagination - for resources/list, return all resources by default unless limit is specified const paginationParams = { cursor: request.params && request.params.cursor, limit: (request.params && request.params.limit) || allResources.length // Return all resources by default }; const paginationResult = paginateArray(allResources, paginationParams); // Log pagination metadata const metadata = createPaginationMetadata(paginationParams, paginationResult); this.log('Resources pagination:', metadata); return { jsonrpc: '2.0', id: request.id, result: { resources: paginationResult.items.map((resource) => ({ uri: resource.uri, name: resource.name, description: resource.description, mimeType: resource.mimeType })), nextCursor: paginationResult.nextCursor, }, }; } /** * Handle resources read requests */ async handleResourcesRead(request) { const { uri } = request.params; const resourceData = getResource(uri); if (!resourceData) { throw createEnhancedError(LegacyErrorCodes.NOT_FOUND, `Resource not found: ${uri}`, { resourceUri: uri, availableResources: getAllResources().map((r) => r.uri) }); } // Get resource content and ensure it's a string const rawContent = getResourceContent(uri); const textContent = typeof rawContent === 'string' ? rawContent : JSON.stringify(rawContent, null, 2); return { jsonrpc: '2.0', id: request.id, result: { contents: [ { uri, name: resourceData.name, // Add required name field mimeType: resourceData.mimeType, text: textContent, // Ensure content is always a string }, ], }, }; } /** * Handle prompts list requests */ async handlePromptsList(request) { this.log('Handling prompts/list request'); try { const handler = this.promptHandlers['prompts/list']; if (!handler) { throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'Prompts list handler not found'); } // Update context for this request handler.context.requestId = request.id; const result = await handler.handle(request.params); return { jsonrpc: '2.0', id: request.id, result }; } catch (error) { throw error instanceof MCPError ? error : new MCPError(ErrorCodes.INTERNAL_ERROR, `Failed to list prompts: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Handle prompts get requests */ async handlePromptsGet(request) { this.log('Handling prompts/get request'); try { const handler = this.promptHandlers['prompts/get']; if (!handler) { throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'Prompts get handler not found'); } // Update context for this request handler.context.requestId = request.id; const result = await handler.handle(request.params); return { jsonrpc: '2.0', id: request.id, result }; } catch (error) { throw error instanceof MCPError ? error : new MCPError(ErrorCodes.INTERNAL_ERROR, `Failed to get prompt: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Handle completion requests */ async handleCompletion(request) { this.log('Handling completion/complete request'); try { const handler = this.completionHandlers['completion/complete']; if (!handler) { throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'Completion handler not found'); } // Update context for this request handler.context.requestId = request.id; const result = await handler.handle(request.params); return { jsonrpc: '2.0', id: request.id, result }; } catch (error) { throw error instanceof MCPError ? error : new MCPError(ErrorCodes.INTERNAL_ERROR, `Failed to handle completion: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Update registry context without recreating the entire registry * This is a performance optimization to avoid the registry recreation issue */ updateRegistryContext(newContext) { // Update the context for all registered handlers for (const toolName of this.toolRegistry.getRegisteredTools()) { const handler = this.toolRegistry.getHandler(toolName); if (handler) { handler.context = newContext; } } } /** * Centralized error handling with transport adapter support and JSON-RPC 2.0 compliance */ handleError(error, requestId) { let mcpError; if (error instanceof MCPError) { mcpError = error; } else { // Create enhanced error for unknown errors mcpError = new MCPError(ErrorCodes.INTERNAL_ERROR, error instanceof Error ? error.message : 'Unknown error', { originalError: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : error, timestamp: new Date().toISOString(), requestId }); } // Use transport adapter for error formatting if available if (this.transportAdapter && this.transportAdapter.formatError) { return this.transportAdapter.formatError(mcpError, requestId); } // Default JSON-RPC 2.0 compliant error response return createStandardErrorResponse(requestId, mcpError.code, mcpError.message, mcpError.data); } /** * Update API key and reinitialize services */ updateApiKey(apiKey) { this.config.apiKey = apiKey; this.openaiService = new OpenAIService(apiKey); // Update the context in the existing registry const context = { openaiService: this.openaiService, toolName: '', requestId: null }; this.updateRegistryContext(context); } /** * Get registry statistics for debugging */ getRegistryStats() { return this.toolRegistry.getStats(); } /** * Check if handler is initialized */ getIsInitialized() { return this.isInitialized; } /** * Debug logging */ log(message, ...args) { if (this.config.debug) { console.log(`[BaseMCPHandler] ${message}`, ...args); } } } module.exports = { BaseMCPHandler, MCPError, ErrorCodes, LegacyErrorCodes }; //# sourceMappingURL=base-mcp-handler.cjs.map