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,73 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleApiResponse, handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerAddModuleToRecordTool(server) {
server.tool(
"add_module_to_record",
`Adds a builder module to a specific record at the desired position. Returns the generated sectionId — use it directly with set_module_config_vars without needing to call list_page_modules.
Required params:
- tableName (string) without 'cms_' prefix
- recordNum (number) record primary key ('num' field, never 'id')
- moduleId (string) module identifier
Optional:
- modulePosition (number) insertion index (0-based, default 0)
Response includes: sectionId, moduleId, position, totalModules`,
withAuthParams({
tableName: z.string().describe("Table name without cms_ prefix, e.g. 'apartados'"),
recordNum: z.union([z.string(), z.number()]).describe("Record num (ID) where the module will be inserted"),
moduleId: z.string().describe("Module ID to insert"),
modulePosition: z.number().optional().describe("Position in the builder array (0-based). Default 0.")
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, recordNum, moduleId, modulePosition }, extra) => {
try {
const validationError = validateRequired({ tableName, recordNum, moduleId }, ['tableName', 'recordNum', 'moduleId'], 'add_module_to_record');
if (validationError) return validationError;
const sessionId = extra.sessionId;
const credentials = await getSessionCredentials(sessionId);
const payload = {
tableName,
recordNum,
moduleId,
modulePosition: modulePosition ?? 0
};
// Same endpoint pattern as create_or_update_record: cmsApi subaction
const response = await AcaiHttpClient.addModuleToRecord(
credentials,
credentials.token,
credentials.tokenHash,
payload
);
const apiError = handleApiResponse(response.data, 'add_module_to_record');
if (apiError) return apiError;
const result = response.data || {}
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
action: 'add_module_to_record',
tableName,
recordNum,
moduleId,
sectionId: result.sectionId,
position: result.position ?? (modulePosition ?? 0),
totalModules: result.totalModules,
}, null, 2)
}]
};
} catch (error) {
return handleToolError(error, 'add_module_to_record', { tableName, recordNum, moduleId });
}
})
);
}

View File

@@ -0,0 +1,145 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { table } from "console";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerCreateOrUpdateRecordTool(server) {
server.tool(
"create_or_update_record",
`Create or update records in a database table. Before using: read resource 'acai-cheat-sheet' for domain rules, then check table schema with get_table_schema.
Key rules: tables without 'cms_' prefix, primary key is 'num', uploads are arrays (use upload_record_image after creating record), dates as YYYY-MM-DD HH:mm:ss, checkboxes as 1/0, enlace as /path/.
For builder tables (e.g. 'apartados'): must include num:null, builder:"[]", controlador, precontrolador, breadcrumb, enlace fields. See resource 'guia-registros' for full field type reference.`,
withAuthParams({
tableName: z.string().describe("Name of the table (without 'cms_' prefix, e.g., 'productos', 'equipo')"),
recordId: z.any().optional().describe("Record ID for updating. Leave empty to create new record. NOT USED when records is an array."),
fields: z.any().describe("Single record object OR array of record objects for batch insert. Example: { nombre: 'Product 1' } or [{ nombre: 'Product 1' }, { nombre: 'Product 2' }]. IMPORTANT: Always consult 'guia-registros' for field types and formats and check if is table with builder fields."),
tableSchema: z.any().describe("Provide the table schema object to validate field types before sending to API. If not provided, schema will not be validated."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, recordId, fields }, extra) => {
try {
// Validate required parameters
const validationError = validateRequired({ tableName, fields }, ['tableName', 'fields'], 'create_or_update_record');
if (validationError) return validationError;
// if fields is string, try to parse as JSON
if (typeof fields === 'string') {
try {
fields = JSON.parse(fields);
} catch (e) {
return {
content: [{ type: "text", text: "Error: 'fields' parameter is a string but not valid JSON." }],
isError: true,
};
}
}
// Determine if fields is array or single object
const isArray = Array.isArray(fields);
const recordsArray = isArray ? fields : [fields];
// Check if trying to update with array (not supported)
if (isArray && recordId) {
return {
content: [{ type: "text", text: "Error: Cannot use recordId when fields is an array. Use fields as array for batch insert only." }],
isError: true,
};
}
// Protect critical fields during updates — these should never be changed by AI
const PROTECTED_UPDATE_FIELDS = ['enlace', 'controlador', 'precontrolador'];
if (recordId) {
// On update: strip protected fields silently
recordsArray.forEach(record => {
PROTECTED_UPDATE_FIELDS.forEach(f => {
if (f in record) delete record[f];
});
});
}
// Process enlace field for new records only
let processedRecords = recordsArray;
if (!recordId) {
processedRecords = recordsArray.map(record => {
let enlaceValue = record.enlace;
if (!enlaceValue) {
// Generate random enlace if not provided to ensure uniqueness
enlaceValue = '/' + Math.random().toString(36).substring(2, 10) + '/';
} else {
// Ensure format /.../
enlaceValue = String(enlaceValue);
if (!enlaceValue.startsWith('/')) enlaceValue = '/' + enlaceValue;
if (!enlaceValue.endsWith('/')) enlaceValue = enlaceValue + '/';
}
return { ...record, enlace: enlaceValue };
});
}
// Prepare payload for CMS API
const credentials = await getSessionCredentials(extra.sessionId);
const recordPayload = {
tableName: tableName,
records: processedRecords,
functions: [],
options: {}
};
// Determine action: insert for new records, update for existing
const isNewRecord = !recordId;
let response;
if (isNewRecord) {
// Insert new record(s)
response = await AcaiHttpClient.postCmsApi(
credentials,
'insert',
recordPayload,
credentials.token,
credentials.tokenHash
);
} else {
// Update existing record (only single record, not array)
response = await AcaiHttpClient.postCmsApi(
credentials,
'update',
{
...recordPayload,
where: `num = ${recordId}`
},
credentials.token,
credentials.tokenHash
);
}
// Check for API errors
const apiError = handleApiResponse(response.data, 'create_or_update_record');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: isNewRecord
? `${isArray ? recordsArray.length : 1} record(s) created successfully`
: `Record ${recordId} updated successfully`,
tableName: tableName,
recordIds: response.data?.data || (recordId || 'new'),
recordsCount: isArray ? recordsArray.length : 1,
createdIds: response.data?.data,
suggestion: isNewRecord && !isArray ? `You can verify the record by fetching: ${credentials.web_url}${processedRecords[0].enlace}` : undefined
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'create_or_update_record', { tableName, recordId, isArray: Array.isArray(fields) });
}
})
);
}

