Initial commit

This commit is contained in:
Jordan
2026-04-01 23:16:45 +01:00
commit 91cfdaee72
200 changed files with 25589 additions and 0 deletions

View File

@@ -0,0 +1,357 @@
# Acai CMS Endpoints Reference
Este documento mapea todos los endpoints de Acai CMS utilizados por las herramientas MCP.
## Endpoints Base
- **CMS Admin**: `https://[website]/admin.php` - Panel administrativo principal
- **Viewer Functions**: `https://[website]/cms/lib/viewer_functions.php` - API de funciones Acai
- **SAAS API**: `https://ws.cocosolution.com/api/schemas/` - API SaaS para esquemas
- **File Upload**: `https://[website]/lib/menus/modals/plupload/multiupload/upload.php` - Subir archivos
## Categoría: Módulos (saveApartados)
### 1. Generar módulo desde HTML
**Endpoint**: `https://acai.cms.cocosolution.com/admin.php?menu=apartados&action=edit&generateModuleFromString=1`
**Método**: POST
**Usado por**: `save_module`
**Headers**: `Content-Type: application/json`, `X-Acai-Token`
**Payload**: moduleData object con html, htmlParsed, vars, etc.
### 2. Obtener esquemas de módulos
**Endpoint**: `/cms/lib/viewer_functions.php`
**Método**: POST via getApiClient
**Usado por**: `save_module`, `saveGeneralSection`, `check_module`, `list_modules`, `get_module`
**Action**: `getModuleSchemas`
**Payload via getCommonParams**:
```javascript
{
action_ws: "getModuleSchemas",
ids: [moduleId], // opcional, para un módulo específico
full: 1 // opcional, para obtener contenido completo
}
```
### 3. Verificar módulo
**Endpoint**: `https://[website]/cms/lib/viewer_functions.php?action_ws=checkModuleCode`
**Método**: POST
**Usado por**: `check_module`
**Payload**:
```javascript
{
moduleName: string,
vars: object // variables de prueba
}
```
## Categoría: Secciones Generales (saveLexicalData)
### 1. Guardar sección con contenido Twig/HTML
**Endpoint**: `https://[website]/cms/lib/viewer_functions.php`
**Método**: POST
**Usado por**: `saveGeneralSection`
**Action**: `saveLexicalData`
**Payload**:
```javascript
{
action_ws: 'saveLexicalData',
token: credentials.token,
tokenHash: credentials.tokenHash,
content: string, // HTML parsed content
rawDataSended: true,
endPointFolder: string, // e.g., 'custom-productos'
parserType: '2' | '0', // 2=Twig, 0=Acai
aditionalFiles: [ // CSS, JS files
{
path: string,
fileName: string,
content: string
}
]
}
```
## Categoría: Registros (CRUD)
### 1. Crear/Actualizar registro
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST
**Usado por**: `create_or_update_record`
**Content-Type**: `application/x-www-form-urlencoded`
**Params**:
```
menu={tableName}
_defaultAction=save
num={recordId} // empty para crear
type=
preSaveTempId={timestamp}
action=save
{fieldname}={value} // campos del registro
{fieldname}:year, :mon, etc // para campos date
enlace={value}
```
### 2. Listar registros
**Endpoint**: `${CMS_URL}/admin.php?menu={tableName}&json=1&page={n}&keyword={q}`
**Método**: GET
**Usado por**: `list_table_records`
**Headers**: `X-Acai-Token`, `X-Requested-With: XMLHttpRequest`
### 3. Eliminar registros
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST
**Usado por**: `delete_table_records`
**Params**:
```
menu={tableName}
_defaultAction=list
page=1
_advancedAction=eraseRecords
_advancedActionSubmit=Ejecutar
selectedRecords[]={id1}
selectedRecords[]={id2}
```
## Categoría: Archivos (saveFileBuilder, removeFileBuilder)
### 1. Escribir archivo
**Endpoint**: `/cms/lib/viewer_functions.php`
**Método**: POST via getApiClient
**Usado por**: `write_file`
**Action**: `saveFileBuilder`
**Payload via getCommonParams**:
```javascript
{
action_ws: "saveFileBuilder",
path: string, // ej: '/modulos/mymodule/'
fileName: string, // ej: 'style.css'
content: string,
rawDataSended: false,
rootFolder: false
}
```
### 2. Listar archivos (FTP)
**Endpoint**: `/cms/lib/viewer_functions.php`
**Método**: POST via getApiClient
**Usado por**: `list_files`
**Action**: `getFTPFiles`
**Payload via getCommonParams**:
```javascript
{
action_ws: "getFTPFiles",
path: string // directorio a listar
}
```
### 3. Eliminar archivo
**Endpoint**: `/cms/lib/viewer_functions.php`
**Método**: POST via getApiClient
**Usado por**: `delete_file`
**Action**: `removeFileBuilder`
**Payload via getCommonParams**:
```javascript
{
action_ws: "removeFileBuilder",
path: string // ruta del archivo
}
```
## Categoría: Tablas (Database Schema)
### 1. Listar tablas (SaaS)
**Endpoint**: `${SAAS_URL}`
**Método**: POST
**Usado por**: `list_tables`
**Payload**:
```javascript
{
action: 'getSchemaTables',
type: 'acai'
}
```
**Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`
### 2. Obtener esquema tabla (SaaS)
**Endpoint**: `${SAAS_URL}`
**Método**: POST
**Usado por**: `get_table_schema`
**Payload**:
```javascript
{
action: 'getSchemaTables',
type: 'acai'
}
```
### 3. Actualizar esquema tabla (SaaS + CMS)
**Endpoint SaaS**: `${SAAS_URL}` (PUT)
**Método**: PUT
**Usado por**: `update_table_schema`
**Payload**:
```javascript
{
action: "saveSchema",
type: "acai",
schema: object, // esquema completo o parcial
dir: "",
id: tableName
}
```
**Luego sincronizar en CMS**:
**Endpoint CMS**: `/cms/lib/viewer_functions.php`
**Método**: POST via getApiClient
**Action**: `updateAllSchemas`
**Payload via getCommonParams**:
```javascript
{
action_ws: "updateAllSchemas",
tokenHash: credentials.tokenHash
}
```
### 4. Crear tabla
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST
**Usado por**: `create_table`
**Params**:
```
menu=database
_defaultAction=addTable_save
type={multi|single|category|separador}
preset=
enlace={on|''}
seo_metas={on|''}
menuName={name}
menuOrder={order}
tableName={name}
```
### 5. Eliminar tabla
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST
**Usado por**: `delete_table`
**Params**:
```
menu=database
action=editTable
dropTable=1
tableName={name}
```
### 6. Editar campos tabla
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST
**Usado por**: `edit_table_field`
**Params**:
```
menu=database
_defaultAction=editTable
editField=1
tableName=cms_{tableName}
save=1
multipleFields={JSON.stringify(fieldArray)}
```
### 7. Eliminar campo tabla
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST
**Usado por**: `delete_table_field`
**Params**:
```
menu=database
action=editTable
editField=1
tableName=cms_{tableName}
fieldname={fieldname}
deleteField=1
```
### 8. Obtener templates tabla (general section)
**Endpoint**: `/cms/lib/viewer_functions.php`
**Método**: POST via getApiClient
**Usado por**: `get_table_templates`
**Action**: `getTableData`
**Payload via getCommonParams**:
```javascript
{
action_ws: "getTableData",
menu: tableName
}
```
## Categoría: Media (Upload)
### 1. Subir imagen a campo
**Endpoint**: `${CMS_URL}/lib/menus/modals/plupload/multiupload/upload.php?menu={table}&fieldName={field}&num={recordId}&preSaveTempId=`
**Método**: POST (FormData)
**Usado por**: `upload_record_image`
**Form Fields**:
```
file={File buffer} // File object
```
### 2. Listar uploads campo
**Endpoint**: `${CMS_URL}/admin.php?menu={table}&action=uploadList&fieldName={field}&num={recordId}&preSaveTempId=&json=1`
**Método**: GET
**Usado por**: `list_record_uploads`
**Headers**: `X-Acai-Token`
### 3. Reemplazar upload
**Endpoint**: `${CMS_URL}/admin.php`
**Método**: POST (FormData)
**Usado por**: `replace_record_image`
**Form Fields**:
```
_defaultAction=uploadModify
menu={tableName}
fieldName={fieldName}
num={recordId}
preSaveTempId=
save=1
uploadNums[]={uploadId}
{uploadId}_file={File buffer}
{uploadId}_name={originalFilePath}
{uploadId}_alt={altText}
action=uploadModify
```
### 4. Eliminar upload
**Endpoint**: `${CMS_URL}/admin.php?menu={table}&action=uploadErase&fieldName={field}&uploadNum={id}&num={recordId}&preSaveTempId=`
**Método**: GET
**Usado por**: `delete_record_upload`
**Headers**: `X-Acai-Token`, `X-Requested-With: XMLHttpRequest`
## Patrones Comunes
### getApiClient Calls
```javascript
const client = getApiClient(extra.sessionId);
const response = await client.post("/cms/lib/viewer_functions.php", getCommonParams(extra.sessionId, {
action_ws: "actionName",
// ... otros params
}));
```
### getCommonParams
Agrega automáticamente:
- token
- tokenHash
- website
- session info
### Headers Recurrentes
```javascript
{
"X-Acai-Token": credentials.token,
"Content-Type": "application/json" | "application/x-www-form-urlencoded"
}
```
## Notas Importantes
1. **Construcción de URLs**: Algunos endpoints usan la URL base dinámicamente (`https://{website}/...`) mientras otros usan `CMS_URL` configurado.
2. **Parámetros de formulario**: Algunos endpoints esperan URLSearchParams, otros JSON.
3. **Token Auth**: Algunos usan `X-Acai-Token`, otros pasan token en payload.
4. **Respuestas**: Varían entre `{success: true}`, `{result: true}`, o respuestas direc tas.

