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 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ export { createGesicommServer } from './server.js';
2
+ export { GesicommApiClient } from './apiClient.js';
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
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.