@tiflux/mcp
Version:
TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients
319 lines (264 loc) • 9.04 kB
JavaScript
/**
* TicketHandler - Handler limpo para operações de ticket
*
* Responsabilidades:
* - Receber requests MCP e validar parâmetros básicos
* - Delegar lógica de negócio para TicketService
* - Aplicar formatação de resposta via middleware
* - Error handling padronizado
* - Logging de requests/responses
*/
class TicketHandler {
constructor(container) {
this.container = container;
this.logger = container.resolve('logger');
this.ticketService = container.resolve('ticketService');
this.domainOrchestrator = container.resolve('domainOrchestrator');
this.responseFormatter = null; // Lazy loading
}
/**
* Handler para buscar um ticket específico
*/
async handleGetTicket(args) {
const timer = this.logger.startTimer('handle_get_ticket');
try {
this.logger.info('Handling get ticket request', {
ticketNumber: args.ticket_number
});
// Validação básica de parâmetros
if (!args.ticket_number) {
throw new ValidationError('ticket_number é obrigatório');
}
// Delega para o domain service
const result = await this.ticketService.getTicket(args.ticket_number);
timer();
return result; // TicketService já retorna formato MCP
} catch (error) {
timer();
this.logger.error('Failed to handle get ticket', {
ticketId: args.ticket_id,
error: error.message
});
return this._formatErrorResponse(error, 'get_ticket');
}
}
/**
* Handler para criar um novo ticket
*/
async handleCreateTicket(args) {
const timer = this.logger.startTimer('handle_create_ticket');
try {
this.logger.info('Handling create ticket request', {
hasTitle: !!args.title,
hasDescription: !!args.description,
hasClientId: !!args.client_id,
hasClientName: !!args.client_name
});
// Validação básica de parâmetros (domain validator faz validação completa)
if (!args.title || !args.description) {
throw new ValidationError('title e description são obrigatórios');
}
if (!args.client_id && !args.client_name) {
throw new ValidationError('client_id ou client_name é obrigatório');
}
// Usar orchestrator para resolução automática de cliente se necessário
const result = await this.domainOrchestrator.createTicketWithClientResolution(args);
timer();
return result; // TicketService já retorna formato MCP
} catch (error) {
timer();
this.logger.error('Failed to handle create ticket', {
title: args.title?.substring(0, 50),
error: error.message
});
return this._formatErrorResponse(error, 'create_ticket');
}
}
/**
* Handler para atualizar um ticket existente
*/
async handleUpdateTicket(args) {
const timer = this.logger.startTimer('handle_update_ticket');
try {
this.logger.info('Handling update ticket request', {
ticketNumber: args.ticket_number,
fields: Object.keys(args).filter(key => key !== 'ticket_number')
});
// Validação básica
if (!args.ticket_number) {
throw new ValidationError('ticket_number é obrigatório');
}
// Extrai dados de atualização (remove ticket_number)
const updateData = { ...args };
delete updateData.ticket_number;
if (Object.keys(updateData).length === 0) {
throw new ValidationError('Pelo menos um campo deve ser fornecido para atualização');
}
// Delega para o domain service
const result = await this.ticketService.updateTicket(args.ticket_number, updateData);
timer();
return result;
} catch (error) {
timer();
this.logger.error('Failed to handle update ticket', {
ticketNumber: args.ticket_number,
error: error.message
});
return this._formatErrorResponse(error, 'update_ticket');
}
}
/**
* Handler para listar tickets com filtros
*/
async handleListTickets(args) {
const timer = this.logger.startTimer('handle_list_tickets');
try {
this.logger.info('Handling list tickets request', {
filters: Object.keys(args || {}),
hasDeskIds: !!args?.desk_ids,
hasClientIds: !!args?.client_ids
});
// Args pode ser vazio, domain validator validará filtros obrigatórios
const filters = args || {};
// Delega para o domain service
const result = await this.ticketService.listTickets(filters);
timer();
return result;
} catch (error) {
timer();
this.logger.error('Failed to handle list tickets', {
filters: Object.keys(args || {}),
error: error.message
});
return this._formatErrorResponse(error, 'list_tickets');
}
}
/**
* Handler para fechar um ticket específico
*/
async handleCloseTicket(args) {
const timer = this.logger.startTimer('handle_close_ticket');
try {
this.logger.info('Handling close ticket request', {
ticketNumber: args.ticket_number
});
// Validação básica de parâmetros
if (!args.ticket_number) {
throw new ValidationError('ticket_number é obrigatório');
}
// Delega para o domain service
const result = await this.ticketService.closeTicket(args.ticket_number);
timer();
return result; // TicketService já retorna formato MCP
} catch (error) {
timer();
this.logger.error('Failed to handle close ticket', {
ticketNumber: args.ticket_number,
error: error.message
});
return this._formatErrorResponse(error, 'close_ticket');
}
}
/**
* Handler para buscar arquivos de um ticket
*/
async handleGetTicketFiles(args) {
const timer = this.logger.startTimer('handle_get_ticket_files');
try {
this.logger.info('Handling get ticket files request', {
ticketNumber: args.ticket_number
});
// Validação básica de parâmetros
if (!args.ticket_number) {
throw new ValidationError('ticket_number é obrigatório');
}
// Delega para o domain service
const result = await this.ticketService.getTicketFiles(args.ticket_number);
timer();
return result; // TicketService já retorna formato MCP
} catch (error) {
timer();
this.logger.error('Failed to handle get ticket files', {
ticketNumber: args.ticket_number,
error: error.message
});
return this._formatErrorResponse(error, 'get_ticket_files');
}
}
/**
* Formata resposta de erro padronizada
*/
_formatErrorResponse(error, operation) {
// Tipos de erro conhecidos
const errorMap = {
ValidationError: '❌',
NotFoundError: '🔍',
APIError: '🔌',
TimeoutError: '⏱️',
NetworkError: '🌐'
};
const icon = errorMap[error.constructor.name] || '❌';
const operationText = {
get_ticket: 'buscar ticket',
create_ticket: 'criar ticket',
update_ticket: 'atualizar ticket',
list_tickets: 'listar tickets',
close_ticket: 'fechar ticket',
get_ticket_files: 'buscar arquivos do ticket'
};
let errorMessage = error.message;
// Adiciona contexto específico para alguns erros
if (error.constructor.name === 'ValidationError') {
errorMessage = `Dados inválidos: ${error.message}`;
} else if (error.constructor.name === 'NotFoundError') {
errorMessage = `Recurso não encontrado: ${error.message}`;
} else if (error.constructor.name === 'APIError') {
errorMessage = `Erro na API: ${error.message}`;
if (error.statusCode) {
errorMessage += ` (HTTP ${error.statusCode})`;
}
}
return {
content: [
{
type: 'text',
text: `**${icon} Erro ao ${operationText[operation] || 'processar solicitação'}**\n\n` +
`**Erro:** ${errorMessage}\n\n` +
`*Verifique os parâmetros fornecidos e tente novamente.*`
}
]
};
}
/**
* Lazy loading do ResponseFormatter
*/
_getResponseFormatter() {
if (!this.responseFormatter) {
this.responseFormatter = this.container.resolve('responseFormatter');
}
return this.responseFormatter;
}
/**
* Estatísticas do handler
*/
getStats() {
return {
operations: ['get_ticket', 'create_ticket', 'update_ticket', 'list_tickets', 'close_ticket', 'get_ticket_files'],
features: {
domain_service_integration: true,
orchestrator_support: true,
error_formatting: true,
request_logging: true,
performance_timing: true
},
dependencies: {
ticketService: !!this.ticketService,
domainOrchestrator: !!this.domainOrchestrator,
logger: !!this.logger
}
};
}
}
// Import das classes de erro
const { ValidationError } = require('../../utils/errors');
module.exports = TicketHandler;