gesicomm-mcp 0.0.0-stage → 1.0.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.
- package/bin/cli.js +25 -0
- package/package.json +31 -4
- package/src/apiClient.js +95 -0
- package/src/index.js +2 -0
- package/src/server.js +191 -0
- package/README.md +0 -3
package/bin/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { createGesicommServer } from '../src/server.js';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const server = createGesicommServer();
|
|
8
|
+
const transport = new StdioServerTransport();
|
|
9
|
+
|
|
10
|
+
// IMPORTANTE en MCP: Todo logging o debug DEBE salir por stderr.
|
|
11
|
+
// stdout es reservado exclusivamente para el protocolo JSON-RPC de MCP.
|
|
12
|
+
console.error('[gesicomm-mcp] Iniciando servidor MCP de Gesicomm...');
|
|
13
|
+
console.error(`[gesicomm-mcp] API URL: ${process.env.GESICOMM_API_URL || 'http://localhost:5000/api'}`);
|
|
14
|
+
if (process.env.GESICOMM_LANDING_ID) {
|
|
15
|
+
console.error(`[gesicomm-mcp] Landing ID por defecto: ${process.env.GESICOMM_LANDING_ID}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await server.connect(transport);
|
|
19
|
+
console.error('[gesicomm-mcp] Conectado exitosamente via stdio.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch((err) => {
|
|
23
|
+
console.error('[gesicomm-mcp] Error fatal:', err);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gesicomm-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
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
|
+
}
|
package/src/apiClient.js
ADDED
|
@@ -0,0 +1,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
|
+
'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
|
+
}
|
package/src/index.js
ADDED
package/src/server.js
ADDED
|
@@ -0,0 +1,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.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
|
+
}
|
package/README.md
DELETED