Skip to main content
Glama

Curupira

by drzln
server.ts11 kB
/** * Curupira MCP Server - Refactored with Dependency Injection * Main server implementation using clean architecture */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import type { Container } from '../core/di/container.js'; import type { ILogger } from '../core/interfaces/logger.interface.js'; import type { IChromeService } from '../core/interfaces/chrome-service.interface.js'; import type { IToolRegistry } from '../core/interfaces/tool-registry.interface.js'; import type { IResourceRegistry } from '../core/interfaces/resource-registry.interface.js'; import type { ServerConfig } from '../core/di/tokens.js'; import { ChromeServiceToken, ToolRegistryToken, ResourceRegistryToken, LoggerToken, ServerConfigToken } from '../core/di/tokens.js'; import { TransportManager, type TransportType } from './transport.js'; import { HealthChecker } from './health.js'; import { SecurityManager } from '../security/index.js'; import { promptHandlers } from '../mcp/prompts/index.js'; export class CurupiraServer { private server: Server; private transportManager?: TransportManager; private healthChecker: HealthChecker; private securityManager: SecurityManager; private readonly logger: ILogger; private readonly chromeService: IChromeService; private readonly toolRegistry: IToolRegistry; private readonly resourceRegistry: IResourceRegistry; private readonly config: ServerConfig; constructor(private readonly container: Container) { // Resolve dependencies from container this.logger = container.resolve(LoggerToken); this.chromeService = container.resolve(ChromeServiceToken); this.toolRegistry = container.resolve(ToolRegistryToken); this.resourceRegistry = container.resolve(ResourceRegistryToken); this.config = container.resolve(ServerConfigToken); // Initialize MCP server this.server = new Server( { name: 'curupira-debug', version: '1.1.3', }, { capabilities: { resources: { list: true, read: true, subscribe: false, }, tools: { list: true, call: true, listChanged: true, // Enable dynamic tool updates }, prompts: { list: true, get: true, }, }, } ); // Initialize health checker with Chrome service this.healthChecker = new HealthChecker(this.chromeService); // Initialize security manager const environment = process.env.NODE_ENV === 'production' ? 'production' : process.env.NODE_ENV === 'staging' ? 'staging' : 'development'; this.securityManager = new SecurityManager({ enabled: environment !== 'development', environment, auth: { enabled: environment !== 'development', jwtSecret: process.env.CURUPIRA_JWT_SECRET, jwtPublicKey: process.env.CURUPIRA_JWT_PUBLIC_KEY, issuer: process.env.CURUPIRA_JWT_ISSUER, audience: process.env.CURUPIRA_JWT_AUDIENCE, }, }); } /** * Initialize and start the server */ async start(): Promise<void> { try { this.logger.info({ config: this.config }, 'Starting Curupira MCP server'); // Setup request handlers this.setupRequestHandlers(); // Initialize transport based on configuration const transportType = process.env.CURUPIRA_TRANSPORT || this.config.transport || 'stdio'; if (transportType !== 'stdio') { const port = process.env.CURUPIRA_PORT ? parseInt(process.env.CURUPIRA_PORT) : this.config.port; this.transportManager = new TransportManager( this.server, { type: transportType as TransportType, port, corsOrigins: process.env.CURUPIRA_CORS_ORIGINS?.split(',') || ['http://localhost:*'], enableSSE: process.env.CURUPIRA_ENABLE_SSE !== 'false', enableWS: process.env.CURUPIRA_ENABLE_WS !== 'false', healthChecker: this.healthChecker, securityManager: this.securityManager }, this.logger ); const serverUrl = await this.transportManager.start(); this.logger.info({ url: serverUrl, transport: transportType }, 'Server started'); } else { // Use stdio transport for standard MCP const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info('Server started with stdio transport'); } // Setup graceful shutdown this.setupShutdownHandlers(); } catch (error) { this.logger.error({ error }, 'Failed to start server'); throw error; } } /** * Setup all request handlers */ private setupRequestHandlers(): void { // Pass server instance to tool registry for notifications if ('setServer' in this.toolRegistry) { (this.toolRegistry as any).setServer(this.server); } // Setup Chrome event listeners for dynamic tool registration if (this.chromeService && 'on' in this.chromeService) { (this.chromeService as any).on('connected', async () => { this.logger.info('Chrome connected - triggering dynamic tool registration'); if ('onChromeConnected' in this.toolRegistry) { await (this.toolRegistry as any).onChromeConnected(); } }); (this.chromeService as any).on('disconnected', async () => { this.logger.info('Chrome disconnected - triggering dynamic tool unregistration'); if ('onChromeDisconnected' in this.toolRegistry) { await (this.toolRegistry as any).onChromeDisconnected(); } }); } // Resources handlers this.server.setRequestHandler(ListResourcesRequestSchema, async () => { try { const resources = await this.resourceRegistry.listAllResources(); return { resources }; } catch (error) { this.logger.error({ error }, 'Failed to list resources'); throw error; } }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const content = await this.resourceRegistry.readResource(request.params.uri); return { contents: [{ uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(content, null, 2) }] }; } catch (error) { this.logger.error({ error, uri: request.params.uri }, 'Failed to read resource'); throw error; } }); // Tools handlers this.server.setRequestHandler(ListToolsRequestSchema, async () => { try { const tools = this.toolRegistry.listAllTools(); this.logger.debug({ toolCount: tools.length }, 'Listing tools'); return { tools }; } catch (error) { this.logger.error({ error }, 'Failed to list tools'); throw error; } }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { this.logger.info({ tool: name, args }, 'Executing tool'); const result = await this.toolRegistry.executeTool(name, args || {}); if (!result.success) { this.logger.error({ tool: name, error: result.error }, 'Tool execution failed'); throw new Error(result.error || 'Tool execution failed'); } this.logger.info({ tool: name, hasData: !!result.data }, 'Tool executed successfully'); // MCP expects a content array with the tool result // Always return a meaningful response, even if no data const content = [{ type: 'text' as const, text: result.data ? (typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2)) : `Tool '${name}' executed successfully but returned no data.` }]; this.logger.debug({ tool: name, contentLength: content[0].text.length }, 'Returning tool result'); return { content }; } catch (error) { this.logger.error({ error: error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : error, tool: name, args }, 'Tool execution error'); throw error; } }); // Prompts handlers this.server.setRequestHandler(ListPromptsRequestSchema, async () => { const prompts = promptHandlers.map((p: any) => p.metadata); return { prompts }; }); this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { const handler = promptHandlers.find((p: any) => p.metadata.name === request.params.name); if (!handler) { throw new Error(`Unknown prompt: ${request.params.name}`); } return handler.handler(request.params.arguments || {}); }); this.logger.info('All request handlers registered'); } /** * Setup graceful shutdown handlers */ private setupShutdownHandlers(): void { const shutdown = async () => { this.logger.info('Shutting down server...'); try { // Disconnect from Chrome await this.chromeService.disconnect(); // Stop transport if running if (this.transportManager) { await this.transportManager.stop(); } this.logger.info('Server shutdown complete'); process.exit(0); } catch (error) { this.logger.error({ error }, 'Error during shutdown'); process.exit(1); } }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } /** * Get the server instance (for testing) */ getServer(): Server { return this.server; } /** * Get the container (for testing) */ getContainer(): Container { return this.container; } /** * Stop the server */ async stop(): Promise<void> { this.logger.info('Stopping Curupira server...'); if (this.transportManager) { await this.transportManager.stop(); } // Disconnect Chrome await this.chromeService.disconnect(); this.logger.info('Curupira server stopped'); } } // Export types needed by CLI export type { ServerConfig } from '../core/di/tokens.js'; export interface ServerOptions { host?: string; port?: number; logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; chrome?: { host?: string; port?: number; }; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/drzln/curupira'

If you have feedback or need assistance with the MCP directory API, please join our Discord server