// MCP Server para PrinterCentral // Permite a agentes de IA gestionar impresoras y templates import { getAllTemplates, getTemplateById, createTemplate, updateTemplate, resolveVariables } from './templates' import type { Operation } from './templates' import { getAllPrinters, getSelectedPrinter, getPrinterById } from './printers' import { buildFromOperations } from './eposBuilder' import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from './printer' // Guías de impresión por modelo export const PRINTING_GUIDES: Record = { 'TM-U220': { model: 'Epson TM-U220', type: 'Matricial (impact)', columns: 40, columnsDoubleWidth: 20, supportedChars: 'ASCII básico (A-Z, a-z, 0-9, símbolos comunes). Evitar Unicode especial.', rules: [ 'Máximo 40 caracteres por línea (20 con width:2)', 'Usar solo caracteres ASCII básicos', 'Evitar caracteres Unicode: ╔═║╚╝ y similares', 'Usar = - _ * para separadores', 'Siempre usar feed entre elementos para legibilidad', 'feed:1 entre items, feed:2 entre secciones', 'feed:4 antes del cut final', 'Los headers con width:2 deben ser cortos (max 20 chars)' ], operations: [ '{ op: "text", value: "texto" } - Imprime texto', '{ op: "feed", lines: N } - Avanza N líneas', '{ op: "cut" } - Corta el papel', '{ op: "align", align: "left|center|right" } - Alineación', '{ op: "style", bold: true, underline: true, width: 1-2, height: 1-2 } - Estilos' ], example: `[ { "op": "align", "align": "center" }, { "op": "style", "bold": true, "width": 2, "height": 2 }, { "op": "text", "value": "TITULO" }, { "op": "style", "bold": false, "width": 1, "height": 1 }, { "op": "feed", "lines": 2 }, { "op": "text", "value": "================================" }, { "op": "feed", "lines": 1 }, { "op": "align", "align": "left" }, { "op": "text", "value": "Contenido aquí" }, { "op": "feed", "lines": 4 }, { "op": "cut" } ]` }, 'TM-T20II': { model: 'Epson TM-T20II', type: 'Térmica', columns: 48, columnsDoubleWidth: 24, supportedChars: 'ASCII extendido, algunos caracteres Unicode soportados', rules: [ 'Máximo 48 caracteres por línea (24 con width:2)', 'Soporta más caracteres que matriciales', 'Impresión rápida y silenciosa', 'Usar feed para espaciado', 'Soporta códigos QR y de barras', 'Ideal para recibos y tickets' ], operations: [ '{ op: "text", value: "texto" } - Imprime texto', '{ op: "feed", lines: N } - Avanza N líneas', '{ op: "cut" } - Corta el papel', '{ op: "align", align: "left|center|right" } - Alineación', '{ op: "style", bold: true, underline: true, width: 1-2, height: 1-2 } - Estilos', '{ op: "qrcode", data: "texto", size: 4 } - Código QR', '{ op: "barcode", data: "123456", type: "ean13" } - Código de barras' ], example: `[ { "op": "align", "align": "center" }, { "op": "style", "bold": true, "width": 2, "height": 2 }, { "op": "text", "value": "RECIBO" }, { "op": "style", "bold": false, "width": 1, "height": 1 }, { "op": "feed", "lines": 1 }, { "op": "text", "value": "================================================" }, { "op": "feed", "lines": 1 }, { "op": "align", "align": "left" }, { "op": "text", "value": "Contenido aquí" }, { "op": "feed", "lines": 2 }, { "op": "qrcode", "data": "https://example.com", "size": 4 }, { "op": "feed", "lines": 4 }, { "op": "cut" } ]` }, 'TM-T88': { model: 'Epson TM-T88 (series)', type: 'Térmica', columns: 48, columnsDoubleWidth: 24, supportedChars: 'ASCII extendido, algunos caracteres Unicode soportados', rules: [ 'Máximo 48 caracteres por línea (24 con width:2)', 'Soporta más caracteres que matriciales', 'Impresión más rápida y silenciosa', 'Usar feed para espaciado', 'Soporta imágenes y códigos QR' ], operations: [ '{ op: "text", value: "texto" } - Imprime texto', '{ op: "feed", lines: N } - Avanza N líneas', '{ op: "cut" } - Corta el papel', '{ op: "align", align: "left|center|right" } - Alineación', '{ op: "style", bold: true, underline: true, width: 1-2, height: 1-2 } - Estilos', '{ op: "qrcode", data: "texto", size: 4 } - Código QR', '{ op: "barcode", data: "123456", type: "ean13" } - Código de barras' ], example: `[ { "op": "align", "align": "center" }, { "op": "style", "bold": true, "width": 2, "height": 2 }, { "op": "text", "value": "RECIBO" }, { "op": "style", "bold": false, "width": 1, "height": 1 }, { "op": "feed", "lines": 1 }, { "op": "qrcode", "data": "https://example.com", "size": 4 }, { "op": "feed", "lines": 2 }, { "op": "cut" } ]` } } // Obtener guía por modelo (búsqueda flexible) export function getPrintingGuide(model: string): typeof PRINTING_GUIDES[string] | null { // Búsqueda exacta if (PRINTING_GUIDES[model]) return PRINTING_GUIDES[model] // Búsqueda parcial (case-insensitive) const modelLower = model.toLowerCase() for (const [key, guide] of Object.entries(PRINTING_GUIDES)) { if (key.toLowerCase().includes(modelLower) || modelLower.includes(key.toLowerCase())) { return guide } } // Default a TM-U220 si no se encuentra return PRINTING_GUIDES['TM-U220'] } // Definición de las tools MCP export const MCP_TOOLS = [ { name: 'printercentral_list_templates', description: 'Lista todos los templates de impresión disponibles. Retorna id, nombre, descripción (con instrucciones de uso) y variables requeridas para cada template.', inputSchema: { type: 'object' as const, properties: {}, required: [] as string[] } }, { name: 'printercentral_list_printers', description: 'Lista todas las impresoras configuradas. Retorna id, nombre, host e indica cuál es la impresora por defecto.', inputSchema: { type: 'object' as const, properties: {}, required: [] as string[] } }, { name: 'printercentral_print_template', description: 'Imprime un template guardado con variables resueltas. Usa list_templates primero para ver los templates disponibles y sus variables.', inputSchema: { type: 'object' as const, properties: { templateId: { type: 'string', description: 'ID del template a imprimir' }, variables: { type: 'object', description: 'Objeto con las variables a resolver. Las claves son los nombres de las variables definidas en el template.', additionalProperties: { type: 'string' } }, printerId: { type: 'string', description: 'ID de la impresora a usar. Si no se especifica, usa la impresora por defecto.' } }, required: ['templateId'] } }, { name: 'printercentral_print_raw', description: `Imprime operaciones ePOS directamente. Útil para impresiones personalizadas sin crear un template. FORMATO DE OPERACIONES (para impresora TM-U220, max 40 caracteres por línea): - { op: "text", value: "texto" } - Imprime texto - { op: "feed", lines: N } - Avanza N líneas (usar entre elementos para legibilidad) - { op: "cut" } - Corta el papel - { op: "align", align: "left|center|right" } - Alinea el texto - { op: "style", bold: true/false, underline: true/false, width: 1-2, height: 1-2 } - Estilo de texto EJEMPLO: [ { "op": "align", "align": "center" }, { "op": "style", "bold": true, "width": 2, "height": 2 }, { "op": "text", "value": "TITULO" }, { "op": "style", "bold": false, "width": 1, "height": 1 }, { "op": "feed", "lines": 2 }, { "op": "text", "value": "Contenido aquí" }, { "op": "feed", "lines": 4 }, { "op": "cut" } ] REGLAS IMPORTANTES: - Max 40 caracteres por línea (20 con width:2) - Usar solo caracteres ASCII (evitar unicode especial) - Siempre terminar con feed y cut`, inputSchema: { type: 'object' as const, properties: { operations: { type: 'array', description: 'Array de operaciones ePOS a ejecutar', items: { type: 'object' } }, variables: { type: 'object', description: 'Variables a resolver en el texto. Usa sintaxis {{nombreVariable}} en los valores de text.', additionalProperties: { type: 'string' } }, printerId: { type: 'string', description: 'ID de la impresora a usar. Si no se especifica, usa la impresora por defecto.' } }, required: ['operations'] } }, { name: 'printercentral_create_template', description: 'Crea un nuevo template de impresión. El template queda guardado para uso futuro.', inputSchema: { type: 'object' as const, properties: { name: { type: 'string', description: 'Nombre del template' }, description: { type: 'string', description: 'Descripción del template. IMPORTANTE: Incluir instrucciones de uso y explicación de cada variable para que otros agentes sepan cómo usarlo.' }, operations: { type: 'array', description: 'Array de operaciones ePOS. Usa {{variable}} para definir variables.', items: { type: 'object' } } }, required: ['name', 'operations'] } }, { name: 'printercentral_update_template', description: 'Actualiza un template existente. Solo se actualizan los campos proporcionados.', inputSchema: { type: 'object' as const, properties: { templateId: { type: 'string', description: 'ID del template a actualizar' }, name: { type: 'string', description: 'Nuevo nombre del template' }, description: { type: 'string', description: 'Nueva descripción del template' }, operations: { type: 'array', description: 'Nuevas operaciones del template', items: { type: 'object' } } }, required: ['templateId'] } }, { name: 'printercentral_get_printing_guide', description: 'Obtiene la guía de formato para un modelo de impresora específico. Incluye límites de caracteres, reglas de formato, operaciones disponibles y ejemplos.', inputSchema: { type: 'object' as const, properties: { model: { type: 'string', description: 'Modelo de impresora (ej: "TM-U220", "TM-T88"). Si no se especifica, retorna todas las guías disponibles.' } }, required: [] as string[] } }, { name: 'printercentral_list_models', description: 'Lista todos los modelos de impresora soportados con sus características básicas.', inputSchema: { type: 'object' as const, properties: {}, required: [] as string[] } } ] // Handlers para cada tool export async function handleToolCall(toolName: string, args: Record): Promise<{ content: Array<{ type: string; text: string }> }> { switch (toolName) { case 'printercentral_list_templates': { const templates = await getAllTemplates() const result = templates.map(t => ({ id: t.id, name: t.name, description: t.description, variables: t.variables, createdAt: t.createdAt, updatedAt: t.updatedAt })) return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } } case 'printercentral_list_printers': { const printers = await getAllPrinters() const selected = await getSelectedPrinter() const result = printers.map(p => ({ id: p.id, name: p.name, host: p.host, deviceId: p.deviceId, model: p.model || 'No especificado', isDefault: p.isDefault, isSelected: selected?.id === p.id })) return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } } case 'printercentral_print_template': { const { templateId, variables = {}, printerId } = args // Obtener template const template = await getTemplateById(templateId) if (!template) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Template no encontrado: ${templateId}` }) }] } } // Resolver variables const resolvedOps = resolveVariables(template.operations, variables) // Construir XML const inner = buildFromOperations(resolvedOps) const soap = buildSoapEnvelope(inner) // Obtener impresora const printer = printerId ? await getPrinterById(printerId) : await getSelectedPrinter() if (!printer) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'No hay impresora configurada' }) }] } } // Enviar a impresora try { const result = await sendToPrinter(soap, printer.host, printer.deviceId, printer.timeout) const { success, code } = parsePrinterResponse(result.data) return { content: [{ type: 'text', text: JSON.stringify({ ok: success, templateName: template.name, printerUsed: { id: printer.id, name: printer.name }, code }) }] } } catch (err: any) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err.message }) }] } } } case 'printercentral_print_raw': { const { operations, variables = {}, printerId } = args if (!operations || !Array.isArray(operations) || operations.length === 0) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'operations es requerido y debe ser un array no vacío' }) }] } } // Resolver variables const resolvedOps = resolveVariables(operations as Operation[], variables) // Construir XML const inner = buildFromOperations(resolvedOps) const soap = buildSoapEnvelope(inner) // Obtener impresora const printer = printerId ? await getPrinterById(printerId) : await getSelectedPrinter() if (!printer) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'No hay impresora configurada' }) }] } } // Enviar a impresora try { const result = await sendToPrinter(soap, printer.host, printer.deviceId, printer.timeout) const { success, code } = parsePrinterResponse(result.data) return { content: [{ type: 'text', text: JSON.stringify({ ok: success, printerUsed: { id: printer.id, name: printer.name }, code }) }] } } catch (err: any) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err.message }) }] } } } case 'printercentral_create_template': { const { name, description, operations } = args if (!name || !operations) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'name y operations son requeridos' }) }] } } try { const template = await createTemplate({ name, description: description || '', operations }) return { content: [{ type: 'text', text: JSON.stringify({ ok: true, template: { id: template.id, name: template.name, variables: template.variables } }) }] } } catch (err: any) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err.message }) }] } } } case 'printercentral_update_template': { const { templateId, name, description, operations } = args if (!templateId) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'templateId es requerido' }) }] } } const updateData: any = {} if (name !== undefined) updateData.name = name if (description !== undefined) updateData.description = description if (operations !== undefined) updateData.operations = operations try { const template = await updateTemplate(templateId, updateData) if (!template) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Template no encontrado: ${templateId}` }) }] } } return { content: [{ type: 'text', text: JSON.stringify({ ok: true, template: { id: template.id, name: template.name, variables: template.variables } }) }] } } catch (err: any) { return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err.message }) }] } } } case 'printercentral_get_printing_guide': { const { model } = args if (model) { const guide = getPrintingGuide(model) return { content: [{ type: 'text', text: JSON.stringify(guide, null, 2) }] } } else { // Retornar todas las guías return { content: [{ type: 'text', text: JSON.stringify(PRINTING_GUIDES, null, 2) }] } } } case 'printercentral_list_models': { const models = Object.entries(PRINTING_GUIDES).map(([key, guide]) => ({ id: key, model: guide.model, type: guide.type, columns: guide.columns, columnsDoubleWidth: guide.columnsDoubleWidth })) return { content: [{ type: 'text', text: JSON.stringify(models, null, 2) }] } } default: return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Tool desconocida: ${toolName}` }) }] } } }