diff --git a/.claude/epson-tmu220-printing-guide.md b/.claude/epson-tmu220-printing-guide.md new file mode 100644 index 0000000..c744274 --- /dev/null +++ b/.claude/epson-tmu220-printing-guide.md @@ -0,0 +1,206 @@ +# Guia de Impresion para Epson TM-U220 + +Esta guia documenta las mejores practicas para generar impresiones legibles en impresoras Epson TM-U220 usando el sistema printerCentral. + +## Especificaciones de la Impresora + +| Caracteristica | Valor | +|----------------|-------| +| Columnas (Font A) | 40-42 caracteres | +| Columnas (Font B) | 33-35 caracteres | +| Columnas con doble ancho | ~20 caracteres | +| Caracteres por pulgada | 17.8/16 cpi | +| Ancho de papel | 57.5mm, 69.5mm, o 76mm | + +## Reglas Fundamentales + +### 1. Limite de Caracteres por Linea + +``` +Texto normal: max 40 caracteres +Texto doble ancho: max 20 caracteres +Texto doble alto: max 40 caracteres +Texto doble ambos: max 20 caracteres +``` + +### 2. Caracteres Soportados + +**USAR** - Caracteres ASCII basicos: +``` += - _ * + | / \ ( ) [ ] < > +A-Z a-z 0-9 +``` + +**EVITAR** - Caracteres Unicode extendidos: +``` +Box drawing: ╔ ═ ╗ ║ ╚ ╝ ┌ ┐ └ ┘ +Simbolos especiales: → ← ↑ ↓ ★ ● ○ +Emojis: cualquiera +``` + +### 3. Espaciado con Feed + +Siempre usar `feed` para crear espacio visual. La impresora NO interpreta lineas vacias automaticamente. + +```json +{ "op": "feed", "lines": 1 } // Espacio simple +{ "op": "feed", "lines": 2 } // Espacio entre secciones +{ "op": "feed", "lines": 4 } // Espacio antes del corte +``` + +## Estructura Recomendada de un Ticket + +### Header (Centrado, Destacado) + +```json +{ "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 } +``` + +### Separadores + +Siempre con feed antes y despues para que queden en su propia linea: + +```json +{ "op": "text", "value": "================================" }, +{ "op": "feed", "lines": 1 } +``` + +Para 40 columnas usar 32 caracteres `=` (deja margen visual). + +### Titulos de Seccion + +Usar bold + underline, seguido de feed: + +```json +{ "op": "style", "bold": true, "underline": true }, +{ "op": "text", "value": "NOMBRE SECCION" }, +{ "op": "style", "bold": false, "underline": false }, +{ "op": "feed", "lines": 1 } +``` + +### Items de Lista + +Cada item con su propio feed para legibilidad: + +```json +{ "op": "text", "value": "[ ] Item uno" }, +{ "op": "feed", "lines": 1 }, +{ "op": "text", "value": "[ ] Item dos" }, +{ "op": "feed", "lines": 1 } +``` + +### Footer y Corte + +```json +{ "op": "align", "align": "center" }, +{ "op": "text", "value": "Texto de pie" }, +{ "op": "feed", "lines": 4 }, +{ "op": "cut" } +``` + +## Ejemplo Completo: Lista de Compras + +```json +{ + "operations": [ + { "op": "align", "align": "center" }, + { "op": "style", "bold": true, "width": 2, "height": 2 }, + { "op": "text", "value": "MI TITULO" }, + { "op": "style", "bold": false, "width": 1, "height": 1 }, + { "op": "feed", "lines": 2 }, + + { "op": "text", "value": "================================" }, + { "op": "feed", "lines": 1 }, + { "op": "style", "bold": true }, + { "op": "text", "value": "SUBTITULO" }, + { "op": "feed", "lines": 1 }, + { "op": "style", "bold": false }, + { "op": "text", "value": "================================" }, + { "op": "feed", "lines": 2 }, + + { "op": "align", "align": "left" }, + { "op": "style", "bold": true, "underline": true }, + { "op": "text", "value": "SECCION 1" }, + { "op": "style", "bold": false, "underline": false }, + { "op": "feed", "lines": 1 }, + { "op": "text", "value": "[ ] Item A" }, + { "op": "feed", "lines": 1 }, + { "op": "text", "value": "[ ] Item B" }, + { "op": "feed", "lines": 2 }, + + { "op": "align", "align": "center" }, + { "op": "text", "value": "================================" }, + { "op": "feed", "lines": 1 }, + { "op": "text", "value": "Pie de pagina" }, + { "op": "feed", "lines": 4 }, + { "op": "cut" } + ] +} +``` + +## Operaciones Disponibles + +| Operacion | Parametros | Ejemplo | +|-----------|------------|---------| +| `text` | `value` | `{ "op": "text", "value": "Hola" }` | +| `feed` | `lines` | `{ "op": "feed", "lines": 2 }` | +| `cut` | - | `{ "op": "cut" }` | +| `align` | `align`: left, center, right | `{ "op": "align", "align": "center" }` | +| `style` | `bold`, `underline`, `width`, `height` | `{ "op": "style", "bold": true }` | + +## Endpoints de Impresion + +### Imprimir Template Guardado + +``` +POST /api/print/template +{ + "templateId": "template_xxx", + "variables": { "nombre": "Juan" }, + "printerId": "printer_xxx", // opcional + "dryRun": false // true para preview +} +``` + +### Imprimir Operaciones Directas + +``` +POST /api/print/raw +{ + "operations": [...], + "variables": { "var1": "valor1" }, + "printerId": "printer_xxx", // opcional + "dryRun": false +} +``` + +## Variables en Templates + +Sintaxis: `{{nombre}}` o `{{nombre:label:default}}` + +```json +{ "op": "text", "value": "Cliente: {{cliente}}" }, +{ "op": "text", "value": "Fecha: {{fecha:Fecha:2025-01-01}}" } +``` + +## Errores Comunes + +1. **Texto cortado o ilegible**: Excediste 40 caracteres por linea +2. **Todo pegado**: Falta usar `feed` entre elementos +3. **Caracteres raros**: Usaste Unicode no soportado +4. **Separadores mezclados**: Falta `feed` antes/despues de lineas `===` +5. **Header muy largo**: Con doble ancho solo caben ~20 chars + +## Checklist Pre-Impresion + +- [ ] Ninguna linea excede 40 caracteres (20 si doble ancho) +- [ ] Solo caracteres ASCII basicos +- [ ] `feed` entre cada item para legibilidad +- [ ] `feed` antes y despues de separadores `===` +- [ ] `feed: 4` antes del `cut` final +- [ ] Header centrado con estilo destacado +- [ ] Secciones con titulos bold+underline diff --git a/package-lock.json b/package-lock.json index a539610..0523221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "printerCentral", "hasInstallScript": true, "dependencies": { + "@modelcontextprotocol/sdk": "^1.22.0", "@nuxt/ui": "^4.1.0", "@vite-pwa/nuxt": "^1.0.7", "axios": "^1.13.2", @@ -3027,6 +3028,47 @@ "node": ">=8" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz", + "integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", @@ -6311,6 +6353,19 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -6375,6 +6430,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/alien-signals": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz", @@ -6837,6 +6909,46 @@ "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -6960,6 +7072,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c12": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.2.tgz", @@ -7364,18 +7485,58 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-es": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "license": "MIT" }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -7418,6 +7579,19 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -8481,6 +8655,18 @@ "bare-events": "^2.7.0" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -8518,6 +8704,63 @@ "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -8665,6 +8908,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -8786,6 +9046,15 @@ "node": ">= 0.6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -9310,28 +9579,23 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-shutdown": { @@ -9517,6 +9781,15 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -9842,6 +10115,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -10761,6 +11040,27 @@ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -11001,6 +11301,15 @@ "integrity": "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==", "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nitropack": { "version": "2.12.9", "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.12.9.tgz", @@ -11349,6 +11658,15 @@ "node": "^14.16.0 || >=16.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -11760,6 +12078,16 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", @@ -11836,6 +12164,15 @@ "node": ">=12.13.0" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -12387,6 +12724,19 @@ "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12402,6 +12752,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -12462,6 +12827,37 @@ "node": ">= 0.6" } }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -12860,6 +13256,22 @@ } } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -14073,6 +14485,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/type-level-regexp": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", @@ -14406,6 +14832,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unplugin": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", @@ -14805,6 +15240,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vaul-vue": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/vaul-vue/-/vaul-vue-0.4.1.tgz", @@ -16208,6 +16652,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index b282d99..8a67e08 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.22.0", "@nuxt/ui": "^4.1.0", "@vite-pwa/nuxt": "^1.0.7", "axios": "^1.13.2", diff --git a/server/api/mcp/index.post.ts b/server/api/mcp/index.post.ts new file mode 100644 index 0000000..4953405 --- /dev/null +++ b/server/api/mcp/index.post.ts @@ -0,0 +1,115 @@ +// MCP Server Endpoint - JSON-RPC 2.0 over HTTP +// Protocolo MCP para agentes de IA + +import { MCP_TOOLS, handleToolCall } from '../../utils/mcp' + +interface JsonRpcRequest { + jsonrpc: '2.0' + id: string | number + method: string + params?: any +} + +interface JsonRpcResponse { + jsonrpc: '2.0' + id: string | number | null + result?: any + error?: { + code: number + message: string + data?: any + } +} + +export default defineEventHandler(async (event) => { + try { + const body = await readBody(event) as JsonRpcRequest + + // Validar JSON-RPC + if (body.jsonrpc !== '2.0') { + return createJsonRpcError(body.id, -32600, 'Invalid Request: jsonrpc must be "2.0"') + } + + if (!body.method) { + return createJsonRpcError(body.id, -32600, 'Invalid Request: method is required') + } + + // Manejar métodos MCP + switch (body.method) { + case 'initialize': { + // Handshake inicial del protocolo MCP + return createJsonRpcResponse(body.id, { + protocolVersion: '2024-11-05', + capabilities: { + tools: {} + }, + serverInfo: { + name: 'printercentral-mcp', + version: '1.0.0' + } + }) + } + + case 'initialized': { + // Notificación de que el cliente está listo + return createJsonRpcResponse(body.id, {}) + } + + case 'tools/list': { + // Listar todas las tools disponibles + return createJsonRpcResponse(body.id, { + tools: MCP_TOOLS + }) + } + + case 'tools/call': { + // Ejecutar una tool + const { name, arguments: args } = body.params || {} + + if (!name) { + return createJsonRpcError(body.id, -32602, 'Invalid params: tool name is required') + } + + // Verificar que la tool existe + const tool = MCP_TOOLS.find(t => t.name === name) + if (!tool) { + return createJsonRpcError(body.id, -32602, `Tool not found: ${name}`) + } + + // Ejecutar la tool + const result = await handleToolCall(name, args || {}) + return createJsonRpcResponse(body.id, result) + } + + case 'ping': { + return createJsonRpcResponse(body.id, {}) + } + + default: + return createJsonRpcError(body.id, -32601, `Method not found: ${body.method}`) + } + } catch (err: any) { + console.error('MCP Error:', err) + return createJsonRpcError(null, -32603, `Internal error: ${err.message}`) + } +}) + +function createJsonRpcResponse(id: string | number | null, result: any): JsonRpcResponse { + return { + jsonrpc: '2.0', + id: id ?? null, + result + } +} + +function createJsonRpcError(id: string | number | null, code: number, message: string, data?: any): JsonRpcResponse { + return { + jsonrpc: '2.0', + id: id ?? null, + error: { + code, + message, + ...(data && { data }) + } + } +} diff --git a/server/utils/mcp.ts b/server/utils/mcp.ts new file mode 100644 index 0000000..848c6f8 --- /dev/null +++ b/server/utils/mcp.ts @@ -0,0 +1,414 @@ +// 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): 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}` }) + }] + } + } +}