View File

@@ -0,0 +1,270 @@
# Error Handling System for Tools
Centralizado error handling para todas las herramientas MCP del servidor.
## Características
**Manejo consistente de errores** - Todas las herramientas retornan el mismo formato
**Logging automático** - Todos los errores se registran en consola
**Validación de parámetros** - Validación requerida y de tipos
**Detección de errores API** - Identifica patrones comunes de error en respuestas
**Información contextual** - Cada error incluye el contexto de dónde ocurrió
## Funciones Disponibles
### `handleToolError(error, context, additionalInfo)`
Maneja cualquier error y retorna una respuesta formateada.
```javascript
import { handleToolError } from "../helpers/errorHandler.js";
try {
// tu código
} catch (error) {
return handleToolError(error, 'my_tool', { userId: 123 });
}
```
**Retorna:**
```json
{
"success": false,
"error": {
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 127.0.0.1:3000",
"context": "my_tool",
"userId": 123
}
}
```
---
### `handleApiResponse(data, context)`
Detecta errores en respuestas de API (busca patrones comunes).
```javascript
const response = await axios.post(url, payload);
// Detecta automáticamente: error, Error, PHPSyntax, success: false, etc.
const apiError = handleApiResponse(response.data, 'save_module');
if (apiError) return apiError;
```
---
### `validateRequired(params, requiredFields, context)`
Valida que los parámetros requeridos estén presentes.
```javascript
const error = validateRequired(
{ name: "Juan", email: "" },
['name', 'email'],
'create_user'
);
// error porque email está vacío
```
---
### `validateTypes(params, schema, context)`
Valida tipos de datos.
```javascript
const error = validateTypes(
{ age: "25", active: true },
{ age: 'number', active: 'boolean' },
'create_user'
);
// error porque age es string, no number
```
---
### `createValidator(requiredFields, typeSchema)`
Crea una función validadora reutilizable.
```javascript
const validateUserInput = createValidator(
['name', 'email'],
{ age: 'number', active: 'boolean' }
);
// Usar en múltiples lugares
const error = validateUserInput(params, 'create_user');
if (error) return error;
```
---
### `withErrorHandling(handler, toolName)`
Envuelve un handler para manejar errores automáticamente.
```javascript
const safeHandler = withErrorHandling(
async (params, extra) => {
// tu código
},
'my_tool'
);
```
---
### `safeJsonParse(jsonString, context)`
Parse JSON seguro con manejo de errores.
```javascript
const result = safeJsonParse(jsonString, 'parse_config');
if (!result.success) {
// result.error contiene el error formateado
return result.error;
}
const data = result.data;
```
---
## Patrón Recomendado para Tools
```javascript
import { z } from "zod";
import axios from "axios";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import {
handleToolError,
handleApiResponse,
validateRequired
} from "../helpers/errorHandler.js";
export function registerMyTool(server) {
server.tool(
"my_tool",
"Descripción de la herramienta",
{
param1: z.string().describe("Parámetro 1"),
param2: z.number().describe("Parámetro 2"),
},
withAuth(async ({ param1, param2 }, extra) => {
try {
// 1. Validar parámetros requeridos
const validationError = validateRequired(
{ param1, param2 },
['param1', 'param2'],
'my_tool'
);
if (validationError) return validationError;
// 2. Obtener credenciales
const credentials = getSessionCredentials(extra.sessionId);
// 3. Hacer llamada API
const response = await axios.post(url, payload, {
headers: { /* ... */ }
});
// 4. Verificar respuesta de API
const apiError = handleApiResponse(response.data, 'my_tool');
if (apiError) return apiError;
// 5. Retornar resultado
return {
content: [{
type: "text",
text: JSON.stringify(response.data, null, 2)
}]
};
} catch (error) {
// Los errores se capturan y formatean automáticamente
return handleToolError(error, 'my_tool', { param1, param2 });
}
})
);
}
```
---
## Errores Detectados Automáticamente
`handleApiResponse()` detecta estos patrones en respuestas:
-`data.error` o `data.Error`
-`data.PHPSyntax` - Errores de sintaxis PHP
-`data.success === false` - Campo success explícito
- ✅ Strings con palabras clave: "error", "fatal", "undefined", "syntax"
- ✅ Respuestas vacías o null
---
## Formato de Error Consistente
Todos los errores retornan este formato:
```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Mensaje descriptivo del error",
"context": "nombre_del_tool",
"...": "información adicional"
}
}
```
---
## Migración de Tools Existentes
Para actualizar un tool existente:
1. Importar funciones de error handler
2. Reemplazar `try-catch` genérico con `handleToolError()`
3. Agregar validación con `validateRequired()`
4. Agregar `handleApiResponse()` después de llamadas API
5. Pasar información contextual útil a `handleToolError()`
**Ejemplo antes:**
```javascript
try {
// código
} catch (error) {
return {
content: [{ type: "text", text: "Error: " + error.message }],
isError: true
};
}
```
**Ejemplo después:**
```javascript
try {
// código
} catch (error) {
return handleToolError(error, 'my_tool', { extraInfo: value });
}
```
---
## Logging
Todos los errores se registran en stderr con contexto:
```
[Tool Error - save_module] Cannot read property 'website' of undefined
Stack: Error: Cannot read property 'website' of undefined
at registerSaveModuleTool (/Users/...save.js:45:20)
...
```
Esto facilita debug y auditoría de errores en producción.

