feat: MCP Server para control de impresoras
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m8s

- 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
This commit is contained in:
2025-11-25 12:41:49 -06:00
parent 583c29cd96
commit 0e86f9d7a9
5 changed files with 1206 additions and 17 deletions

View File

@@ -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

487
package-lock.json generated
View File

@@ -7,6 +7,7 @@
"name": "printerCentral", "name": "printerCentral",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.22.0",
"@nuxt/ui": "^4.1.0", "@nuxt/ui": "^4.1.0",
"@vite-pwa/nuxt": "^1.0.7", "@vite-pwa/nuxt": "^1.0.7",
"axios": "^1.13.2", "axios": "^1.13.2",
@@ -3027,6 +3028,47 @@
"node": ">=8" "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": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
@@ -6311,6 +6353,19 @@
"node": ">=6.5" "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": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -6375,6 +6430,23 @@
"url": "https://github.com/sponsors/epoberezkin" "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": { "node_modules/alien-signals": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz",
@@ -6837,6 +6909,46 @@
"integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==",
"license": "MIT" "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": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -6960,6 +7072,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/c12": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.2.tgz", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.2.tgz",
@@ -7364,18 +7485,58 @@
"node": "^14.18.0 || >=16.10.0" "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": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"license": "MIT" "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": { "node_modules/cookie-es": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
"integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==",
"license": "MIT" "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": { "node_modules/copy-anything": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
@@ -7418,6 +7579,19 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT" "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": { "node_modules/crc-32": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -8481,6 +8655,18 @@
"bare-events": "^2.7.0" "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": { "node_modules/eventsource-parser": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "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", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" "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": { "node_modules/exsolve": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
@@ -8665,6 +8908,23 @@
"node": ">=8" "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": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -8786,6 +9046,15 @@
"node": ">= 0.6" "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": { "node_modules/fraction.js": {
"version": "5.3.4", "version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -9310,28 +9579,23 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"depd": "2.0.0", "depd": "~2.0.0",
"inherits": "2.0.4", "inherits": "~2.0.4",
"setprototypeof": "1.2.0", "setprototypeof": "~1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.2",
"toidentifier": "1.0.1" "toidentifier": "~1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} },
}, "funding": {
"node_modules/http-errors/node_modules/statuses": { "type": "opencollective",
"version": "2.0.1", "url": "https://opencollective.com/express"
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
} }
}, },
"node_modules/http-shutdown": { "node_modules/http-shutdown": {
@@ -9517,6 +9781,15 @@
"url": "https://opencollective.com/ioredis" "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": { "node_modules/iron-webcrypto": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@@ -9842,6 +10115,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/is-reference": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -10761,6 +11040,27 @@
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"license": "CC0-1.0" "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": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -11001,6 +11301,15 @@
"integrity": "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==", "integrity": "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==",
"license": "MIT" "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": { "node_modules/nitropack": {
"version": "2.12.9", "version": "2.12.9",
"resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.12.9.tgz", "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.12.9.tgz",
@@ -11349,6 +11658,15 @@
"node": "^14.16.0 || >=16.10.0" "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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -11760,6 +12078,16 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC" "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": { "node_modules/path-type": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
@@ -11836,6 +12164,15 @@
"node": ">=12.13.0" "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": { "node_modules/pkg-types": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
@@ -12387,6 +12724,19 @@
"integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==",
"license": "MIT" "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -12402,6 +12752,21 @@
"node": ">=6" "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": { "node_modules/quansync": {
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -12462,6 +12827,37 @@
"node": ">= 0.6" "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": { "node_modules/rc9": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "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": { "node_modules/run-applescript": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
@@ -14073,6 +14485,20 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/type-level-regexp": {
"version": "0.1.17", "version": "0.1.17",
"resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz",
@@ -14406,6 +14832,15 @@
"node": ">= 10.0.0" "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": { "node_modules/unplugin": {
"version": "2.3.10", "version": "2.3.10",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
@@ -14805,6 +15240,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/vaul-vue": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/vaul-vue/-/vaul-vue-0.4.1.tgz", "resolved": "https://registry.npmjs.org/vaul-vue/-/vaul-vue-0.4.1.tgz",
@@ -16208,6 +16652,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "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"
}
} }
} }
} }

View File

@@ -10,6 +10,7 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.22.0",
"@nuxt/ui": "^4.1.0", "@nuxt/ui": "^4.1.0",
"@vite-pwa/nuxt": "^1.0.7", "@vite-pwa/nuxt": "^1.0.7",
"axios": "^1.13.2", "axios": "^1.13.2",

View File

@@ -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 })
}
}
}

414
server/utils/mcp.ts Normal file
View File

@@ -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<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}` })
}]
}
}
}