View File

@@ -0,0 +1,89 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerDeleteTableRecordsTool(server) {
server.tool(
"delete_table_records",
"⚠️ DANGEROUS: Delete records from a database table. This is a PERMANENT operation that cannot be undone. Use with extreme caution. You can delete specific records by their 'num' (primary key) or delete all records from a table. Table names are WITHOUT the 'cms_' prefix.",
withAuthParams({
tableName: z.string().describe("Name of the table to delete records from (without 'cms_' prefix)"),
recordIds: z.array(z.union([z.string(), z.number()])).optional().describe("Array of record 'num' values (primary key) to delete. If not provided, you must set deleteAll to true."),
deleteAll: z.boolean().optional().describe("Set to true to delete ALL records from the table. Use with extreme caution."),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, recordIds, deleteAll = false }, extra) => {
try {
// Validation: must provide either recordIds or deleteAll
const validationError = validateRequired({ tableName }, ['tableName'], 'delete_table_records');
if (validationError) return validationError;
if (!recordIds && !deleteAll) {
return {
content: [{ type: "text", text: "Error: You must provide either 'recordIds' or set 'deleteAll' to true." }],
isError: true,
};
}
if (deleteAll && recordIds) {
return {
content: [{ type: "text", text: "Error: Cannot specify both 'recordIds' and 'deleteAll'. Choose one." }],
isError: true,
};
}
if (deleteAll) {
return {
content: [{ type: "text", text: "Error: 'deleteAll' is not currently supported with this method. Please provide 'recordIds'." }],
isError: true,
};
}
// Build delete parameters for CMS API
const credentials = await getSessionCredentials(extra.sessionId);
// Build SQL where clause for deleting multiple records
const whereClause = recordIds.length === 1
? `num = ${recordIds[0]}`
: `num IN (${recordIds.join(',')})`;
const payload = {
tableName: tableName,
where: whereClause,
options: {}
};
// Send to CMS API via viewer_functions
const response = await AcaiHttpClient.postCmsApi(
credentials,
'delete',
payload,
credentials.token,
credentials.tokenHash
);
// Check for API errors
const apiError = handleApiResponse(response.data, 'delete_table_records');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `${recordIds.length} record(s) deleted from table '${tableName}'`,
deletedCount: recordIds.length,
tableName: tableName,
serverResponse: response.data ? "Response received" : "No response body"
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'delete_table_records', { tableName, recordCount: recordIds?.length || 0 });
}
})
);
}