View File

@@ -0,0 +1,587 @@
/**
* Acai CMS HTTP Client
*
* Centralizado helper para todas las llamadas HTTP a Acai CMS.
* Proporciona métodos consistentes para interactuar con:
* - Admin panel (admin.php)
* - Viewer functions API
* - SaaS API
* - File upload endpoints
*
* Ventajas:
* - Consistencia en headers, manejo de errores, logging
* - Reduce duplicación de código
* - Facilita mantenimiento y debugging
* - Centraliza URLs y configuración
*/
import axios from 'axios';
import { getSessionCredentials } from '../../auth/index.js';
import { CMS_URL } from '../../config/index.js';
import { assertSafeCmsTarget } from '../../utils/cmsTargetSafety.js';
/**
* AcaiHttpClient - Helper para solicitudes HTTP a Acai CMS
*/
export class AcaiHttpClient {
static resolveCmsTarget(target) {
const { publicUrl, apiUrl, forgeHost } = assertSafeCmsTarget(target, "AcaiHttpClient");
const headers = {};
if (forgeHost) {
headers.Host = forgeHost;
}
return {
publicUrl,
apiUrl,
headers,
};
}
static buildViewerUrl(target, query = "") {
const { apiUrl } = AcaiHttpClient.resolveCmsTarget(target);
const baseUrl = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
return `${baseUrl}/cms/lib/viewer_functions.php${query ? `?${query}` : ""}`;
}
static buildViewerHeaders(target, extraHeaders = {}) {
const { headers } = AcaiHttpClient.resolveCmsTarget(target);
return {
"Content-Type": "application/json",
...headers,
...extraHeaders,
};
}
static async postViewerAction(target, actionWs, payload, token, tokenHash, extraHeaders = {}, timeout = 30000) {
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, `action_ws=${actionWs}`);
const body = {
...payload,
token,
tokenHash,
};
return axios.post(viewerUrl, body, {
headers: AcaiHttpClient.buildViewerHeaders(target, extraHeaders),
timeout,
});
}
/**
* POST a admin.php con URLSearchParams
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
* @param {URLSearchParams} params - Parámetros del formulario
* @param {string} token - Token Acai
* @returns {Promise<Object>} Respuesta del servidor
*/
static async postAdminForm(website, params, token) {
const cmsUrl = `${CMS_URL}/admin.php`;
try {
console.error(`[AcaiHttpClient] postAdminForm - START: ${cmsUrl}`);
const response = await axios.post(cmsUrl, params, {
headers: {
"X-Acai-Token": token,
"Content-Type": "application/x-www-form-urlencoded"
},
timeout: 30000
});
console.error(`[AcaiHttpClient] postAdminForm - SUCCESS: ${cmsUrl} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] postAdminForm - ERROR: ${cmsUrl} - ${error.message}`);
if (error.response) {
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
} else if (error.code) {
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
}
throw error;
}
}
/**
* POST a admin.php con FormData (para uploads)
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
* @param {FormData} formData - Datos del formulario
* @param {string} token - Token Acai
* @returns {Promise<Object>} Respuesta del servidor
*/
static async postAdminFormData(website, formData, token) {
const cmsUrl = `${CMS_URL}/admin.php`;
try {
console.error(`[AcaiHttpClient] postAdminFormData - START: ${cmsUrl}`);
const response = await axios.post(cmsUrl, formData, {
headers: {
...formData.getHeaders(),
"X-Acai-Token": token
},
timeout: 30000
});
console.error(`[AcaiHttpClient] postAdminFormData - SUCCESS: ${cmsUrl} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] postAdminFormData - ERROR: ${cmsUrl} - ${error.message}`);
if (error.response) {
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
} else if (error.code) {
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
}
throw error;
}
}
/**
* GET a admin.php con query parameters
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
* @param {URLSearchParams | string} params - Parámetros de query
* @param {string} token - Token Acai
* @returns {Promise<Object>} Respuesta del servidor
*/
static async getAdminQuery(website, params, token) {
const cmsUrl = `${CMS_URL}/admin.php`;
const queryString = params instanceof URLSearchParams
? params.toString()
: params;
try {
console.error(`[AcaiHttpClient] getAdminQuery - START: ${cmsUrl}?${queryString.substring(0, 100)}`);
const response = await axios.get(
`${cmsUrl}?${queryString}`,
{
headers: {
"X-Acai-Token": token,
"X-Requested-With": "XMLHttpRequest"
},
timeout: 30000
}
);
console.error(`[AcaiHttpClient] getAdminQuery - SUCCESS: ${cmsUrl} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] getAdminQuery - ERROR: ${cmsUrl} - ${error.message}`);
if (error.response) {
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
} else if (error.code) {
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
}
throw error;
}
}
/**
* POST a viewer_functions.php vía getApiClient
* Requiere llamar desde dentro de withAuth para tener acceso a getApiClient
* @param {Object} client - cliente de axios (getApiClient)
* @param {Object} payload - Payload con action_ws y otros parámetros
* @returns {Promise<Object>} Respuesta del servidor
*/
static async postViewerFunctions(client, payload) {
return client.post("/cms/lib/viewer_functions.php", payload);
}
/**
* POST a viewer_functions.php para saveLexicalData (secciones, contenido)
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {Object} credentials - {token, tokenHash}
* @param {Object} data - Datos a guardar
* @returns {Promise<Object>} Respuesta del servidor
*/
static async saveLexicalData(target, credentials, data) {
const viewerUrl = AcaiHttpClient.buildViewerUrl(target);
const payload = {
action_ws: 'saveLexicalData',
token: credentials.token,
tokenHash: credentials.tokenHash,
rawDataSended: true,
...data
};
return axios.post(viewerUrl, payload, {
headers: AcaiHttpClient.buildViewerHeaders(target)
});
}
/**
* POST para generar módulo desde HTML
* @param {Object} moduleData - Datos del módulo
* @param {string} token - Token Acai
* @returns {Promise<Object>} Respuesta del servidor
*/
static async generateModuleFromString(moduleData, token) {
const cmsUrl = 'https://acai.cms.cocosolution.com/admin.php?menu=apartados&action=edit&generateModuleFromString=1';
return axios.post(cmsUrl, moduleData, {
headers: {
"Content-Type": "application/json",
"X-Acai-Token": token
}
});
}
/**
* POST a viewer_functions para CMS API (insert, update, delete, get)
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} action - 'insert', 'update', 'delete', 'get'
* @param {Object} payload - Datos de la operación
* @param {string} token - Token Acai
* @returns {Promise<Object>} Respuesta del servidor
*/
static async postCmsApi(target, action, payload, token, tokenHash) {
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, `action_ws=cmsApi&subaction=${action}`);
try {
console.error(`[AcaiHttpClient] postCmsApi - START: ${action} on ${viewerUrl}`);
console.error(`[AcaiHttpClient] Payload:`, JSON.stringify(payload).substring(0, 500));
console.error(`[AcaiHttpClient] Token: ${token ? '****' + token.slice(-4) : 'No token provided'}`);
payload["token"] = token;
payload["tokenHash"] = tokenHash;
const response = await axios.post(viewerUrl, payload, {
headers: AcaiHttpClient.buildViewerHeaders(target, {
"X-Acai-Token": token
}),
timeout: 30000
});
console.error(`[AcaiHttpClient] postCmsApi - SUCCESS: ${action} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] postCmsApi - ERROR: ${action} on ${viewerUrl} - ${error.message}`);
if (error.response) {
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
} else if (error.code) {
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
}
throw error;
}
}
/**
* POST a viewer_functions para checkModuleCode
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} token - Token Acai
* @param {Object} data - {moduleName, vars}
* @returns {Promise<Object>} Respuesta del servidor
*/
static async checkModuleCode(target, token, data) {
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, "action_ws=checkModuleCode");
try {
data["token"] = token;
console.error(`[AcaiHttpClient] checkModuleCode - START: ${viewerUrl}`);
const response = await axios.post(viewerUrl, data, {
headers: AcaiHttpClient.buildViewerHeaders(target),
timeout: 30000
});
console.error(`[AcaiHttpClient] checkModuleCode - SUCCESS: ${viewerUrl} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] checkModuleCode - ERROR: ${viewerUrl}`, error.message);
throw error;
}
}
/**
* POST a viewer_functions para addModuleToRecord
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} token - Token Acai
* @param {Object} data - {moduleName, vars}
* @returns {Promise<Object>} Respuesta del servidor
*/
static async addModuleToRecord(target, token, tokenHash, data) {
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, "action_ws=addModuleToRecord");
try {
data["token"] = token;
data["tokenHash"] = tokenHash;
console.error(`[AcaiHttpClient] addModuleToRecord - START: ${viewerUrl}`);
const response = await axios.post(viewerUrl, data, {
headers: AcaiHttpClient.buildViewerHeaders(target),
timeout: 30000
});
console.error(`[AcaiHttpClient] addModuleToRecord - SUCCESS: ${viewerUrl} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] addModuleToRecord - ERROR: ${viewerUrl}`, error.message);
throw error;
}
}
/**
* POST a mcp_respond.php para setModuleConfigVars
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} token - Token Acai
* @param {string} tokenHash - Token hash Acai
* @param {Object} data - {tableName, recordNum, sectionId, vars}
* @returns {Promise<Object>} Respuesta del servidor
*/
static async setModuleConfigVars(target, token, tokenHash, data) {
const url = AcaiHttpClient.buildViewerUrl(target, "action_ws=setModuleConfigVars");
try {
data["token"] = token;
data["tokenHash"] = tokenHash;
console.error(`[AcaiHttpClient] setModuleConfigVars - START: ${url}`);
const response = await axios.post(url, data, {
headers: AcaiHttpClient.buildViewerHeaders(target),
timeout: 30000
});
console.error(`[AcaiHttpClient] setModuleConfigVars - SUCCESS: ${url} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] setModuleConfigVars - ERROR: ${url}`, error.message);
throw error;
}
}
/**
* POST a viewer_functions para getModuleConfigVars
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} token - Token Acai
* @param {string} tokenHash - Token hash Acai
* @param {Object} data - {tableName, recordNum, sectionId}
* @returns {Promise<Object>} Respuesta del servidor
*/
static async getModuleConfigVars(target, token, tokenHash, data) {
const url = AcaiHttpClient.buildViewerUrl(target, "action_ws=getModuleConfigVars");
try {
data["token"] = token;
data["tokenHash"] = tokenHash;
console.error(`[AcaiHttpClient] getModuleConfigVars - START: ${url}`);
const response = await axios.post(url, data, {
headers: AcaiHttpClient.buildViewerHeaders(target),
timeout: 30000
});
console.error(`[AcaiHttpClient] getModuleConfigVars - SUCCESS: ${url} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] getModuleConfigVars - ERROR: ${url}`, error.message);
throw error;
}
}
/**
* POST para subir imagen a campo de registro
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
* @param {string} tableName - Nombre de la tabla
* @param {string} recordId - ID del registro
* @param {string} fieldName - Nombre del campo
* @param {FormData} formData - Datos del archivo
* @param {string} token - Token Acai
* @returns {Promise<Object>} Respuesta del servidor
*/
static async uploadRecordImage(website, tableName, recordId, fieldName, formData, token) {
const uploadUrl = `${CMS_URL}/lib/menus/modals/plupload/multiupload/upload.php?menu=${tableName}&fieldName=${fieldName}&num=${recordId}&preSaveTempId=`;
return axios.post(uploadUrl, formData, {
headers: {
...formData.getHeaders(),
"X-Acai-Token": token
}
});
}
/**
* POST a SaaS API para guardar esquema
* @param {Object} payload - {action, type, schema, dir, id, ...}
* @param {string} token - Token autenticación
* @returns {Promise<Object>} Respuesta del servidor
*/
static async saasPostRequest(payload, token) {
const SAAS_URL = 'https://ws.cocosolution.com/api/schemas/';
try {
console.error(`[AcaiHttpClient] saasPostRequest - START: ${SAAS_URL} (action: ${payload.action})`);
const response = await axios.post(SAAS_URL, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 30000
});
console.error(`[AcaiHttpClient] saasPostRequest - SUCCESS: ${SAAS_URL} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] saasPostRequest - ERROR: ${SAAS_URL} - ${error.message}`);
if (error.response) {
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
} else if (error.code) {
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
}
throw error;
}
}
/**
* PUT a SaaS API para actualizar esquema
* @param {Object} payload - {action, type, schema, dir, id}
* @param {string} token - Token autenticación
* @returns {Promise<Object>} Respuesta del servidor
*/
static async saasPutRequest(payload, token) {
const SAAS_URL = 'https://ws.cocosolution.com/api/schemas/';
try {
console.error(`[AcaiHttpClient] saasPutRequest - START: ${SAAS_URL} (action: ${payload.action})`);
const response = await axios.put(SAAS_URL, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 30000
});
console.error(`[AcaiHttpClient] saasPutRequest - SUCCESS: ${SAAS_URL} (${response.status})`);
return response;
} catch (error) {
console.error(`[AcaiHttpClient] saasPutRequest - ERROR: ${SAAS_URL} - ${error.message}`);
if (error.response) {
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
} else if (error.code) {
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
}
throw error;
}
}
}
/**
* Helper para construir parámetros comunes de formulario
*/
export class FormParamsBuilder {
static buildRecordSaveParams(tableName, recordId, fields, enlace) {
const params = new URLSearchParams();
params.append('menu', tableName);
params.append('_defaultAction', 'save');
params.append('num', recordId ? String(recordId) : '');
params.append('type', '');
params.append('preSaveTempId', Date.now().toString());
params.append('action=save', 'Guardar');
// Agregar todos los campos
for (const [fieldName, value] of Object.entries(fields)) {
if (fieldName === 'enlace') continue;
if (value !== null && value !== undefined) {
const strValue = String(value);
params.append(fieldName, strValue);
// Detectar y descomponer fechas
const dateRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
const match = strValue.match(dateRegex);
if (match) {
params.append(`${fieldName}:year`, match[1]);
params.append(`${fieldName}:mon`, match[2]);
params.append(`${fieldName}:day`, match[3]);
params.append(`${fieldName}:hour24`, match[4]);
params.append(`${fieldName}:min`, match[5]);
}
}
}
params.append('enlace', enlace);
return params;
}
static buildDeleteRecordsParams(tableName, recordIds) {
const params = new URLSearchParams();
params.append('menu', tableName);
params.append('_defaultAction', 'list');
params.append('page', '1');
params.append('_advancedAction', 'eraseRecords');
params.append('_advancedActionSubmit', 'Ejecutar');
recordIds.forEach(id => {
params.append('selectedRecords[]', String(id));
});
return params;
}
static buildTableCreateParams(menuName, tableName, type, enlace, seo_metas, menuOrder) {
return new URLSearchParams({
menu: "database",
_defaultAction: "addTable_save",
type: type,
preset: "",
enlace: enlace ? "on" : "",
seo_metas: seo_metas ? "on" : "",
menuName: menuName,
menuOrder: menuOrder.toString(),
tableName: tableName
});
}
static buildTableDeleteParams(tableName) {
const params = new URLSearchParams();
params.append('menu', 'database');
params.append('action', 'editTable');
params.append('dropTable', '1');
params.append('tableName', tableName);
return params;
}
static buildFieldEditParams(tableName, multipleFields) {
const params = new URLSearchParams();
params.append('menu', 'database');
params.append('_defaultAction', 'editTable');
params.append('editField', '1');
params.append('tableName', tableName);
params.append('save', '1');
params.append('multipleFields', JSON.stringify(multipleFields));
return params;
}
static buildFieldDeleteParams(tableName, fieldname) {
const params = new URLSearchParams();
params.append('menu', 'database');
params.append('action', 'editTable');
params.append('editField', '1');
params.append('tableName', tableName);
params.append('fieldname', fieldname);
params.append('deleteField', '1');
return params;
}
}
/**
* Helper para construir URLs de query
*/
export class QueryParamsBuilder {
static buildListRecordsQuery(tableName, page, keyword) {
const params = new URLSearchParams({
menu: tableName,
json: "1"
});
if (page) params.append("page", String(page));
if (keyword) params.append("keyword", keyword);
return params;
}
static buildListUploadsQuery(tableName, recordId, fieldName) {
return new URLSearchParams({
menu: tableName,
action: 'uploadList',
fieldName: fieldName,
num: recordId,
preSaveTempId: '',
json: '1'
});
}
static buildDeleteUploadQuery(tableName, recordId, fieldName, uploadId) {
return new URLSearchParams({
menu: tableName,
action: 'uploadErase',
fieldName: fieldName,
uploadNum: uploadId,
num: recordId,
preSaveTempId: ''
});
}
}
export default AcaiHttpClient;

