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