coreto-mcp-glpi
Version:
MCP Server para integração CORETO AI com GLPI via tools de tickets
459 lines (384 loc) • 13.1 kB
JavaScript
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)
})
}