View File

@@ -0,0 +1,63 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleApiResponse, handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerGetModuleConfigVarsTool(server) {
server.tool(
"get_module_config_vars",
`Get the current configuration variable values for a module instance on a page record. Returns resolved values (text, HTML, etc.) for simple vars and arrays of objects for multi/repeater vars.
Required params:
- tableName (string) without 'cms_' prefix
- recordNum (number) record primary key ('num' field, never 'id')
- sectionId (string) section ID of the module instance`,
withAuthParams({
tableName: z.string().describe("Parent table name (e.g. 'apartados')"),
recordNum: z.number().describe("Parent record number"),
sectionId: z.string().describe("Section ID of the module instance"),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ tableName, recordNum, sectionId }, extra) => {
try {
const validationError = validateRequired({ tableName, recordNum, sectionId }, ['tableName', 'recordNum', 'sectionId'], 'get_module_config_vars');
if (validationError) return validationError;
const sessionId = extra.sessionId;
const credentials = await getSessionCredentials(sessionId);
const payload = {
tableName,
recordNum,
sectionId
};
const response = await AcaiHttpClient.getModuleConfigVars(
credentials,
credentials.token,
credentials.tokenHash,
payload
);
const apiError = handleApiResponse(response.data, 'get_module_config_vars');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
action: 'get_module_config_vars',
tableName,
recordNum,
sectionId,
data: response.data?.data ?? response.data
}, null, 2)
}]
};
} catch (error) {
return handleToolError(error, 'get_module_config_vars', { tableName, recordNum, sectionId });
}
})
);
}

View File

@@ -0,0 +1,96 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerGetRecordTool(server) {
server.tool(
"get_record",
`Get a single record by its 'num' (primary key) with full details including uploads and relations.
Table names: use WITHOUT 'cms_' prefix for tables that have a schema in cms/data/schema/.
For tables WITHOUT schema (system tables, custom tables), pass the EXACT full table name including 'cms_' prefix.
Examples:
- "productos" (has schema) → fetches from cms_productos automatically
- "cms_uploads" (no schema) → fetches with exact name, no prefix added`,
withAuthParams({
tableName: z.string().describe("Table name. Without 'cms_' prefix if it has schema, or exact name with 'cms_' prefix if no schema."),
recordNum: z.string().describe("Record 'num' (primary key)"),
loadUploads: z.boolean().optional().default(true).describe("Load upload field data (default: true)"),
loadRelations: z.boolean().optional().default(true).describe("Resolve foreign key relations (default: true)"),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ tableName, recordNum, loadUploads = true, loadRelations = true }, extra) => {
try {
const validationError = validateRequired(
{ tableName, recordNum },
['tableName', 'recordNum'],
'get_record'
);
if (validationError) return validationError;
const credentials = await getSessionCredentials(extra.sessionId);
// Detect if table name has cms_ prefix (no schema table)
const noSchema = tableName.startsWith("cms_");
const payload = {
tableName,
where: noSchema ? `num=${recordNum}` : `num=${recordNum}`,
limit: 1,
options: {
uploads: loadUploads,
relations: loadRelations,
relationsDepth: 2,
},
};
// Tables without schema need special options
if (noSchema) {
payload.options.prefix = "";
payload.options.ignoreSchema = true;
}
const response = await AcaiHttpClient.postCmsApi(
credentials,
"get",
payload,
credentials.token,
credentials.tokenHash
);
const records = response.data?.data || [];
if (records.length === 0) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: `Record num=${recordNum} not found in table '${tableName}'`,
hint: noSchema
? "Table queried with exact name (no schema mode)."
: "If the table has no schema, try with the full name including 'cms_' prefix."
}, null, 2)
}],
};
}
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
tableName,
record: records[0],
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'get_record', { tableName, recordNum });
}
})
);
}

View File

