Initial commit
This commit is contained in:
73
mcp-server/tools/records/addModuleToRecord.js
Normal file
73
mcp-server/tools/records/addModuleToRecord.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
145
mcp-server/tools/records/createUpdate.js
Normal file
145
mcp-server/tools/records/createUpdate.js
Normal 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) });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
89
mcp-server/tools/records/delete.js
Normal file
89
mcp-server/tools/records/delete.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
63
mcp-server/tools/records/getModuleConfigVars.js
Normal file
63
mcp-server/tools/records/getModuleConfigVars.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
96
mcp-server/tools/records/getRecord.js
Normal file
96
mcp-server/tools/records/getRecord.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
26
mcp-server/tools/records/index.js
Normal file
26
mcp-server/tools/records/index.js
Normal 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);
|
||||
}
|
||||
|
||||
89
mcp-server/tools/records/list.js
Normal file
89
mcp-server/tools/records/list.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
118
mcp-server/tools/records/listPageModules.js
Normal file
118
mcp-server/tools/records/listPageModules.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
78
mcp-server/tools/records/removeModuleFromRecord.js
Normal file
78
mcp-server/tools/records/removeModuleFromRecord.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
64
mcp-server/tools/records/reorderModule.js
Normal file
64
mcp-server/tools/records/reorderModule.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
70
mcp-server/tools/records/setModuleConfigVars.js
Normal file
70
mcp-server/tools/records/setModuleConfigVars.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
76
mcp-server/tools/records/toggleModuleVisibility.js
Normal file
76
mcp-server/tools/records/toggleModuleVisibility.js
Normal 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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user