jezweb-mcp-core
Version:
Jezweb Model Context Protocol (MCP) Core - A universal server for providing AI tools and resources, designed for seamless integration with various AI models and clients. Features adaptable multi-provider support, comprehensive tool and resource management
704 lines • 29.7 kB
JavaScript
/**
* Base MCP Handler - Consolidated handler for all deployment targets
*
* This class provides a unified MCP protocol implementation that can be used
* across different deployment targets (Cloudflare Workers, NPM package, local dev).
* It eliminates code duplication while preserving deployment-specific optimizations.
*
* Key Features:
* - Single source of truth for MCP protocol handling
* - Transport-agnostic design (HTTP, Stdio, etc.)
* - Shared tool registry with performance optimizations
* - Consistent error handling across deployments
* - Resource management support
* - Deployment-specific adapter pattern
*/
import { MCPError, ErrorCodes, LegacyErrorCodes, createEnhancedError, createStandardErrorResponse, } from '../types/index.js';
import { OpenAIService } from '../services/openai-service.js';
import { ProviderRegistry, } from '../types/generic-types.js';
import { openaiProviderFactory } from '../services/providers/openai.js';
import { getAllResources, getResource, getResourceContent } from '../resources/index.js';
import { ToolRegistry } from './tool-registry.js';
import { createFlatHandlerMap, validateHandlerCompleteness } from './handlers/index.js';
import { generateToolDefinitions } from './tool-definitions.js';
import { createPromptHandlers } from './handlers/prompt-handlers.js';
import { createCompletionHandlers } from './handlers/completion-handlers.js';
import { paginateArray, createPaginationMetadata, PAGINATION_DEFAULTS } from './pagination-utils.js';
/**
* 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.
*/
export class BaseMCPHandler {
providerRegistry;
toolRegistry; // Definite assignment assertion - initialized in initializeHandlerSystem
promptHandlers = {};
completionHandlers = {};
config;
transportAdapter;
isInitialized = false;
constructor(config, providerRegistryOrTransportAdapter, transportAdapter) {
// Set default configuration
this.config = {
...config,
serverName: config.serverName || 'openai-assistants-mcp',
serverVersion: config.serverVersion || '3.0.0',
debug: config.debug || false,
capabilities: {
tools: { listChanged: false },
resources: { subscribe: false, listChanged: false },
prompts: { listChanged: false },
completions: {},
...config.capabilities,
},
};
// Handle backward compatibility for constructor overloads
if (providerRegistryOrTransportAdapter) {
// Check if the second parameter is a ProviderRegistry or TransportAdapter
if ('getDefaultProvider' in providerRegistryOrTransportAdapter) {
// It's a ProviderRegistry
this.providerRegistry = providerRegistryOrTransportAdapter;
this.transportAdapter = transportAdapter;
}
else {
// It's a TransportAdapter (backward compatibility mode)
this.transportAdapter = providerRegistryOrTransportAdapter;
// Create a default provider registry for backward compatibility
this.providerRegistry = this.createBackwardCompatibilityRegistry(config);
}
}
else {
// No second parameter provided (backward compatibility mode)
this.providerRegistry = this.createBackwardCompatibilityRegistry(config);
}
// Initialize the handler system once (performance optimization)
this.initializeHandlerSystem();
this.initializePromptHandlers();
this.initializeCompletionHandlers();
}
/**
* Create a backward compatibility provider registry
* This is used when the old constructor signature is used
*/
createBackwardCompatibilityRegistry(config) {
if (!config.apiKey) {
throw new MCPError(ErrorCodes.INVALID_PARAMS, 'API key is required for backward compatibility mode');
}
// Create a simple registry with just the OpenAI provider
const registryConfig = {
defaultProvider: 'openai',
providers: [
{
provider: 'openai',
enabled: true,
config: { apiKey: config.apiKey },
},
],
};
const registry = new ProviderRegistry(registryConfig);
registry.registerFactory(openaiProviderFactory);
// Initialize the registry synchronously for backward compatibility
// Note: This is not ideal but necessary for backward compatibility
registry.initialize().catch(error => {
console.error('[BaseMCPHandler] Failed to initialize backward compatibility registry:', error);
});
return registry;
}
/**
* Initialize the handler system with performance optimizations
*/
initializeHandlerSystem() {
// Get the default provider from the registry
let provider = this.providerRegistry.getDefaultProvider();
let openaiService;
if (!provider) {
// If no provider is available yet (registry not initialized), use fallback
console.warn('[BaseMCPHandler] No default provider available yet, using fallback OpenAI service');
if (this.config.apiKey) {
openaiService = new OpenAIService(this.config.apiKey);
}
else {
throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'No provider available and no API key for fallback');
}
}
else {
// For backward compatibility, we need to extract the OpenAIService from the provider
// This assumes the provider is an OpenAI provider with access to the underlying service
openaiService = this.getOpenAIServiceFromProvider(provider);
}
const context = {
provider: provider || this.createFallbackProvider(openaiService),
toolName: '',
requestId: null
};
console.log('[BaseMCPHandler] DEBUG: Starting handler system initialization...');
this.log('Initializing handler system...');
try {
this.toolRegistry = new ToolRegistry(context);
const handlers = createFlatHandlerMap(context);
// Validate that we have all expected handlers
const validation = validateHandlerCompleteness(handlers);
if (!validation.isComplete) {
console.warn('[BaseMCPHandler] Missing handlers:', validation.missingTools);
if (validation.extraTools.length > 0) {
console.warn('[BaseMCPHandler] Extra handlers:', validation.extraTools);
}
}
// Register all handlers
this.toolRegistry.registerBatch(handlers);
console.log('[BaseMCPHandler] DEBUG: Handler system setup completed');
// Log registered tools for debugging (dynamic count)
const registeredTools = this.toolRegistry.getRegisteredTools();
console.log(`[BaseMCPHandler] DEBUG: Registry returned ${registeredTools.length} tools`);
this.log(`Registered ${registeredTools.length} tools:`, registeredTools);
// Dynamic validation - no hardcoded tool count assumption
if (registeredTools.length > 0) {
console.log(`[BaseMCPHandler] SUCCESS: ${registeredTools.length} tools registered successfully`);
}
else {
console.warn('[BaseMCPHandler] WARNING: No tools were registered');
}
}
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?.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?.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 - returns all tools (no pagination needed)
*/
async handleToolsList(request) {
this.log('Generating tool definitions...');
// Use the shared tool definition generator for consistency
const allTools = generateToolDefinitions(this.toolRegistry);
this.log(`Generated ${allTools.length} tool definitions`);
// Log tool count for debugging (no hardcoded validation)
this.log(`Tool definitions generated: ${allTools.length} tools available`);
return {
jsonrpc: '2.0',
id: request.id,
result: {
tools: allTools,
},
};
}
/**
* Handle tools call requests with optimized registry usage
*/
async handleToolsCall(request) {
const { name, arguments: args } = request.params;
try {
this.log(`Executing tool: ${name}`);
// Enhanced logging for debugging if enabled
if (this.config.debug) {
this.log(`Tool execution details:`, {
toolName: name,
arguments: args,
requestId: request.id,
timestamp: new Date().toISOString(),
});
}
// Get the provider for this request (for now, use default provider)
const provider = this.providerRegistry.getDefaultProvider();
if (!provider) {
throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'No default provider available');
}
// For backward compatibility, extract OpenAIService from provider
const openaiService = this.getOpenAIServiceFromProvider(provider);
// Update context for this specific request (no registry recreation)
const currentContext = {
provider: provider,
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);
// Enhanced result formatting if debug enabled
const responseText = this.config.debug
? JSON.stringify(result, null, 2)
: JSON.stringify(result);
return {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: responseText,
},
],
},
};
}
catch (error) {
// Enhanced error reporting if debug enabled
const errorText = this.config.debug
? `Error in ${name}: ${error instanceof Error ? error.message : 'Unknown error'}\nStack: ${error instanceof Error ? error.stack : 'N/A'}`
: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
return {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: errorText,
},
],
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
const paginationParams = {
cursor: request.params?.cursor,
limit: PAGINATION_DEFAULTS.DEFAULT_LIMIT // Use default limit for resources
};
const paginationResult = paginateArray(allResources, paginationParams);
// Log pagination metadata if debug is enabled
if (this.config.debug) {
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?.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
* Note: This method now works with the provider registry
*/
updateApiKey(apiKey) {
this.config.apiKey = apiKey;
// For backward compatibility, we need to update the provider in the registry
// This is a simplified approach - in a full implementation, you might want to
// reinitialize the entire provider registry with new configuration
const provider = this.providerRegistry.getDefaultProvider();
if (provider) {
// Update the provider's configuration if it supports it
// This assumes the provider has an initialize method that can update the API key
provider.initialize({ apiKey }).catch(error => {
console.error('[BaseMCPHandler] Failed to update provider API key:', error);
});
}
// For backward compatibility, extract OpenAIService from provider
const openaiService = this.getOpenAIServiceFromProvider(provider);
// Update the context in the existing registry
const context = {
provider: provider || this.createFallbackProvider(openaiService),
toolName: '',
requestId: null
};
this.updateRegistryContext(context);
}
/**
* Helper method to extract OpenAIService from provider for backward compatibility
* This is a bridge method to maintain compatibility with existing tool handlers
*/
getOpenAIServiceFromProvider(provider) {
if (!provider) {
throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'No provider available');
}
// For now, we assume the provider is an OpenAI provider
// In a full implementation, you might want to check the provider type
// and handle different provider types appropriately
// Access the underlying OpenAI service from the provider
// This assumes the OpenAI provider exposes its internal service
if (provider.openaiService) {
return provider.openaiService;
}
// Fallback: create a new OpenAI service with the API key from config
// This maintains backward compatibility but doesn't fully utilize the provider system
if (this.config.apiKey) {
return new OpenAIService(this.config.apiKey);
}
throw new MCPError(ErrorCodes.INTERNAL_ERROR, 'Unable to extract OpenAI service from provider');
}
/**
* Get registry statistics for debugging
*/
getRegistryStats() {
return this.toolRegistry.getStats();
}
/**
* Check if handler is initialized
*/
getIsInitialized() {
return this.isInitialized;
}
/**
* Create a fallback provider wrapper for backward compatibility
* This wraps an OpenAIService in a minimal LLMProvider interface
*/
createFallbackProvider(openaiService) {
// Create a minimal provider wrapper that delegates to the OpenAI service
// This maintains backward compatibility while using the new interface
return {
metadata: {
name: 'openai-fallback',
displayName: 'OpenAI (Fallback)',
version: '1.0.0',
description: 'Fallback OpenAI provider for backward compatibility',
capabilities: {
assistants: true,
threads: true,
messages: true,
runs: true,
runSteps: true,
fileAttachments: true,
functionCalling: true,
codeInterpreter: true,
fileSearch: true,
streaming: false,
}
},
async initialize(config) {
// OpenAIService is already initialized
},
async validateConnection() {
// For backward compatibility, assume connection is valid
return true;
},
// Delegate all methods to the OpenAI service
// Note: This is a simplified implementation for backward compatibility
// In a full implementation, you would properly map all methods
async createAssistant(request) {
return openaiService.createAssistant(request);
},
async listAssistants(request) {
return openaiService.listAssistants(request);
},
async getAssistant(assistantId) {
return openaiService.getAssistant(assistantId);
},
async updateAssistant(assistantId, request) {
return openaiService.updateAssistant(assistantId, request);
},
async deleteAssistant(assistantId) {
return openaiService.deleteAssistant(assistantId);
},
async createThread(request) {
return openaiService.createThread(request);
},
async getThread(threadId) {
return openaiService.getThread(threadId);
},
async updateThread(threadId, request) {
return openaiService.updateThread(threadId, request);
},
async deleteThread(threadId) {
return openaiService.deleteThread(threadId);
},
async createMessage(threadId, request) {
return openaiService.createMessage(threadId, request);
},
async listMessages(threadId, request) {
return openaiService.listMessages(threadId, request);
},
async getMessage(threadId, messageId) {
return openaiService.getMessage(threadId, messageId);
},
async updateMessage(threadId, messageId, request) {
return openaiService.updateMessage(threadId, messageId, request);
},
async deleteMessage(threadId, messageId) {
return openaiService.deleteMessage(threadId, messageId);
},
async createRun(threadId, request) {
return openaiService.createRun(threadId, request);
},
async listRuns(threadId, request) {
return openaiService.listRuns(threadId, request);
},
async getRun(threadId, runId) {
return openaiService.getRun(threadId, runId);
},
async updateRun(threadId, runId, request) {
return openaiService.updateRun(threadId, runId, request);
},
async cancelRun(threadId, runId) {
return openaiService.cancelRun(threadId, runId);
},
async submitToolOutputs(threadId, runId, request) {
return openaiService.submitToolOutputs(threadId, runId, request);
},
async listRunSteps(threadId, runId, request) {
return openaiService.listRunSteps(threadId, runId, request);
},
async getRunStep(threadId, runId, stepId) {
return openaiService.getRunStep(threadId, runId, stepId);
},
async handleUnsupportedOperation(operation, ...args) {
throw new Error(`Unsupported operation: ${operation}`);
}
};
}
/**
* Debug logging with feature flag support
*/
log(message, ...args) {
if (this.config.debug) {
console.log(`[BaseMCPHandler] ${message}`, ...args);
}
}
}
//# sourceMappingURL=base-mcp-handler.js.map