View File

@@ -0,0 +1,6 @@
/**
* Auth parameters helper.
* In stdio mode, credentials come from environment variables — no inline params needed.
* withAuthParams just passes through the schema unchanged.
*/
export const withAuthParams = (schema) => schema;

View File

@@ -0,0 +1,215 @@
/**
* Centralized error handling for tools
* Provides consistent error responses and logging
*/
/**
* Handle and format tool errors
* @param {Error|string} error - The error object or message
* @param {string} context - Context where the error occurred (e.g., "save_module", "create_record")
* @param {Object} additionalInfo - Additional information to include in response
* @returns {Object} Formatted error response
*/
export function handleToolError(error, context = "unknown", additionalInfo = {}) {
// Log error to console
console.error(`[Tool Error - ${context}]`, error instanceof Error ? error.message : error);
if (error instanceof Error && error.stack) {
console.error(`Stack:`, error.stack);
}
// Extract error message
let errorMessage = error instanceof Error ? error.message : String(error);
let errorCode = "UNKNOWN_ERROR";
let statusCode = 500;
// Handle specific error types
if (error.response) {
// Axios error with response
statusCode = error.response.status || 500;
errorMessage = error.response.data?.message ||
error.response.data?.error ||
errorMessage;
errorCode = `HTTP_${statusCode}`;
} else if (error.code) {
// Error with code (like ENOTFOUND, ECONNREFUSED, etc.)
errorCode = error.code;
errorMessage = `${error.code}: ${errorMessage}`;
}
// Return formatted error response
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: {
code: errorCode,
message: errorMessage,
context: context,
...additionalInfo
}
}, null, 2)
}],
isError: true
};
}
/**
* Handle API response errors (when response contains error indication)
* @param {Object} data - Response data from API
* @param {string} context - Context where error occurred
* @returns {Object|null} Error response or null if no error
*/
export function handleApiResponse(data, context = "unknown") {
// Check for common error patterns in Acai CMS responses
/*if (!data) {
return handleToolError("Empty response from API", context, { details: "API returned null or undefined" });
}*/
// PHP/Acai error responses typically have error field or PHPSyntax errors
if (data.error || data.Error) {
return handleToolError(data.error || data.Error, context, { details: data });
}
if (data.PHPSyntax) {
return handleToolError(`PHP Syntax Error: ${data.PHPSyntax}`, context, { details: data });
}
// If it's a string response with error indicators
if (typeof data === 'string' && data.trim().length > 0) {
// Check for common error patterns
if (data.toLowerCase().includes('error') ||
data.toLowerCase().includes('fatal') ||
data.toLowerCase().includes('undefined') ||
data.toLowerCase().includes('syntax')) {
return handleToolError(data, context, { details: "API returned error string" });
}
}
// If success field exists and is false
if (data.success === false) {
return handleToolError(data.message || "API returned success: false", context, { details: data });
}
// No error detected
return null;
}
/**
* Validate required parameters
* @param {Object} params - Parameters object
* @param {string[]} requiredFields - Array of required field names
* @param {string} context - Context where validation occurs
* @returns {Object|null} Error response or null if all valid
*/
export function validateRequired(params, requiredFields, context = "unknown") {
const missingFields = [];
requiredFields.forEach(field => {
const value = params[field];
if (value === null || value === undefined ||
(typeof value === 'string' && value.trim() === '')) {
missingFields.push(field);
}
});
if (missingFields.length > 0) {
return handleToolError(
`Missing required parameters: ${missingFields.join(', ')}`,
context,
{ requiredFields, missingFields }
);
}
return null;
}
/**
* Validate parameter types
* @param {Object} params - Parameters object
* @param {Object} schema - Schema of expected types {fieldName: 'string'|'number'|'boolean'|'array'|'object'}
* @param {string} context - Context where validation occurs
* @returns {Object|null} Error response or null if all valid
*/
export function validateTypes(params, schema, context = "unknown") {
const typeErrors = [];
for (const [field, expectedType] of Object.entries(schema)) {
const value = params[field];
if (value === null || value === undefined) {
continue; // Skip optional fields that are not provided
}
let actualType = typeof value;
if (Array.isArray(value)) actualType = 'array';
if (value instanceof Date) actualType = 'date';
if (actualType !== expectedType) {
typeErrors.push(`${field}: expected ${expectedType}, got ${actualType}`);
}
}
if (typeErrors.length > 0) {
return handleToolError(
`Type validation failed: ${typeErrors.join('; ')}`,
context,
{ typeErrors }
);
}
return null;
}
/**
* Safely parse JSON with error handling
* @param {string} jsonString - JSON string to parse
* @param {string} context - Context where parsing occurs
* @returns {Object} Parsed object or error response object
*/
export function safeJsonParse(jsonString, context = "unknown") {
try {
return { success: true, data: JSON.parse(jsonString) };
} catch (error) {
return {
success: false,
error: handleToolError(error, `${context} - JSON parsing`, { input: jsonString.substring(0, 100) })
};
}
}
/**
* Create a validation middleware for tools
* @param {string[]} requiredFields - Required parameter names
* @param {Object} typeSchema - Type validation schema
* @returns {Function} Middleware function
*/
export function createValidator(requiredFields = [], typeSchema = {}) {
return function validateInput(params, context = "unknown") {
// Check required fields
const requiredError = validateRequired(params, requiredFields, context);
if (requiredError) return requiredError;
// Check types
const typeError = validateTypes(params, typeSchema, context);
if (typeError) return typeError;
return null; // No errors
};
}
/**
* Wrap a tool handler with automatic error handling
* @param {Function} handler - The tool handler function
* @param {string} toolName - Name of the tool for logging
* @returns {Function} Wrapped handler
*/
export function withErrorHandling(handler, toolName = "unknown") {
return async (params, extra) => {
try {
return await handler(params, extra);
} catch (error) {
return handleToolError(error, toolName);
}
};
}