@@ -0,0 +1,26 @@
import { registerListTableRecordsTool } from './list.js';
import { registerGetRecordTool } from './getRecord.js';
import { registerCreateOrUpdateRecordTool } from './createUpdate.js';
import { registerDeleteTableRecordsTool } from './delete.js';
import { registerAddModuleToRecordTool } from './addModuleToRecord.js';
import { registerRemoveModuleFromRecordTool } from './removeModuleFromRecord.js';
import { registerListPageModulesTool } from './listPageModules.js';
import { registerReorderModuleTool } from './reorderModule.js';
import { registerToggleModuleVisibilityTool } from './toggleModuleVisibility.js';
import { registerSetModuleConfigVarsTool } from './setModuleConfigVars.js';
import { registerGetModuleConfigVarsTool } from './getModuleConfigVars.js';
export function registerRecordTools(server) {
registerListTableRecordsTool(server);
registerGetRecordTool(server);
registerCreateOrUpdateRecordTool(server);
registerDeleteTableRecordsTool(server);
registerAddModuleToRecordTool(server);
registerRemoveModuleFromRecordTool(server);
registerListPageModulesTool(server);
registerReorderModuleTool(server);
registerToggleModuleVisibilityTool(server);
registerSetModuleConfigVarsTool(server);
registerGetModuleConfigVarsTool(server);
}

View File

@@ -0,0 +1,89 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerListTableRecordsTool(server) {
server.tool(
"list_table_records",
"List or search records in a database table. Returns JSON. Default limit is 50 — request only what you need. Use 'fields' to return only the columns you need (saves tokens). ALWAYS use 'num' as primary key, NEVER 'id'. Upload fields are arrays with urlPath.",
withAuthParams({
tableName: z.string().describe("Name of the table (without 'cms_' prefix, e.g., 'productos')"),
page: z.number().optional().describe("Page number (default: 1)"),
where: z.string().optional().describe("SQL WHERE clause to filter records (e.g., \"name LIKE '%keyword%'\")"),
limit: z.number().optional().describe("Max records to return. Default: 50. Use 5-10 for previews, up to 200 max for large exports."),
fields: z.array(z.string()).optional().describe("Return only these columns (e.g., ['num', 'titulo', 'precio']). Omit to return all columns. Always include 'num' if you need record IDs."),
truncateText: z.number().optional().describe("Truncate string field values longer than this many chars. Appends '... [truncated, N chars]'. Combine with 'fields' for maximum token savings."),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ tableName, page, where, limit, fields, truncateText }, extra) => {
try {
// Validate required parameters
const validationError = validateRequired({ tableName }, ['tableName'], 'list_table_records');
if (validationError) return validationError;
// Build payload for CMS API
const credentials = await getSessionCredentials(extra.sessionId);
const payload = {
tableName: tableName,
where: where || "",
order: "",
limit: limit || 50,
options: {}
};
// Send to CMS API via viewer_functions
const response = await AcaiHttpClient.postCmsApi(
credentials,
'get',
payload,
credentials.token,
credentials.tokenHash
);
// Check for API errors
const apiError = handleApiResponse(response.data, 'list_table_records');
if (apiError) return apiError;
// Post-process: filter fields if requested
let resultData = response.data;
if (fields && fields.length > 0 && Array.isArray(resultData?.data)) {
resultData = {
...resultData,
data: resultData.data.map(record =>
Object.fromEntries(fields.map(f => [f, record[f]]))
)
};
}
// Post-process: truncate long text values if requested
if (truncateText && truncateText > 0 && Array.isArray(resultData?.data)) {
resultData = {
...resultData,
data: resultData.data.map(record => {
const truncated = {};
for (const [key, value] of Object.entries(record)) {
if (typeof value === 'string' && value.length > truncateText) {
truncated[key] = value.substring(0, truncateText) + `... [truncated, ${value.length} chars]`;
} else {
truncated[key] = value;
}
}
return truncated;
})
};
}
return {
content: [{
type: "text",
text: JSON.stringify(resultData, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'list_table_records', { tableName, page });
}
})
);
}

View File

@@ -0,0 +1,118 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerListPageModulesTool(server) {
server.tool(
"list_page_modules",
`List all builder modules placed on a page/record. Returns module IDs, section_ids, positions, visibility, and config-vars.
Use this to understand the current layout of a page before adding, removing, or reordering modules.
Table names WITHOUT 'cms_' prefix. The recordNum is the 'num' primary key.
Common table for pages: 'apartados'.`,
withAuthParams({
tableName: z.string().describe("Table name without cms_ prefix (e.g. 'apartados')"),
recordNum: z.union([z.string(), z.number()]).describe("Record num (primary key)"),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ tableName, recordNum }, extra) => {
try {
const validationError = validateRequired(
{ tableName, recordNum },
['tableName', 'recordNum'],
'list_page_modules'
);
if (validationError) return validationError;
const credentials = await getSessionCredentials(extra.sessionId);
const response = await AcaiHttpClient.postCmsApi(
credentials,
"get",
{
tableName,
where: `num=${recordNum}`,
limit: 1,
options: { uploads: false, relations: false },
},
credentials.token,
credentials.tokenHash
);
const records = response.data?.data || [];
if (records.length === 0) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: `Record num=${recordNum} not found in table '${tableName}'`,
}, null, 2)
}],
};
}
const record = records[0];
const builderRaw = record.builder || "[]";
let builderData;
try {
builderData = JSON.parse(builderRaw);
} catch {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: "Could not parse builder JSON",
raw: builderRaw.substring(0, 500),
}, null, 2)
}],
isError: true,
};
}
if (!Array.isArray(builderData)) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: "Builder field is not an array",
}, null, 2)
}],
isError: true,
};
}
const modules = builderData.map((mod, index) => ({
position: index,
moduleId: mod.modulo || null,
sectionId: mod.section_id || null,
hidden: !!mod.oculto,
configVars: mod["config-vars"] || {},
}));
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
tableName,
recordNum,
recordTitle: record.titulo || record.name || record.nombre || null,
recordEnlace: record.enlace || null,
modulesCount: modules.length,
modules,
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'list_page_modules', { tableName, recordNum });
}
})
);
}

