UNPKG

coreto-mcp-glpi

Version:

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

414 lines (345 loc) 11.3 kB
import fetch from 'node-fetch' import { glpiLogger } from './logger.js' import { GLPI_CONFIG, ERROR_CODES } from './constants.js' class GLPIConnector { constructor(credentials, tenantId) { this.tenantId = tenantId this.baseUrl = credentials.url?.replace(/\/+$/, '') // Remove trailing slashes // Extract token value if stored with "user_token " prefix this.userToken = credentials.user_token?.replace(/^user_token\s+/, '') || credentials.user_token this.appToken = credentials.app_token this.sessionToken = null this.sessionExpiry = null this.sessionTimeout = GLPI_CONFIG.SESSION_TIMEOUT this.logger = glpiLogger.child({ tenantId }) // Validate credentials this.validateCredentials() } /** * Validate GLPI credentials */ validateCredentials() { if (!this.baseUrl || !this.userToken || !this.appToken) { throw new Error('GLPI credentials incomplete: url, user_token, and app_token are required') } try { new URL(this.baseUrl) } catch (error) { throw new Error('Invalid GLPI URL format') } } /** * Initialize GLPI connection */ async initialize() { this.logger.info('Initializing GLPI connector', { baseUrl: this.baseUrl }) try { await this.createSession() this.logger.info('GLPI connector initialized successfully') } catch (error) { this.logger.error('Failed to initialize GLPI connector', { error: error.message }) throw error } } /** * Create GLPI session */ async createSession() { try { this.logger.debug('Creating GLPI session') const response = await fetch(`${this.baseUrl}${GLPI_CONFIG.ENDPOINTS.INIT_SESSION}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `user_token ${this.userToken}`, 'App-Token': this.appToken }, timeout: 10000 }) if (!response.ok) { const errorText = await response.text() throw new Error(`GLPI auth failed (${response.status}): ${errorText}`) } const data = await response.json() if (!data.session_token) { throw new Error('No session token returned from GLPI') } this.sessionToken = data.session_token this.sessionExpiry = Date.now() + this.sessionTimeout this.logger.info('GLPI session created successfully', { sessionExpiry: new Date(this.sessionExpiry).toISOString() }) } catch (error) { this.logger.error('GLPI session creation failed', { error: error.message, baseUrl: this.baseUrl }) if (error.code === 'ECONNREFUSED') { throw new Error('Cannot connect to GLPI server - check URL and network connectivity') } throw new Error(`Failed to authenticate with GLPI: ${error.message}`) } } /** * Ensure session is valid, create new if needed */ async ensureValidSession() { if (!this.sessionToken || !this.isSessionValid()) { this.logger.debug('Session invalid or expired, creating new session') await this.createSession() } } /** * Check if current session is valid * @returns {boolean} Is session valid */ isSessionValid() { return !!(this.sessionToken && Date.now() < this.sessionExpiry) } /** * Make authenticated request to GLPI API * @param {string} method - HTTP method * @param {string} endpoint - API endpoint * @param {Object} data - Request data * @param {number} retryCount - Current retry count * @returns {Object} Response data */ async request(method, endpoint, data = null, retryCount = 0) { await this.ensureValidSession() const url = `${this.baseUrl}${endpoint}` const options = { method: method.toUpperCase(), headers: { 'Content-Type': 'application/json', 'Session-Token': this.sessionToken, 'App-Token': this.appToken }, timeout: 15000 } if (data && (method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT')) { options.body = JSON.stringify(data) } try { this.logger.debug('Making GLPI request', { method: method.toUpperCase(), endpoint, hasData: !!data }) const response = await fetch(url, options) if (!response.ok) { const errorText = await response.text() // If unauthorized and we haven't retried yet, try to recreate session if (response.status === 401 && retryCount < GLPI_CONFIG.MAX_RETRIES) { this.logger.warn('Session expired, recreating and retrying', { retryCount }) this.sessionToken = null // Force new session await this.ensureValidSession() // Retry request with new session return await this.request(method, endpoint, data, retryCount + 1) } throw new Error(`GLPI request failed (${response.status}): ${errorText}`) } const responseData = await response.json() this.logger.debug('GLPI request successful', { method: method.toUpperCase(), endpoint, hasResponse: !!responseData }) return responseData } catch (error) { this.logger.error('GLPI request failed', { method: method.toUpperCase(), endpoint, error: error.message, retryCount }) if (error.code === 'ECONNREFUSED') { throw new Error('Cannot connect to GLPI server') } if (error.name === 'FetchError' && error.code === 'ENOTFOUND') { throw new Error('GLPI server not found - check URL') } throw error } } /** * Get ticket by ID * @param {number} ticketId - Ticket ID * @returns {Object} Ticket data */ async getTicket(ticketId) { if (!ticketId || !Number.isInteger(Number(ticketId))) { throw new Error('Valid ticket ID is required') } try { const ticket = await this.request('GET', `${GLPI_CONFIG.ENDPOINTS.TICKET}/${ticketId}`) if (!ticket || ticket.length === 0) { throw new Error(`Ticket #${ticketId} not found`) } return Array.isArray(ticket) ? ticket[0] : ticket } catch (error) { this.logger.error('Failed to get ticket', { ticketId, error: error.message }) throw error } } /** * Create new ticket * @param {Object} ticketData - Ticket data * @returns {Object} Created ticket response */ async createTicket(ticketData) { if (!ticketData.name || !ticketData.content) { throw new Error('Ticket name and content are required') } try { const response = await this.request('POST', GLPI_CONFIG.ENDPOINTS.TICKET, { input: { name: ticketData.name, content: ticketData.content, priority: ticketData.priority || 3, itilcategories_id: ticketData.category_id || 0, status: ticketData.status || 1, // New type: ticketData.type || 1, // Incident ...ticketData.extra } }) if (!response.id) { throw new Error('Failed to create ticket - no ID returned') } this.logger.info('Ticket created successfully', { ticketId: response.id, title: ticketData.name }) return response } catch (error) { this.logger.error('Failed to create ticket', { ticketData, error: error.message }) throw error } } /** * Add followup to ticket * @param {number} ticketId - Ticket ID * @param {string} content - Followup content * @param {Object} options - Additional options * @returns {Object} Followup response */ async addFollowup(ticketId, content, options = {}) { if (!ticketId || !content) { throw new Error('Ticket ID and content are required') } try { const response = await this.request('POST', GLPI_CONFIG.ENDPOINTS.FOLLOWUP, { input: { items_id: Number(ticketId), itemtype: 'Ticket', content: content, is_private: options.isPrivate || false, ...options.extra } }) this.logger.info('Followup added successfully', { ticketId, followupId: response.id }) return response } catch (error) { this.logger.error('Failed to add followup', { ticketId, content, error: error.message }) throw error } } /** * Search tickets by criteria * @param {Object} criteria - Search criteria * @returns {Array} Found tickets */ async searchTickets(criteria = {}) { try { let endpoint = GLPI_CONFIG.ENDPOINTS.TICKET // Build search parameters const params = new URLSearchParams() if (criteria.status) { params.append('criteria[0][field]', '12') // Status field params.append('criteria[0][searchtype]', 'equals') params.append('criteria[0][value]', criteria.status) } if (criteria.user_id) { params.append('criteria[1][field]', '4') // Requester field params.append('criteria[1][searchtype]', 'equals') params.append('criteria[1][value]', criteria.user_id) } if (params.toString()) { endpoint += `?${params.toString()}` } const tickets = await this.request('GET', endpoint) return Array.isArray(tickets) ? tickets : [] } catch (error) { this.logger.error('Failed to search tickets', { criteria, error: error.message }) throw error } } /** * Kill GLPI session */ async killSession() { if (this.sessionToken) { try { await fetch(`${this.baseUrl}${GLPI_CONFIG.ENDPOINTS.KILL_SESSION}`, { method: 'POST', headers: { 'Session-Token': this.sessionToken, 'App-Token': this.appToken }, timeout: 5000 }) this.logger.debug('GLPI session killed successfully') } catch (error) { this.logger.warn('Error killing GLPI session', { error: error.message }) } this.sessionToken = null this.sessionExpiry = null } } /** * Cleanup connector resources */ async cleanup() { this.logger.debug('Cleaning up GLPI connector') try { await this.killSession() } catch (error) { this.logger.error('Error during GLPI connector cleanup', { error: error.message }) } } /** * Get connector statistics * @returns {Object} Connector stats */ getStats() { return { tenantId: this.tenantId, baseUrl: this.baseUrl, hasSession: !!this.sessionToken, sessionValid: this.isSessionValid(), sessionExpiry: this.sessionExpiry ? new Date(this.sessionExpiry).toISOString() : null } } } export default GLPIConnector