UNPKG

coreto-mcp-glpi

Version:

MCP Server para integração CORETO AI com GLPI via tools de tickets

459 lines (384 loc) 13.1 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import fetch from 'node-fetch' // Simple relative imports - works with NPX since all files are at root level import { mcpLogger } from './logger.js' import { TOOLS_CONFIG } from './constants.js' import GLPIConnector from './glpi-connector.js' // Import tools with simple relative paths import createTicketTool from './tools/create-ticket.js' import getTicketStatusTool from './tools/get-ticket-status.js' import addFollowupTool from './tools/add-followup.js' import getUserTicketsTool from './tools/get-user-tickets.js' import getItilCategoriesTool from './tools/get-itil-categories.js' class CoretoMCPServer { constructor() { this.server = new Server({ name: 'coreto-mcp-server', version: '1.0.8' }, { capabilities: { tools: {} } }) this.logger = mcpLogger this.glpiConnectors = new Map() // tenant_id -> GLPIConnector // Available tools by source this.tools = { whatsapp: [ createTicketTool, getTicketStatusTool, addFollowupTool, getUserTicketsTool, getItilCategoriesTool ], chatai: [ createTicketTool, getTicketStatusTool, addFollowupTool, getUserTicketsTool, getItilCategoriesTool // Can add more advanced tools for Chat IA here ] } this.setupHandlers() } /** * Setup MCP request handlers */ setupHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { try { const { source = 'whatsapp', tenant_id } = request.params || {} this.logger.debug('Listing tools', { source, tenant_id }) const availableTools = this.tools[source] || this.tools.whatsapp const toolsResponse = availableTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) this.logger.info('Tools listed successfully', { count: toolsResponse.length, source }) return { tools: toolsResponse } } catch (error) { this.logger.error('Error listing tools', { error: error.message, params: request.params }) return { tools: [] } } }) // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args, tenant_context } = request.params this.logger.debug('Tool call received', { name, args, tenant_context }) // Se não tem tenant_context, tenta usar env vars let finalTenantContext = tenant_context if (!tenant_context || !tenant_context.tenant_id) { // Buscar tenant token das env vars const envTenantToken = process.env.CORETO_TENANT_TOKEN if (envTenantToken) { finalTenantContext = { tenant_token: envTenantToken, tenant_id: `env_${envTenantToken}`, source: 'cursor' } this.logger.info('Using tenant from environment variables', { tenant_token: envTenantToken }) } else { this.logger.error('Missing tenant context and no env tenant token', { name, args }) return { content: [{ type: 'text', text: JSON.stringify({ error: 'Contexto do tenant é obrigatório ou configure CORETO_TENANT_TOKEN', code: 'MISSING_TENANT_CONTEXT' }) }], isError: true } } } try { // Setup GLPI connector for tenant const glpiConnector = await this.getGLPIConnector(finalTenantContext) // Find and execute tool const tool = this.findTool(name, finalTenantContext.source) if (!tool) { throw new Error(`Ferramenta '${name}' não encontrada`) } this.logger.info('Executing tool', { toolName: name, tenantId: finalTenantContext.tenant_id, source: finalTenantContext.source }) const result = await tool.handler(args, { ...finalTenantContext, glpiConnector }) this.logger.info('Tool executed successfully', { toolName: name, tenantId: finalTenantContext.tenant_id, success: result.success }) return { content: [{ type: 'text', text: JSON.stringify(result) }] } } catch (error) { this.logger.error('Tool execution error', { toolName: name, tenantId: finalTenantContext.tenant_id, error: error.message, stack: error.stack }) return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, tool: name, code: 'TOOL_EXECUTION_ERROR' }) }], isError: true } } }) // Note: MCP Server já gerencia initialize internamente } /** * Find tool by name and source * @param {string} name - Tool name * @param {string} source - Source type ('whatsapp', 'chatai') * @returns {Object|null} Tool object */ findTool(name, source = 'whatsapp') { const toolSet = this.tools[source] || this.tools.whatsapp return toolSet.find(tool => tool.name === name) } /** * Get or create GLPI connector for tenant * @param {Object} tenantContext - Tenant context with credentials * @returns {GLPIConnector} GLPI connector instance */ async getGLPIConnector(tenantContext) { const { tenant_id, tenant_token } = tenantContext // Se não tem tenant_token, busca credenciais das env vars (para casos diretos) if (!tenant_token) { const envCredentials = { url: process.env.GLPI_URL, user_token: process.env.GLPI_USER_TOKEN, app_token: process.env.GLPI_APP_TOKEN } if (!envCredentials.url || !envCredentials.user_token || !envCredentials.app_token) { throw new Error('Credenciais GLPI não configuradas. Configure GLPI_URL, GLPI_USER_TOKEN e GLPI_APP_TOKEN ou forneça tenant_token válido') } tenantContext.glpi_credentials = envCredentials } else { // Buscar credenciais do tenant no backend const credentials = await this.fetchTenantCredentials(tenant_token) tenantContext.glpi_credentials = credentials } const { glpi_credentials } = tenantContext if (!glpi_credentials) { throw new Error('Credenciais GLPI não configuradas para este tenant') } // Check if connector already exists and is valid if (this.glpiConnectors.has(tenant_id)) { const connector = this.glpiConnectors.get(tenant_id) // Validate session is still active if (connector.isSessionValid()) { return connector } else { // Remove invalid connector this.glpiConnectors.delete(tenant_id) } } // Create new connector this.logger.info('Creating new GLPI connector', { tenant_id }) try { const connector = new GLPIConnector(glpi_credentials, tenant_id) await connector.initialize() this.glpiConnectors.set(tenant_id, connector) this.logger.info('GLPI connector created successfully', { tenant_id }) return connector } catch (error) { this.logger.error('Failed to create GLPI connector', { tenant_id, error: error.message }) throw new Error(`Falha ao conectar com GLPI: ${error.message}`) } } /** * Fetch tenant credentials from backend API * @param {string} tenantToken - Tenant token * @returns {Object} GLPI credentials */ async fetchTenantCredentials(tenantToken) { try { // URL do backend (pode vir de env var) const backendUrl = process.env.CORETO_BACKEND_URL || 'http://localhost:3000' const response = await fetch(`${backendUrl}/internal/tenant/${tenantToken}/credentials`, { headers: { 'Authorization': `Bearer ${process.env.CORETO_INTERNAL_TOKEN || 'internal-access'}`, 'Content-Type': 'application/json' } }) if (!response.ok) { throw new Error(`Failed to fetch tenant credentials: ${response.status}`) } const data = await response.json() if (!data.glpi_credentials) { throw new Error('No GLPI credentials found for tenant') } this.logger.info('Tenant credentials fetched successfully', { tenantToken }) return data.glpi_credentials } catch (error) { this.logger.error('Failed to fetch tenant credentials', { tenantToken, error: error.message }) throw new Error(`Unable to fetch credentials for tenant: ${error.message}`) } } /** * Cleanup expired GLPI connectors */ cleanupConnectors() { let cleanedCount = 0 for (const [tenantId, connector] of this.glpiConnectors.entries()) { if (!connector.isSessionValid()) { connector.cleanup() this.glpiConnectors.delete(tenantId) cleanedCount++ } } if (cleanedCount > 0) { this.logger.info('Cleaned up expired GLPI connectors', { cleanedCount }) } } /** * Get server statistics * @returns {Object} Server stats */ getStats() { return { activeConnectors: this.glpiConnectors.size, availableTools: { whatsapp: this.tools.whatsapp.length, chatai: this.tools.chatai.length }, uptime: process.uptime(), memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) } } } /** * Start the MCP server */ async start() { try { this.logger.info('🔧 Starting CORETO MCP Server...') // Setup cleanup interval for connectors setInterval(() => { this.cleanupConnectors() }, 300000) // 5 minutes // Create STDIO transport const transport = new StdioServerTransport() await this.server.connect(transport) this.logger.info('✅ CORETO MCP Server started on STDIO') // Log server stats periodically setInterval(() => { const stats = this.getStats() this.logger.debug('Server stats', stats) }, 60000) // 1 minute } catch (error) { this.logger.error('Failed to start MCP Server', { error: error.message }) throw error } } /** * Shutdown server gracefully */ async shutdown() { this.logger.info('🛑 Shutting down MCP Server...') try { // Cleanup all GLPI connectors for (const [tenantId, connector] of this.glpiConnectors.entries()) { try { await connector.cleanup() } catch (error) { this.logger.error('Error cleaning up connector', { tenantId, error: error.message }) } } this.glpiConnectors.clear() // Close server await this.server.close() this.logger.info('✅ MCP Server shutdown completed') } catch (error) { this.logger.error('Error during MCP Server shutdown', { error: error.message }) } } } // Export the class for direct usage export default CoretoMCPServer // Only run as standalone if this file is the main module if (import.meta.url === `file://${process.argv[1]}`) { // Create and start server const server = new CoretoMCPServer() // Handle process signals for graceful shutdown process.on('SIGTERM', async () => { await server.shutdown() process.exit(0) }) process.on('SIGINT', async () => { await server.shutdown() process.exit(0) }) // Handle uncaught errors process.on('uncaughtException', (error) => { mcpLogger.error('Uncaught exception in MCP Server', { error: error.message, stack: error.stack }) process.exit(1) }) process.on('unhandledRejection', (reason, promise) => { mcpLogger.error('Unhandled rejection in MCP Server', { reason: reason?.message || reason }) }) // Start the server server.start().catch(error => { mcpLogger.error('Failed to start MCP Server', { error: error.message }) process.exit(1) }) }