View File

@@ -0,0 +1,78 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
export function registerRemoveModuleFromRecordTool(server) {
server.tool(
"remove_module_from_record",
`Removes a builder module from a record's builder array.
Identify the module to remove by either:
- sectionId (unique, preferred) — get it from the builder JSON of the record
- modulePosition (0-based index) — position in the builder array
Required: tableName + recordNum + (sectionId OR modulePosition)`,
withAuthParams({
tableName: z.string().describe("Table name without cms_ prefix, e.g. 'apartados'"),
recordNum: z.union([z.string(), z.number()]).describe("Record num (primary key)"),
sectionId: z.string().optional().describe("section_id of the module to remove (preferred)"),
modulePosition: z.number().optional().describe("Position in builder array (0-based). Use if sectionId not available."),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, recordNum, sectionId, modulePosition }, extra) => {
try {
const validationError = validateRequired(
{ tableName, recordNum },
['tableName', 'recordNum'],
'remove_module_from_record'
);
if (validationError) return validationError;
if (!sectionId && modulePosition === undefined) {
return {
content: [{ type: "text", text: "Error: sectionId or modulePosition is required" }],
isError: true,
};
}
const credentials = await getSessionCredentials(extra.sessionId);
const payload = {
tableName,
recordNum,
};
if (sectionId) payload.sectionId = sectionId;
if (modulePosition !== undefined) payload.modulePosition = modulePosition;
const response = await AcaiHttpClient.postViewerAction(
credentials,
"removeModuleFromRecord",
payload,
credentials.token,
credentials.tokenHash,
{},
15000
);
const apiError = handleApiResponse(response.data, 'remove_module_from_record');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
action: 'remove_module_from_record',
tableName,
recordNum,
...response.data,
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'remove_module_from_record', { tableName, recordNum, sectionId, modulePosition });
}
})
);
}

View File

@@ -0,0 +1,64 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
export function registerReorderModuleTool(server) {
server.tool(
"reorder_module",
`Move a module from one position to another in a record's builder array.
Use list_page_modules first to see current positions.
Table names WITHOUT 'cms_' prefix. The recordNum is the 'num' primary key.`,
withAuthParams({
tableName: z.string().describe("Table name without cms_ prefix (e.g. 'apartados')"),
recordNum: z.union([z.string(), z.number()]).describe("Record num (primary key)"),
fromPosition: z.number().describe("Current position of the module (0-based)"),
toPosition: z.number().describe("Target position to move the module to (0-based)"),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, recordNum, fromPosition, toPosition }, extra) => {
try {
const validationError = validateRequired(
{ tableName, recordNum },
['tableName', 'recordNum'],
'reorder_module'
);
if (validationError) return validationError;
const credentials = await getSessionCredentials(extra.sessionId);
const response = await AcaiHttpClient.postViewerAction(
credentials,
"reorderModule",
{
tableName,
recordNum,
fromPosition,
toPosition,
},
credentials.token,
credentials.tokenHash,
{},
15000
);
const apiError = handleApiResponse(response.data, 'reorder_module');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
action: 'reorder_module',
...response.data,
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'reorder_module', { tableName, recordNum, fromPosition, toPosition });
}
})
);
}