View File

@@ -0,0 +1,102 @@
import axios from "axios";
/**
* Helper to save files using saveFileBuilder action
* Used by multiple tools (save.js, saveGeneralSection.js, write.js, etc.)
*
* @param {Object} params
* @param {string} params.web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} params.token - Session token
* @param {string} params.tokenHash - Token hash
* @param {string} params.path - Folder path (e.g., '/modulos/mymodule/')
* @param {string} params.fileName - File name (e.g., 'script.js', 'style.css')
* @param {string} params.content - File content
* @returns {Promise<Object>} Response from the API
*/
export async function saveFileBuilder({
web_url,
token,
tokenHash,
path,
fileName,
content,
rawDataSended = true
}) {
if (!content) {
return null;
}
const viewerUrl = web_url + '/cms/lib/viewer_functions.php';
const payload = {
action_ws: 'saveFileBuilder',
token: token,
tokenHash: tokenHash,
fileName: fileName,
content: content,
rawDataSended: rawDataSended,
rootFolder: false,
path: path
};
console.error(`[saveFileBuilder] URL: ${viewerUrl}`);
console.error(`[saveFileBuilder] Path: ${path}`);
console.error(`[saveFileBuilder] Content length: ${content.length} chars`);
try {
const response = await axios.post(viewerUrl, payload, {
headers: { "Content-Type": "application/json" }
});
console.error(`[saveFileBuilder] Response for ${fileName}:`, JSON.stringify(response.data, null, 2));
return {
success: response.data.success || false,
message: response.data.message || (response.data.success ? 'OK' : 'Error'),
data: response.data
};
} catch (error) {
console.error(`[saveFileBuilder] Error saving ${fileName}:`, error.message);
return {
success: false,
message: `Error saving ${fileName}: ${error.message}`,
error: error.message
};
}
}
/**
* Helper to save multiple files at once
*
* @param {Object} params
* @param {string} params.web_url - URL base del sitio (ej: http://localhost:PORT)
* @param {string} params.token - Session token
* @param {string} params.tokenHash - Token hash
* @param {string} params.path - Folder path (e.g., '/modulos/mymodule/')
* @param {Object} params.files - Object with fileName: content pairs
* @returns {Promise<Object>} Results for each file
*/
export async function saveMultipleFiles({
web_url,
token,
tokenHash,
path,
files
}) {
const results = {};
for (const [fileName, content] of Object.entries(files)) {
if (content) {
results[fileName] = await saveFileBuilder({
web_url,
token,
tokenHash,
path,
fileName,
content
});
}
}
return results;
}