gesicomm-mcp 1.0.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/package.json +35 -33
  2. package/src/apiClient.js +100 -95
  3. package/src/server.js +196 -191
package/package.json CHANGED
@@ -1,33 +1,35 @@
1
- {
2
- "name": "gesicomm-mcp",
3
- "version": "1.0.0",
4
- "description": "Servidor MCP oficial de Gesicomm para Claude y Codex",
5
- "type": "module",
6
- "main": "src/index.js",
7
- "bin": {
8
- "gesicomm-mcp": "bin/cli.js"
9
- },
10
- "files": [
11
- "bin",
12
- "src"
13
- ],
14
- "publishConfig": {
15
- "access": "public"
16
- },
17
- "keywords": [
18
- "mcp",
19
- "model-context-protocol",
20
- "gesicomm",
21
- "ecommerce",
22
- "claude",
23
- "codex"
24
- ],
25
- "scripts": {
26
- "start": "node bin/cli.js"
27
- },
28
- "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.32.1",
30
- "dotenv": "^16.4.5",
31
- "zod": "^3.25.76"
32
- }
33
- }
1
+ {
2
+ "name": "gesicomm-mcp",
3
+ "version": "1.2.1",
4
+ "description": "Conector MCP de Gesicomm para tiendas y productos",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "gesicomm-mcp": "bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src/index.js",
13
+ "src/apiClient.js",
14
+ "src/server.js"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "gesicomm",
23
+ "ecommerce",
24
+ "claude",
25
+ "codex"
26
+ ],
27
+ "scripts": {
28
+ "start": "node bin/cli.js"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.32.1",
32
+ "dotenv": "^16.4.5",
33
+ "zod": "^3.25.76"
34
+ }
35
+ }
package/src/apiClient.js CHANGED
@@ -1,95 +1,100 @@
1
- /**
2
- * Cliente HTTP para comunicarse con la API de Gesicomm.
3
- */
4
-
5
- export class GesicommApiClient {
6
- constructor({ apiUrl, token, tiendaId }) {
7
- let url = (apiUrl || process.env.GESICOMM_API_URL || 'https://api.gesicomm.com/api').trim().replace(/\/+$/, '');
8
-
9
- // Si el usuario configuró el dominio del frontend (gesicomm.com), redirigir automáticamente a api.gesicomm.com/api
10
- if (url === 'https://gesicomm.com' || url === 'http://gesicomm.com' || url === 'https://gesicomm.com/api' || url === 'http://gesicomm.com/api') {
11
- url = 'https://api.gesicomm.com/api';
12
- } else if (url === 'https://www.gesicomm.com' || url === 'http://www.gesicomm.com' || url === 'https://www.gesicomm.com/api' || url === 'http://www.gesicomm.com/api') {
13
- url = 'https://api.gesicomm.com/api';
14
- } else if (!url.endsWith('/api') && !url.includes('localhost') && !url.includes('127.0.0.1')) {
15
- url = `${url}/api`;
16
- }
17
-
18
- this.apiUrl = url;
19
- this.token = token || process.env.GESICOMM_TOKEN || '';
20
- this.tiendaId = tiendaId || process.env.GESICOMM_TIENDA_ID || null;
21
- }
22
-
23
- async _request(path, options = {}) {
24
- if (!this.token) {
25
- throw new Error('Falta el token de autenticación de Gesicomm (GESICOMM_TOKEN). Generalo desde Gesicomm > Lienzo en blanco.');
26
- }
27
-
28
- const url = new URL(`${this.apiUrl}${path}`);
29
- if (this.tiendaId && !url.searchParams.has('tienda_id')) {
30
- url.searchParams.set('tienda_id', this.tiendaId);
31
- }
32
-
33
- const headers = {
34
- 'Authorization': `Bearer ${this.token}`,
35
- 'Content-Type': 'application/json',
36
- ...(options.headers || {}),
37
- };
38
-
39
- const res = await fetch(url.toString(), {
40
- ...options,
41
- headers,
42
- });
43
-
44
- const rawText = await res.text();
45
- let data;
46
- try {
47
- data = JSON.parse(rawText);
48
- } catch {
49
- if (rawText.includes('<!DOCTYPE') || rawText.includes('<html')) {
50
- throw new Error(`El servidor en ${this.apiUrl} respondió con una página HTML en lugar de JSON. Si estás probando en desarrollo local, usá http://localhost:5000/api o asegurate de que el backend en producción tenga desplegados los endpoints /api/mcp.`);
51
- }
52
- throw new Error(`Respuesta inválida del servidor (código ${res.status}): ${rawText.substring(0, 150)}`);
53
- }
54
-
55
- if (!res.ok) {
56
- const msg = data.error || data.message || `Error en la solicitud a Gesicomm (${res.status})`;
57
- const err = new Error(msg);
58
- err.status = res.status;
59
- err.detalles = data;
60
- throw err;
61
- }
62
-
63
- return data;
64
- }
65
-
66
- async listarLandings() {
67
- const data = await this._request('/mcp/landings');
68
- return data.landings || [];
69
- }
70
-
71
- async obtenerContexto(landingId) {
72
- const data = await this._request(`/mcp/landings/${landingId}/contexto`);
73
- return data.contexto;
74
- }
75
-
76
- async obtenerCodigo(landingId, vista = 'inicio', contentId = null) {
77
- const query = new URLSearchParams({ vista });
78
- if (contentId) query.set('content_id', contentId);
79
- return this._request(`/mcp/landings/${landingId}/codigo?${query.toString()}`);
80
- }
81
-
82
- async validarCodigo(landingId, html, vista = 'inicio') {
83
- return this._request(`/mcp/landings/${landingId}/validar`, {
84
- method: 'POST',
85
- body: JSON.stringify({ html, vista }),
86
- });
87
- }
88
-
89
- async guardarCodigo(landingId, { vista = 'inicio', html, css = '', js = '', contentId = null }) {
90
- return this._request(`/mcp/landings/${landingId}/codigo`, {
91
- method: 'POST',
92
- body: JSON.stringify({ vista, html, css, js, contentId }),
93
- });
94
- }
95
- }
1
+ /**
2
+ * Cliente HTTP para comunicarse con la API de Gesicomm.
3
+ */
4
+
5
+ export class GesicommApiClient {
6
+ constructor({ apiUrl, token, tiendaId }) {
7
+ let url = (apiUrl || process.env.GESICOMM_API_URL || 'https://api.gesicomm.com/api').trim().replace(/\/+$/, '');
8
+
9
+ // Si el usuario configuró el dominio del frontend (gesicomm.com), redirigir automáticamente a api.gesicomm.com/api
10
+ if (url === 'https://gesicomm.com' || url === 'http://gesicomm.com' || url === 'https://gesicomm.com/api' || url === 'http://gesicomm.com/api') {
11
+ url = 'https://api.gesicomm.com/api';
12
+ } else if (url === 'https://www.gesicomm.com' || url === 'http://www.gesicomm.com' || url === 'https://www.gesicomm.com/api' || url === 'http://www.gesicomm.com/api') {
13
+ url = 'https://api.gesicomm.com/api';
14
+ } else if (!url.endsWith('/api') && !url.includes('localhost') && !url.includes('127.0.0.1')) {
15
+ url = `${url}/api`;
16
+ }
17
+
18
+ this.apiUrl = url;
19
+ this.token = token || process.env.GESICOMM_TOKEN || '';
20
+ this.tiendaId = tiendaId || process.env.GESICOMM_TIENDA_ID || null;
21
+ }
22
+
23
+ async _request(path, options = {}) {
24
+ if (!this.token) {
25
+ throw new Error('Falta el token de autenticación de Gesicomm (GESICOMM_TOKEN). Generalo desde Gesicomm > Lienzo en blanco.');
26
+ }
27
+
28
+ const url = new URL(`${this.apiUrl}${path}`);
29
+ if (this.tiendaId && !url.searchParams.has('tienda_id')) {
30
+ url.searchParams.set('tienda_id', this.tiendaId);
31
+ }
32
+
33
+ const headers = {
34
+ 'Authorization': `Bearer ${this.token}`,
35
+ ...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }),
36
+ ...(options.headers || {}),
37
+ };
38
+
39
+ const res = await fetch(url.toString(), {
40
+ ...options,
41
+ headers,
42
+ });
43
+
44
+ const rawText = await res.text();
45
+ let data;
46
+ try {
47
+ data = JSON.parse(rawText);
48
+ } catch {
49
+ if (rawText.includes('<!DOCTYPE') || rawText.includes('<html')) {
50
+ throw new Error(`El servidor en ${this.apiUrl} respondió con una página HTML en lugar de JSON. Si estás probando en desarrollo local, usá http://localhost:5000/api o asegurate de que el backend en producción tenga desplegados los endpoints /api/mcp.`);
51
+ }
52
+ throw new Error(`Respuesta inválida del servidor (código ${res.status}): ${rawText.substring(0, 150)}`);
53
+ }
54
+
55
+ if (!res.ok) {
56
+ const msg = data.error || data.message || `Error en la solicitud a Gesicomm (${res.status})`;
57
+ const err = new Error(msg);
58
+ err.status = res.status;
59
+ err.detalles = data;
60
+ throw err;
61
+ }
62
+
63
+ return data;
64
+ }
65
+
66
+ async listarLandings() {
67
+ const data = await this._request('/mcp/landings');
68
+ return data.landings || [];
69
+ }
70
+
71
+ async crearProducto(form) {
72
+ return this._request('/mcp/productos', { method: 'POST', body: form });
73
+ }
74
+
75
+ async obtenerContexto(landingId) {
76
+ const data = await this._request(`/mcp/landings/${landingId}/contexto`);
77
+ return data.contexto;
78
+ }
79
+
80
+ async obtenerCodigo(landingId, vista = 'inicio', contentId = null, seccionId = null) {
81
+ const query = new URLSearchParams({ vista });
82
+ if (seccionId) query.set('seccion_id', seccionId);
83
+ if (contentId) query.set('content_id', contentId);
84
+ return this._request(`/mcp/landings/${landingId}/codigo?${query.toString()}`);
85
+ }
86
+
87
+ async validarCodigo(landingId, html, vista = 'inicio') {
88
+ return this._request(`/mcp/landings/${landingId}/validar`, {
89
+ method: 'POST',
90
+ body: JSON.stringify({ html, vista }),
91
+ });
92
+ }
93
+
94
+ async guardarCodigo(landingId, { vista = 'inicio', html, css, js, contentId = null, seccion_id, revision }) {
95
+ return this._request(`/mcp/landings/${landingId}/codigo`, {
96
+ method: 'POST',
97
+ body: JSON.stringify({ vista, html, css, js, contentId, seccion_id, revision }),
98
+ });
99
+ }
100
+ }
package/src/server.js CHANGED
@@ -1,191 +1,196 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { z } from 'zod';
3
- import { GesicommApiClient } from './apiClient.js';
4
-
5
- export function createGesicommServer(config = {}) {
6
- const client = new GesicommApiClient(config);
7
- const defaultLandingId = process.env.GESICOMM_LANDING_ID ? Number(process.env.GESICOMM_LANDING_ID) : null;
8
-
9
- const server = new McpServer({
10
- name: 'gesicomm-mcp',
11
- version: '1.0.0',
12
- });
13
-
14
- // Tool 1: Listar Landings
15
- server.tool(
16
- 'gesicomm_listar_landings',
17
- 'Lista las landings activas y en borrador de la tienda en Gesicomm para conocer sus IDs y títulos.',
18
- {},
19
- async () => {
20
- try {
21
- const landings = await client.listarLandings();
22
- return {
23
- content: [{
24
- type: 'text',
25
- text: JSON.stringify(landings, null, 2),
26
- }],
27
- };
28
- } catch (err) {
29
- return {
30
- isError: true,
31
- content: [{ type: 'text', text: `Error al listar landings: ${err.message}` }],
32
- };
33
- }
34
- }
35
- );
36
-
37
- // Tool 2: Obtener Contexto Comercial Completo
38
- server.tool(
39
- 'gesicomm_obtener_contexto_landing',
40
- 'Obtiene el contexto comercial completo de la landing: catálogo de productos con precios en Guaraníes (PYG), imágenes, variantes, ofertas (order bumps, packs), branding de la tienda y el contrato técnico de directivas obligatorias para que el carrito, los pedidos y el Píxel funcionen.',
41
- {
42
- landing_id: z.number().int().positive().optional().describe('ID numérico de la landing en Gesicomm (si se omite, usa GESICOMM_LANDING_ID)'),
43
- },
44
- async ({ landing_id }) => {
45
- const id = landing_id || defaultLandingId;
46
- if (!id) {
47
- return {
48
- isError: true,
49
- content: [{ type: 'text', text: 'Debes proporcionar landing_id o configurar GESICOMM_LANDING_ID.' }],
50
- };
51
- }
52
- try {
53
- const contexto = await client.obtenerContexto(id);
54
- if (!contexto) {
55
- throw new Error(`No se pudo obtener el contexto de la landing #${id}. Verificá que la landing exista y pertenezca a tu tienda.`);
56
- }
57
- return {
58
- content: [{
59
- type: 'text',
60
- text: JSON.stringify(contexto, null, 2),
61
- }],
62
- };
63
- } catch (err) {
64
- return {
65
- isError: true,
66
- content: [{ type: 'text', text: `Error al obtener contexto comercial de la landing #${id}: ${err.message}` }],
67
- };
68
- }
69
- }
70
- );
71
-
72
- // Tool 3: Leer Código de la Landing
73
- server.tool(
74
- 'gesicomm_leer_codigo_landing',
75
- 'Lee el código actual (HTML, CSS, JS) de una vista específica de la landing (inicio, producto, catalogo, checkout, legal) para poder inspeccionarlo, mejorarlo o modificarlo.',
76
- {
77
- landing_id: z.number().int().positive().optional().describe('ID numérico de la landing'),
78
- vista: z.enum(['inicio', 'producto', 'catalogo', 'checkout', 'legal']).default('inicio').describe('Vista a consultar'),
79
- content_id: z.string().optional().describe('Para fichas propias de un producto específico, el content_id (ej: "producto-15")'),
80
- },
81
- async ({ landing_id, vista, content_id }) => {
82
- const id = landing_id || defaultLandingId;
83
- if (!id) {
84
- return {
85
- isError: true,
86
- content: [{ type: 'text', text: 'Debes proporcionar landing_id o configurar GESICOMM_LANDING_ID.' }],
87
- };
88
- }
89
- try {
90
- const data = await client.obtenerCodigo(id, vista, content_id);
91
- return {
92
- content: [{
93
- type: 'text',
94
- text: JSON.stringify(data, null, 2),
95
- }],
96
- };
97
- } catch (err) {
98
- return {
99
- isError: true,
100
- content: [{ type: 'text', text: `Error al leer código de la landing #${id}: ${err.message}` }],
101
- };
102
- }
103
- }
104
- );
105
-
106
- // Tool 4: Validar Motor Comercial
107
- server.tool(
108
- 'gesicomm_validar_motor_comercial',
109
- 'Audita un fragmento o documento HTML antes de guardarlo, verificando que tenga los botones de compra (data-gesicomm-comprar/agregar), casillas de order bump, selector de variantes y binds de precios necesarios para procesar pedidos y activar el Píxel de Meta.',
110
- {
111
- html: z.string().describe('Código HTML a validar'),
112
- vista: z.enum(['inicio', 'producto', 'catalogo', 'checkout', 'legal']).default('inicio').describe('Vista a validar'),
113
- landing_id: z.number().int().positive().optional().describe('ID de la landing (opcional)'),
114
- },
115
- async ({ html, vista, landing_id }) => {
116
- const id = landing_id || defaultLandingId;
117
- try {
118
- let res;
119
- if (id) {
120
- res = await client.validarCodigo(id, html, vista);
121
- } else {
122
- // Validación local básica si no hay id
123
- const tieneComprar = /data-gesicomm-comprar/.test(html) || /data-gesicomm-agregar/.test(html);
124
- res = {
125
- valido: tieneComprar,
126
- errores: tieneComprar ? [] : ['Falta botón de compra: El HTML debe contener data-gesicomm-comprar o data-gesicomm-agregar.'],
127
- advertencias: [],
128
- sugerencias: [],
129
- };
130
- }
131
- return {
132
- content: [{
133
- type: 'text',
134
- text: JSON.stringify(res, null, 2),
135
- }],
136
- };
137
- } catch (err) {
138
- return {
139
- isError: true,
140
- content: [{ type: 'text', text: `Error en validación: ${err.message}` }],
141
- };
142
- }
143
- }
144
- );
145
-
146
- // Tool 5: Guardar Código en Gesicomm
147
- server.tool(
148
- 'gesicomm_guardar_codigo_landing',
149
- 'Guarda el código HTML/CSS/JS generado o modificado en la base de datos de Gesicomm. Valida automáticamente las directivas comerciales y sanitiza el código antes de persistirlo. La vista previa en Gesicomm se actualizará al instante.',
150
- {
151
- landing_id: z.number().int().positive().optional().describe('ID numérico de la landing'),
152
- vista: z.enum(['inicio', 'producto', 'catalogo', 'checkout', 'legal']).default('inicio').describe('Vista a actualizar'),
153
- html: z.string().describe('Código HTML de la vista'),
154
- css: z.string().optional().default('').describe('Estilos CSS de la vista'),
155
- js: z.string().optional().default('').describe('JavaScript de la vista (opcional)'),
156
- content_id: z.string().optional().describe('Content ID si es la ficha propia de un producto específico'),
157
- },
158
- async ({ landing_id, vista, html, css, js, content_id }) => {
159
- const id = landing_id || defaultLandingId;
160
- if (!id) {
161
- return {
162
- isError: true,
163
- content: [{ type: 'text', text: 'Debes proporcionar landing_id o configurar GESICOMM_LANDING_ID.' }],
164
- };
165
- }
166
- try {
167
- const resultado = await client.guardarCodigo(id, {
168
- vista,
169
- html,
170
- css,
171
- js,
172
- contentId: content_id,
173
- });
174
- return {
175
- content: [{
176
- type: 'text',
177
- text: JSON.stringify(resultado, null, 2),
178
- }],
179
- };
180
- } catch (err) {
181
- const detalles = err.detalles?.errores ? `\nErrores encontrados:\n- ${err.detalles.errores.join('\n- ')}` : '';
182
- return {
183
- isError: true,
184
- content: [{ type: 'text', text: `Error al guardar en Gesicomm: ${err.message}${detalles}` }],
185
- };
186
- }
187
- }
188
- );
189
-
190
- return server;
191
- }
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { GesicommApiClient } from './apiClient.js';
4
+
5
+ export function createGesicommServer(config = {}) {
6
+ const client = new GesicommApiClient(config);
7
+ const defaultLandingId = process.env.GESICOMM_LANDING_ID ? Number(process.env.GESICOMM_LANDING_ID) : null;
8
+
9
+ const server = new McpServer({
10
+ name: 'gesicomm-mcp',
11
+ version: '1.2.1',
12
+ });
13
+
14
+ // Tool 1: Listar Landings
15
+ server.tool(
16
+ 'gesicomm_listar_landings',
17
+ 'Lista las landings activas y en borrador de la tienda en Gesicomm para conocer sus IDs y títulos.',
18
+ {},
19
+ async () => {
20
+ try {
21
+ const landings = await client.listarLandings();
22
+ return {
23
+ content: [{
24
+ type: 'text',
25
+ text: JSON.stringify(landings, null, 2),
26
+ }],
27
+ };
28
+ } catch (err) {
29
+ return {
30
+ isError: true,
31
+ content: [{ type: 'text', text: `Error al listar landings: ${err.message}` }],
32
+ };
33
+ }
34
+ }
35
+ );
36
+
37
+ // Tool 2: Obtener Contexto Comercial Completo
38
+ server.tool(
39
+ 'gesicomm_obtener_contexto_landing',
40
+ 'Obtiene el contexto comercial completo de la landing: catálogo de productos con precios en Guaraníes (PYG), imágenes, variantes, ofertas (order bumps, packs), branding de la tienda y el contrato técnico de directivas obligatorias para que el carrito, los pedidos y el Píxel funcionen.',
41
+ {
42
+ landing_id: z.number().int().positive().optional().describe('ID numérico de la landing en Gesicomm (si se omite, usa GESICOMM_LANDING_ID)'),
43
+ },
44
+ async ({ landing_id }) => {
45
+ const id = landing_id || defaultLandingId;
46
+ if (!id) {
47
+ return {
48
+ isError: true,
49
+ content: [{ type: 'text', text: 'Debes proporcionar landing_id o configurar GESICOMM_LANDING_ID.' }],
50
+ };
51
+ }
52
+ try {
53
+ const contexto = await client.obtenerContexto(id);
54
+ if (!contexto) {
55
+ throw new Error(`No se pudo obtener el contexto de la landing #${id}. Verificá que la landing exista y pertenezca a tu tienda.`);
56
+ }
57
+ return {
58
+ content: [{
59
+ type: 'text',
60
+ text: JSON.stringify(contexto, null, 2),
61
+ }],
62
+ };
63
+ } catch (err) {
64
+ return {
65
+ isError: true,
66
+ content: [{ type: 'text', text: `Error al obtener contexto comercial de la landing #${id}: ${err.message}` }],
67
+ };
68
+ }
69
+ }
70
+ );
71
+
72
+ // Tool 3: Leer Código de la Landing
73
+ server.tool(
74
+ 'gesicomm_leer_codigo_landing',
75
+ 'Lee el código actual (HTML, CSS, JS) de una vista específica de la landing (inicio, producto, catalogo, categoria, checkout, legal) para poder inspeccionarlo, mejorarlo o modificarlo.',
76
+ {
77
+ landing_id: z.number().int().positive().optional().describe('ID numérico de la landing'),
78
+ vista: z.enum(['inicio', 'producto', 'catalogo', 'categoria', 'checkout', 'legal']).default('inicio').describe('Vista a consultar'),
79
+ seccion_id: z.string().regex(/^[a-zA-Z0-9_]+$/).optional().describe('Bloque de inicio a leer o editar (ej: anuncios). Omitir al leer para descubrir secciones.'),
80
+ content_id: z.string().optional().describe('Para fichas propias de un producto específico, el content_id (ej: "producto-15")'),
81
+ },
82
+ async ({ landing_id, vista, content_id, seccion_id }) => {
83
+ const id = landing_id || defaultLandingId;
84
+ if (!id) {
85
+ return {
86
+ isError: true,
87
+ content: [{ type: 'text', text: 'Debes proporcionar landing_id o configurar GESICOMM_LANDING_ID.' }],
88
+ };
89
+ }
90
+ try {
91
+ const data = await client.obtenerCodigo(id, vista, content_id, seccion_id);
92
+ return {
93
+ content: [{
94
+ type: 'text',
95
+ text: JSON.stringify(data, null, 2),
96
+ }],
97
+ };
98
+ } catch (err) {
99
+ return {
100
+ isError: true,
101
+ content: [{ type: 'text', text: `Error al leer código de la landing #${id}: ${err.message}` }],
102
+ };
103
+ }
104
+ }
105
+ );
106
+
107
+ // Tool 4: Validar Motor Comercial
108
+ server.tool(
109
+ 'gesicomm_validar_motor_comercial',
110
+ 'Audita un fragmento o documento HTML antes de guardarlo, verificando que tenga los botones de compra (data-gesicomm-comprar/agregar), casillas de order bump, selector de variantes y binds de precios necesarios para procesar pedidos y activar el Píxel de Meta.',
111
+ {
112
+ html: z.string().describe('Código HTML a validar'),
113
+ vista: z.enum(['inicio', 'producto', 'catalogo', 'categoria', 'checkout', 'legal']).default('inicio').describe('Vista a validar'),
114
+ landing_id: z.number().int().positive().optional().describe('ID de la landing (opcional)'),
115
+ },
116
+ async ({ html, vista, landing_id }) => {
117
+ const id = landing_id || defaultLandingId;
118
+ try {
119
+ let res;
120
+ if (id) {
121
+ res = await client.validarCodigo(id, html, vista);
122
+ } else {
123
+ // Validación local básica si no hay id
124
+ const tieneComprar = /data-gesicomm-comprar/.test(html) || /data-gesicomm-agregar/.test(html);
125
+ res = {
126
+ valido: tieneComprar,
127
+ errores: tieneComprar ? [] : ['Falta botón de compra: El HTML debe contener data-gesicomm-comprar o data-gesicomm-agregar.'],
128
+ advertencias: [],
129
+ sugerencias: [],
130
+ };
131
+ }
132
+ return {
133
+ content: [{
134
+ type: 'text',
135
+ text: JSON.stringify(res, null, 2),
136
+ }],
137
+ };
138
+ } catch (err) {
139
+ return {
140
+ isError: true,
141
+ content: [{ type: 'text', text: `Error en validación: ${err.message}` }],
142
+ };
143
+ }
144
+ }
145
+ );
146
+
147
+ // Tool 5: Guardar Código en Gesicomm
148
+ server.tool(
149
+ 'gesicomm_guardar_codigo_landing',
150
+ 'Guarda el código HTML/CSS/JS generado o modificado en la base de datos de Gesicomm. Valida automáticamente las directivas comerciales y sanitiza el código antes de persistirlo. Para cambios puntuales usá seccion_id y revision: conserva otros bloques y su drag and drop. Sincronizá el editor después de guardar.',
151
+ {
152
+ landing_id: z.number().int().positive().optional().describe('ID numérico de la landing'),
153
+ vista: z.enum(['inicio', 'producto', 'catalogo', 'categoria', 'checkout', 'legal']).default('inicio').describe('Vista a actualizar'),
154
+ revision: z.string().optional().describe('Revision devuelta por la lectura; obligatoria para editar una sección.'),
155
+ html: z.string().describe('HTML de la vista o raíz única del bloque indicado en seccion_id'),
156
+ css: z.string().optional().describe('Estilos CSS de la vista'),
157
+ js: z.string().optional().describe('JavaScript de la vista (opcional)'),
158
+ seccion_id: z.string().regex(/^[a-zA-Z0-9_]+$/).optional().describe('Bloque de inicio a leer o editar (ej: anuncios). Omitir al leer para descubrir secciones.'),
159
+ content_id: z.string().optional().describe('Content ID si es la ficha propia de un producto específico'),
160
+ },
161
+ async ({ landing_id, vista, html, css, js, content_id, seccion_id, revision }) => {
162
+ const id = landing_id || defaultLandingId;
163
+ if (!id) {
164
+ return {
165
+ isError: true,
166
+ content: [{ type: 'text', text: 'Debes proporcionar landing_id o configurar GESICOMM_LANDING_ID.' }],
167
+ };
168
+ }
169
+ try {
170
+ const resultado = await client.guardarCodigo(id, {
171
+ vista,
172
+ html,
173
+ css,
174
+ js,
175
+ contentId: content_id,
176
+ seccion_id,
177
+ revision,
178
+ });
179
+ return {
180
+ content: [{
181
+ type: 'text',
182
+ text: JSON.stringify(resultado, null, 2),
183
+ }],
184
+ };
185
+ } catch (err) {
186
+ const detalles = err.detalles?.errores ? `\nErrores encontrados:\n- ${err.detalles.errores.join('\n- ')}` : '';
187
+ return {
188
+ isError: true,
189
+ content: [{ type: 'text', text: `Error al guardar en Gesicomm: ${err.message}${detalles}` }],
190
+ };
191
+ }
192
+ }
193
+ );
194
+
195
+ return server;
196
+ }