View File

@@ -0,0 +1,70 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleApiResponse, handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerSetModuleConfigVarsTool(server) {
server.tool(
"set_module_config_vars",
`Set configuration variables for a module instance on a page record. Supports simple vars (text, list, checkbox, colorpicker, etc.) and multi/repeater vars (records array). For simple vars, pass key-value pairs. For multi vars, pass an array of objects with sub-var values.
All field types are passed the same way as string values. Fields like list, checkbox and colorpicker are stored directly in config-vars (not in builder_custom). Text, title, wysiwyg and upload fields are stored in builder_custom automatically.
The response includes 'uploadFields' — a map of upload variable names to their recordNum and fieldName. Use these directly with upload_record_image (tableName="builder_custom") without needing to read builder.json. For multi vars with uploads, the key is "varName.subVarName" and the value is an array of {index, fieldName, recordNum}.
Required params:
- tableName (string) without 'cms_' prefix
- recordNum (number) record primary key ('num' field, never 'id')
- sectionId (string) section ID of the module instance
- vars (object) variable names as keys`,
withAuthParams({
tableName: z.string().describe("Parent table name (e.g. 'apartados')"),
recordNum: z.number().describe("Parent record number"),
sectionId: z.string().describe("Section ID of the module instance"),
vars: z.record(z.any()).describe("Object with variable names as keys. Simple vars: string values. Multi vars: array of objects with sub-var values. Example: { titulo: 'My Title', records: [{ pregunta: 'Q1', respuesta: 'A1' }] }")
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, recordNum, sectionId, vars }, extra) => {
try {
const validationError = validateRequired({ tableName, recordNum, sectionId, vars }, ['tableName', 'recordNum', 'sectionId', 'vars'], 'set_module_config_vars');
if (validationError) return validationError;
const sessionId = extra.sessionId;
const credentials = await getSessionCredentials(sessionId);
const payload = {
tableName,
recordNum,
sectionId,
vars
};
const response = await AcaiHttpClient.setModuleConfigVars(
credentials,
credentials.token,
credentials.tokenHash,
payload
);
const apiError = handleApiResponse(response.data, 'set_module_config_vars');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
action: 'set_module_config_vars',
tableName,
recordNum,
sectionId,
data: response.data?.data ?? response.data
}, null, 2)
}]
};
} catch (error) {
return handleToolError(error, 'set_module_config_vars', { tableName, recordNum, sectionId });
}
})
);
}

View File

@@ -0,0 +1,76 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
export function registerToggleModuleVisibilityTool(server) {
server.tool(
"toggle_module_visibility",
`Show or hide a module on a page without removing it.
Identify the module by sectionId (preferred) or modulePosition.
Optionally set visible=true/false explicitly, or omit to toggle.
Table names WITHOUT 'cms_' prefix. The recordNum is the 'num' primary key.`,
withAuthParams({
tableName: z.string().describe("Table name without cms_ prefix (e.g. 'apartados')"),
recordNum: z.union([z.string(), z.number()]).describe("Record num (primary key)"),
sectionId: z.string().optional().describe("section_id of the module (preferred)"),
modulePosition: z.number().optional().describe("Position in builder array (0-based)"),
visible: z.boolean().optional().describe("Set explicitly: true=show, false=hide. Omit to toggle."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, recordNum, sectionId, modulePosition, visible }, extra) => {
try {
const validationError = validateRequired(
{ tableName, recordNum },
['tableName', 'recordNum'],
'toggle_module_visibility'
);
if (validationError) return validationError;
if (!sectionId && modulePosition === undefined) {
return {
content: [{ type: "text", text: "Error: sectionId or modulePosition is required" }],
isError: true,
};
}
const credentials = await getSessionCredentials(extra.sessionId);
const payload = {
tableName,
recordNum,
};
if (sectionId) payload.sectionId = sectionId;
if (modulePosition !== undefined) payload.modulePosition = modulePosition;
if (visible !== undefined) payload.visible = visible;
const response = await AcaiHttpClient.postViewerAction(
credentials,
"toggleModuleVisibility",
payload,
credentials.token,
credentials.tokenHash,
{},
15000
);
const apiError = handleApiResponse(response.data, 'toggle_module_visibility');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
action: 'toggle_module_visibility',
...response.data,
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'toggle_module_visibility', { tableName, recordNum, sectionId, modulePosition });
}
})
);
}