@zincapp/mcp-server 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,8 @@ import { registerApplyAuthorization } from './apply-authorization.js';
16
16
  import { registerLinkGestorAuthorizations } from './link-gestor-authorizations.js';
17
17
  import { registerProposeNt } from './propose-nt.js';
18
18
  import { registerLookupAuthorization } from './lookup-authorization.js';
19
+ import { registerNtFinalGestor } from './nt-final-gestor.js';
20
+ import { registerUpsertGestor } from './upsert-gestor.js';
19
21
  export function registerAllTools(server, client) {
20
22
  registerSearchDocs(server, client);
21
23
  registerReadDoc(server, client);
@@ -35,4 +37,6 @@ export function registerAllTools(server, client) {
35
37
  registerLinkGestorAuthorizations(server, client);
36
38
  registerProposeNt(server, client);
37
39
  registerLookupAuthorization(server, client);
40
+ registerNtFinalGestor(server, client);
41
+ registerUpsertGestor(server, client);
38
42
  }
@@ -0,0 +1,12 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ /** Límite de tamaño de PDF, alineado con el importer (LapazImportEndpoint.kt:62 → 25 MB/PDF). */
4
+ export declare const MAX_PDF_BYTES: number;
5
+ /** Valida que los bytes son un PDF (`%PDF` magic) y no exceden el límite. Puro y exportado para test. */
6
+ export declare function validatePdf(bytes: Buffer): {
7
+ ok: true;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ export declare function registerNtFinalGestor(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolveElevation } from './elevation.js';
4
+ /** Límite de tamaño de PDF, alineado con el importer (LapazImportEndpoint.kt:62 → 25 MB/PDF). */
5
+ export const MAX_PDF_BYTES = 25 * 1024 * 1024;
6
+ /** Valida que los bytes son un PDF (`%PDF` magic) y no exceden el límite. Puro y exportado para test. */
7
+ export function validatePdf(bytes) {
8
+ if (bytes.length < 5 || bytes.subarray(0, 5).toString('latin1') !== '%PDF-') {
9
+ return { ok: false, error: 'El fichero no es un PDF (no empieza por "%PDF-").' };
10
+ }
11
+ if (bytes.length > MAX_PDF_BYTES) {
12
+ return { ok: false, error: `El PDF es demasiado grande (${bytes.length} bytes; máximo ${MAX_PDF_BYTES}, 25 MB).` };
13
+ }
14
+ return { ok: true };
15
+ }
16
+ /** Resuelve el base64 del PDF desde EXACTAMENTE uno de pdfPath | pdfBase64. */
17
+ function resolvePdfBase64(pdfPath, pdfBase64) {
18
+ const hasPath = typeof pdfPath === 'string' && pdfPath.length > 0;
19
+ const hasB64 = typeof pdfBase64 === 'string' && pdfBase64.length > 0;
20
+ if (hasPath === hasB64) {
21
+ return { ok: false, error: 'Aporta EXACTAMENTE uno de pdfPath o pdfBase64 (no ambos, no ninguno).' };
22
+ }
23
+ let bytes;
24
+ if (hasPath) {
25
+ try {
26
+ bytes = readFileSync(pdfPath);
27
+ }
28
+ catch (e) {
29
+ return { ok: false, error: `No pude leer el PDF en '${pdfPath}': ${e.message}` };
30
+ }
31
+ }
32
+ else {
33
+ try {
34
+ bytes = Buffer.from(pdfBase64, 'base64');
35
+ }
36
+ catch (e) {
37
+ return { ok: false, error: `pdfBase64 no es base64 válido: ${e.message}` };
38
+ }
39
+ }
40
+ const v = validatePdf(bytes);
41
+ if (!v.ok)
42
+ return v;
43
+ return { ok: true, base64: bytes.toString('base64') };
44
+ }
45
+ const errorContent = (text) => ({ content: [{ type: 'text', text }] });
46
+ export function registerNtFinalGestor(server, client) {
47
+ server.tool('preview_nt_final_gestor', 'DRY-RUN: extrae por IA el "Gestor final para la operación Dxx" del PDF de una Notificación Previa ' +
48
+ '(NT), lo resuelve contra las entidades/centros de tu tenant, valida las invariantes E3L ' +
49
+ '(operación en Table2Type + autorización por LER del centro), y devuelve una PROPUESTA opaca ' +
50
+ '(con proposalId) SIN escribir nada. Aporta el PDF con pdfPath (ruta local) o pdfBase64 ' +
51
+ '(exactamente uno). Revisa la propuesta y, si es correcta, aplícala con apply_nt_final_gestor. ' +
52
+ 'Corre bajo tu elevación de operador (company derivada de ella).', {
53
+ ntNumero: z.string().describe('El número de la NT objetivo (p.ej. "NT30280008283420220064303")'),
54
+ pdfPath: z.string().optional().describe('Ruta local al PDF de la NT (alternativa a pdfBase64)'),
55
+ pdfBase64: z.string().optional().describe('El PDF de la NT en base64 (alternativa a pdfPath)'),
56
+ }, async ({ ntNumero, pdfPath, pdfBase64 }) => {
57
+ const pdf = resolvePdfBase64(pdfPath, pdfBase64);
58
+ if (!pdf.ok)
59
+ return errorContent(`# Error\n${pdf.error}`);
60
+ const elev = await resolveElevation(client);
61
+ if ('content' in elev)
62
+ return elev;
63
+ const p = await client.post('/mwm/retirada/preview-nt-final-gestor', { ntNumero, pdfBase64: pdf.base64 });
64
+ if (!p.resoluble) {
65
+ return errorContent(`# Propuesta · NT ${p.ntNumero} — NO RESOLUBLE\n\n` +
66
+ `**Razón:** ${p.razon ?? 'desconocida'}\n\n` +
67
+ `Terna actual en la NT: ${fmtTriple(p.ternaActual)}. No se ha persistido nada.`);
68
+ }
69
+ const text = `# Propuesta · NT ${p.ntNumero}\n\n` +
70
+ `- **proposalId:** \`${p.proposalId}\`\n` +
71
+ `- **Nº de NT en el documento:** ${p.ntNumeroDocumento ?? '—'}\n` +
72
+ `- **Gestor final propuesto:** ${fmtTriple(p.gestorFinal)}\n` +
73
+ `- **Terna actual en la NT:** ${fmtTriple(p.ternaActual)}\n\n` +
74
+ `Si es correcto, aplica con \`apply_nt_final_gestor({ proposalId: "${p.proposalId}" })\`. ` +
75
+ `La propuesta caduca (TTL) — aplícala pronto.`;
76
+ return { content: [{ type: 'text', text }] };
77
+ });
78
+ server.tool('apply_nt_final_gestor', 'Aplica una propuesta de gestor final previamente revisada (por proposalId de preview_nt_final_gestor). ' +
79
+ 'NO re-ejecuta la IA ni re-transporta el PDF. Escribe la terna (clientId, centerId, operación) en ' +
80
+ 'la NT bajo tu elevación de operador, por el framework (hooks + auditoría), con regla ' +
81
+ 'complete-what\'s-missing (si la NT ya tiene un gestor final distinto → conflicto, no se ' +
82
+ 'sobrescribe). Sólo aplica propuestas que hayas verificado.', {
83
+ proposalId: z.string().describe('El proposalId devuelto por preview_nt_final_gestor'),
84
+ }, async ({ proposalId }) => {
85
+ const elev = await resolveElevation(client);
86
+ if ('content' in elev)
87
+ return elev;
88
+ const r = await client.post('/mwm/retirada/apply-nt-final-gestor', { proposalId });
89
+ const titulo = r.outcome === 'applied'
90
+ ? `# Aplicado · NT ${r.ntNumero}`
91
+ : `# Sin cambios · NT ${r.ntNumero}`;
92
+ const text = `${titulo}\n\n` +
93
+ `- **Resultado:** ${r.outcome}\n` +
94
+ `- **Gestor final:** ${fmtTriple(r.gestorFinal)}\n\n` +
95
+ `_Recuerda des-elevarte desde el portal web con un motivo cuando termines._`;
96
+ return { content: [{ type: 'text', text }] };
97
+ });
98
+ }
99
+ function fmtTriple(t) {
100
+ if (!t || (t.clientId == null && t.centerId == null && t.operacion == null))
101
+ return '(vacía)';
102
+ return `entidad ${t.clientId ?? '—'} / centro ${t.centerId ?? '—'} · operación ${t.operacion ?? '—'}`;
103
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerUpsertGestor(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
3
+ export function registerUpsertGestor(server, client) {
4
+ server.tool('upsert_entidad', 'Alta o completado (best-effort) de una entidad gestor/transportista por CIF, bajo la elevación del ' +
5
+ 'operador. Idempotente: si el CIF ya existe la completa (solo los campos aportados), si no la crea. ' +
6
+ 'No valida E3L: rellena lo disponible.', {
7
+ cif: z.string().describe('CIF de la entidad (identidad; requerido).'),
8
+ nombre: z.string().optional(),
9
+ entityType: z.number().int().optional().describe('tipo_entidad (E3L lo exige a nivel entidad).'),
10
+ telefono: z.string().optional(),
11
+ mail: z.string().optional(),
12
+ gestor: z.boolean().optional(),
13
+ transportista: z.boolean().optional(),
14
+ }, async (args) => {
15
+ const elev = await resolveElevation(client);
16
+ if ('content' in elev)
17
+ return elev;
18
+ const res = await client.post('/mwm/retirada/upsert-entidad', { confirm: true, ...args });
19
+ const verbo = res.outcome === 'created' ? 'creada' : 'actualizada';
20
+ return {
21
+ content: [{
22
+ type: 'text',
23
+ text: `Entidad **${verbo}** (CIF \`${args.cif}\`) — clientId \`${res.clientId}\`.`,
24
+ }],
25
+ };
26
+ });
27
+ server.tool('upsert_centro', 'Alta o completado (best-effort) de un centro de un gestor, bajo la elevación del operador. Identidad ' +
28
+ 'por centerId o por nima inequívoco (varios con la misma nima → 409, aporta centerId). Valida un id de ' +
29
+ 'catálogo solo si se aporta. Si aportas authId+autorizacion, hace también el upsert de la autorización.', {
30
+ clientId: z.number().int().describe('clientId de la entidad padre (requerido; debe existir).'),
31
+ centerId: z.number().int().optional().describe('Identidad explícita del centro (si se conoce).'),
32
+ nima: z.number().int().optional().describe('NIMA para identificar el centro si no hay centerId.'),
33
+ nombre: z.string().optional(),
34
+ direccion: z.string().optional(),
35
+ ciudad: z.string().optional(),
36
+ cp: z.string().optional(),
37
+ regionId: z.number().int().optional(),
38
+ provinceId: z.number().int().optional(),
39
+ localityId: z.number().int().optional(),
40
+ vialId: z.number().int().optional(),
41
+ codigoCnae: z.string().optional().describe('Código CNAE (varchar(4), PK de IneCnae).'),
42
+ telefono: z.string().optional(),
43
+ mail: z.string().optional(),
44
+ authId: z.string().optional().describe('Tipo de autorización (T01/G0x) — con autorizacion, hace el upsert.'),
45
+ autorizacion: z.string().optional().describe('Número de registro de la autorización (lo aporta el humano).'),
46
+ }, async (args) => {
47
+ const elev = await resolveElevation(client);
48
+ if ('content' in elev)
49
+ return elev;
50
+ const res = await client.post('/mwm/retirada/upsert-centro', { confirm: true, ...args });
51
+ const verbo = res.outcome === 'created' ? 'creado' : 'actualizado';
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: `Centro **${verbo}** — clientId \`${res.clientId}\`, centerId \`${res.centerId}\`.`,
56
+ }],
57
+ };
58
+ });
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zincapp/mcp-server",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "MCP server for ZincApp — search docs, read API reference, execute sandbox calls, and audit/complete waste removals (retiradas)",
5
5
  "type": "module",
6
6
  "bin": {