Files
printerCentral/server/utils/mcp.ts
josedario87 0e86f9d7a9
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m8s
feat: MCP Server para control de impresoras
- Endpoint HTTP JSON-RPC en /api/mcp
- 6 tools: list_templates, list_printers, print_template, print_raw, create_template, update_template
- Guia de formato para impresora TM-U220
- Protegido por Authentik forward auth
2025-11-25 12:41:49 -06:00

415 lines
12 KiB
TypeScript

// 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'
// 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']
}
}
]
// Handlers para cada tool
export async function handleToolCall(toolName: string, args: Record<string, any>): 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,
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 })
}]
}
}
}
default:
return {
content: [{
type: 'text',
text: JSON.stringify({ ok: false, error: `Tool desconocida: ${toolName}` })
}]
}
}
}