mcp tablas

This commit is contained in:
Jordan Diaz
2026-04-25 08:51:17 +00:00
parent 62239cb0a5
commit e84a36c83d
17 changed files with 535 additions and 497 deletions

View File

@@ -0,0 +1,52 @@
import { pythonPost } from "../helpers/pythonServerClient.js";
import { getCurrentProjectInfo } from "../files/helpers.js";
/**
* Llama a un endpoint /api/schema/* del server Python.
*
* Todas las schema-tools comparten el mismo contrato:
* - Resolver projectSlug desde la sesion actual.
* - POST al endpoint con { project, ...body }.
* - Mapear respuesta a formato MCP conservando warnings/schema/etc.
*
* No validamos payloads aqui: la responsabilidad es del caller (zod) y
* del backend Python (validaciones fuertes + proxy al PHP).
*
* @param {string} endpoint - Path relativo, ej: "/api/schema/create-table"
* @param {object} body - Campos especificos de la tool (sin `project`)
* @returns {Promise<{mcp: object}>} Respuesta lista para devolver desde la tool
*/
export async function callSchemaEndpoint(endpoint, body) {
const { projectSlug } = getCurrentProjectInfo();
const result = await pythonPost(endpoint, { project: projectSlug, ...body });
// success=false -> error con los campos utiles del backend (error,
// warnings, recordCount, dataCount, ...) preservados para el LLM.
if (!result || result.success === false) {
const { success: _s, ...rest } = result || {};
return {
mcp: {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: (result && result.error) || "Schema endpoint returned no success",
...rest,
}, null, 2),
}],
isError: true,
},
};
}
// success=true -> devolvemos el payload completo (incluye warnings,
// schema, recordCount, etc. cuando el backend los adjunta).
return {
mcp: {
content: [{
type: "text",
text: JSON.stringify(result, null, 2),
}],
},
};
}

View File

@@ -1,99 +1,51 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { SAAS_URL } from "../../config/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { AcaiHttpClient, FormParamsBuilder } from "../helpers/acaiHttpClient.js";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: create_table
// Crea una tabla nueva delegando en /api/schema/create-table. Enviamos solo
// "intencion" (menuType + flags) — el server Python construye el schemaPreset
// por defecto. Los tableNames viajan SIN el prefijo `cms_`; la PK siempre es `num`.
export function registerCreateTableTool(server) {
server.tool(
"create_table",
"Create a new database table/schema in the system. This creates the table structure with basic configuration. After creation, you can use update_table_schema to add custom fields and modify the schema. Table types: 'multi' (multiple records like news, contacts), 'single' (single record like homepage), 'category' (category menu), 'separador' (menu separator/container). Table names are WITHOUT the 'cms_' prefix.",
`Create a new database table/module for the current Acai project.
Menu types:
- 'multi': regular table with many records (news, products, contacts...)
- 'single': single-record page (homepage, about us...)
- 'category': category container — groups other tables under a menu node
- 'separador': visual separator in the admin menu
Parameters:
- tableName: technical name, lowercase + underscores, WITHOUT 'cms_' prefix. Primary key is always 'num'.
- menuName: display name in the admin sidebar.
- enlace: REQUIRED. Whether the table participates in public URLs (generates the 'enlace' field + slug). This is an architectural decision — ALWAYS ask the user before calling this tool.
- seoMetas: adds SEO meta fields (title, description, og:image). Default false.
- menuOrder: optional integer for sidebar order. Backend assigns one if omitted.`,
withAuthParams({
menuName: z.string().describe("Display name for the menu (e.g., 'Noticias', 'Productos')"),
tableName: z.string().describe("Technical table name, lowercase with underscores (e.g., 'noticias', 'productos'). Will be auto-generated from menuName if not provided."),
type: z.enum(["multi", "single", "category", "separador"]).describe("Table type: 'multi' for multiple records, 'single' for single record, 'category' for category menu, 'separador' for menu separator"),
enlace: z.boolean().describe("Whether this table should include the 'enlace' field (true = generates general section URLs, false = no enlace). Ask the user before running this tool."),
seo_metas: z.boolean().optional().describe("Whether this table has SEO meta fields. Default: false"),
menuOrder: z.number().optional().describe("Order in the menu. If not provided, will be added at the end."),
tableName: z.string().describe("Technical table name, lowercase + underscores, without 'cms_' prefix"),
menuName: z.string().describe("Display name shown in the admin sidebar"),
menuType: z.enum(["multi", "single", "category", "separador"]).describe("'multi' | 'single' | 'category' | 'separador'"),
enlace: z.boolean().describe("Whether the table has public URLs (generates 'enlace' field). REQUIRED — ask the user first."),
seoMetas: z.boolean().optional().describe("Include SEO meta fields. Default false."),
menuOrder: z.number().int().optional().describe("Order in the admin sidebar. Backend picks one if omitted."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ menuName, tableName, type, enlace, seo_metas = false, menuOrder }, extra) => {
withAuth(async ({ tableName, menuName, menuType, enlace, seoMetas, menuOrder }, _extra) => {
try {
// Validate required parameters
const validationError = validateRequired(
{ menuName, tableName, type, enlace },
['menuName', 'tableName', 'type', 'enlace'],
'create_table'
);
if (validationError) return validationError;
const body = { tableName, menuName, menuType, enlace };
if (typeof seoMetas === "boolean") body.seoMetas = seoMetas;
if (typeof menuOrder === "number") body.menuOrder = menuOrder;
if (typeof enlace !== "boolean") {
return {
content: [{ type: "text", text: "Error: 'enlace' must be explicitly set to true or false before calling this tool." }],
isError: true,
};
}
// If menuOrder not provided, get max order from existing tables
let order = menuOrder;
if (!order) {
try {
const credentials = await getSessionCredentials(extra.sessionId);
const tablesResponse = await AcaiHttpClient.saasPostRequest(
{
action: "getSchemaTables",
type: "acai"
},
credentials.token
);
if (tablesResponse.data.result && tablesResponse.data.data) {
const orders = tablesResponse.data.data.map(t => t.menuOrder || 0);
order = Math.max(...orders, 0) + 1;
} else {
order = 1;
}
} catch (e) {
order = 1;
}
}
// Create table via Acai CMS admin using centralized HTTP client
const params = FormParamsBuilder.buildTableCreateParams(menuName, tableName, type, enlace, seo_metas, order);
const credentials = await getSessionCredentials(extra.sessionId);
const createResponse = await AcaiHttpClient.postAdminForm(
credentials.website,
params,
credentials.token
);
// Check for API errors
const apiError = handleApiResponse(createResponse.data, 'create_table');
if (apiError) return apiError;
// Log response for debugging (stderr to avoid corrupting MCP stream)
console.error("CMS Response:", createResponse.data);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: "Table created successfully",
tableName: tableName,
menuName: menuName,
type: type,
menuOrder: order,
note: "Table created. You can now use get_table_schema to view it or update_table_schema to add custom fields."
}, null, 2)
}],
};
const { mcp } = await callSchemaEndpoint("/api/schema/create-table", body);
return mcp;
} catch (error) {
return handleToolError(error, 'create_table', { menuName, tableName, type });
return handleToolError(error, "create_table", { tableName, menuType });
}
})
);
}

View File

@@ -0,0 +1,55 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: create_field
// Crea un nuevo campo en una tabla existente. Backend aplica los defaults
// segun `type` y permite overrides via `initialProps`.
const FIELD_TYPES = [
"textfield", "textbox", "wysiwyg", "date", "list",
"checkbox", "upload", "multitext", "codigo", "separator",
];
export function registerCreateFieldTool(server) {
server.tool(
"create_field",
`Add a new field to an existing table.
Field types:
- textfield: single-line text
- textbox: multi-line plain text
- wysiwyg: rich text editor
- codigo: code editor (HTML/JS/CSS snippet)
- date: date/datetime picker
- list: select/radio/checkboxes (needs listType + optionsType in initialProps)
- checkbox: boolean
- upload: file upload (images/docs)
- multitext: repeater of text entries
- separator: visual separator in the form (no data column)
'initialProps' is optional; use it to override defaults (e.g. {isRequired:1, maxLength:100}).
Table names WITHOUT 'cms_' prefix. Primary key is always 'num'.`,
withAuthParams({
tableName: z.string().describe("Table name without 'cms_' prefix"),
fieldName: z.string().describe("New field name (SQL-safe identifier)"),
label: z.string().describe("Human-readable label shown in the admin form"),
type: z.enum(FIELD_TYPES).describe("Field type"),
initialProps: z.object({}).passthrough().optional().describe("Optional overrides for the default field config"),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, fieldName, label, type, initialProps }, _extra) => {
try {
const body = { tableName, fieldName, label, type };
if (initialProps && typeof initialProps === "object") body.initialProps = initialProps;
const { mcp } = await callSchemaEndpoint("/api/schema/create-field", body);
return mcp;
} catch (error) {
return handleToolError(error, "create_field", { tableName, fieldName, type });
}
})
);
}

View File

@@ -1,52 +1,49 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { AcaiHttpClient, FormParamsBuilder } from "../helpers/acaiHttpClient.js";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: delete_table
// Borra el schema (ini.php) de una tabla. Si dropData=true tambien hace DROP TABLE
// en MySQL — destruye los datos de forma IRREVERSIBLE. Si la tabla tiene registros
// y dropData=false el backend devuelve error con recordCount (intencional: no se
// debe borrar el schema dejando datos huerfanos en MySQL sin confirmacion explicita).
export function registerDeleteTableTool(server) {
server.tool(
"delete_table",
"⚠️ DANGEROUS: Delete a database table/module entirely. This removes the table definition and all its data. This operation is IRREVERSIBLE. Table names are WITHOUT the 'cms_' prefix.",
`Delete a table from the project. IRREVERSIBLE when dropData=true.
Behaviour:
- dropData=false (default): deletes the schema (.ini.php). If the MySQL table
has records, the backend refuses and returns recordCount so you can warn
the user.
- dropData=true: DROP TABLE in MySQL + delete schema. Data is permanently
destroyed.
- dryRun=true: does not delete anything, only reports recordCount. Use as a
pre-flight check before asking the user for confirmation.
Table names are WITHOUT the 'cms_' prefix.`,
withAuthParams({
tableName: z.string().describe("Name of the table/module to delete (without 'cms_' prefix, e.g., 'equipo')"),
tableName: z.string().describe("Table name without 'cms_' prefix"),
dropData: z.boolean().optional().describe("If true, DROP the MySQL table. Default false."),
dryRun: z.boolean().optional().describe("If true, only report recordCount without deleting. Default false."),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName }, extra) => {
withAuth(async ({ tableName, dropData, dryRun }, _extra) => {
try {
// Validate required parameters
const validationError = validateRequired({ tableName }, ['tableName'], 'delete_table');
if (validationError) return validationError;
// Build delete table parameters using centralized builder
const params = FormParamsBuilder.buildTableDeleteParams(tableName);
const credentials = await getSessionCredentials(extra.sessionId);
// Delete table via Acai CMS admin using centralized HTTP client
const response = await AcaiHttpClient.postAdminForm(
credentials.website,
params,
credentials.token
);
// Check for API errors
const apiError = handleApiResponse(response.data, 'delete_table');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Table '${tableName}' deleted successfully`
}, null, 2)
}],
const body = {
tableName,
confirm: true,
dropData: dropData === true,
dryRun: dryRun === true,
};
const { mcp } = await callSchemaEndpoint("/api/schema/delete-table", body);
return mcp;
} catch (error) {
return handleToolError(error, 'delete_table', { tableName });
return handleToolError(error, "delete_table", { tableName, dropData: dropData === true, dryRun: dryRun === true });
}
})
);
}

View File

@@ -0,0 +1,45 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: delete_field
// Borra un campo del schema y opcionalmente su columna MySQL (dropColumn=true).
// Si dropColumn=false y la columna tiene datos, el backend devuelve error con
// dataCount para permitir confirmacion explicita.
export function registerDeleteFieldTool(server) {
server.tool(
"delete_field",
`Delete a field from a table.
- dropColumn=false (default): removes only the schema entry. If the MySQL
column has data, backend refuses and returns dataCount so you can warn
the user.
- dropColumn=true: ALTER TABLE DROP COLUMN. Data in that column is lost
permanently.
Table names WITHOUT 'cms_' prefix.`,
withAuthParams({
tableName: z.string().describe("Table name without 'cms_' prefix"),
fieldName: z.string().describe("Field name to delete"),
dropColumn: z.boolean().optional().describe("If true, DROP COLUMN in MySQL. Default false."),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, fieldName, dropColumn }, _extra) => {
try {
const body = {
tableName,
fieldName,
confirm: true,
dropColumn: dropColumn === true,
};
const { mcp } = await callSchemaEndpoint("/api/schema/delete-field", body);
return mcp;
} catch (error) {
return handleToolError(error, "delete_field", { tableName, fieldName, dropColumn: dropColumn === true });
}
})
);
}

View File

@@ -1,207 +0,0 @@
import { z } from "zod";
import fsPromises from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { AcaiHttpClient, FormParamsBuilder } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export function registerEditTableFieldTool(server) {
server.tool(
"edit_table_field",
`Create or edit fields in a database table. Use this for ALL field operations — do NOT use update_table_schema.
Tables WITHOUT 'cms_' prefix. Field types: textfield, textbox, wysiwyg, codigo, checkbox, date, list, multitext, upload, separator, none.
For 'list': set optionsType to 'text', 'table', or 'query' with corresponding option params.
TIP: Don't set isRequired=true on upload fields.`,
withAuthParams({
tableName: z.string().describe("Name of the table (without 'cms_' prefix)"),
fields: z.array(z.object({
fieldname: z.string().describe("Current field name (for editing) or new field name (for creating)"),
newFieldname: z.string().optional().describe("New field name if renaming the field. Leave empty if not renaming."),
label: z.string().optional().describe("Field label shown in the UI"),
type: z.enum(["textfield", "textbox", "wysiwyg", "codigo", "checkbox", "date", "list", "multitext", "upload", "separator", "none"]).optional().describe("Field type"),
order: z.number().optional().describe("Display order in the form"),
defaultValue: z.string().optional().describe("Default value for the field"),
description: z.string().optional().describe("Field description/help text"),
isRequired: z.union([z.number(), z.boolean()]).optional().describe("Whether field is required (0/1 or false/true)"),
isUnique: z.union([z.number(), z.boolean()]).optional().describe("Whether field must be unique (0/1 or false/true)"),
// List field options
listType: z.enum(["pulldown", "radios", "pulldownMulti", "checkboxes"]).optional().describe("For 'list' type: how to display options"),
optionsType: z.enum(["text", "table", "query"]).optional().describe("For 'list' type: source of options"),
optionsText: z.string().optional().describe("For optionsType='text': newline-separated options (use 'value|Label' format)"),
optionsTablename: z.string().optional().describe("For optionsType='table': source table name"),
optionsValueField: z.string().optional().describe("For optionsType='table': field to use as value"),
optionsLabelField: z.string().optional().describe("For optionsType='table': field to display as label"),
optionsQuery: z.string().optional().describe("For optionsType='query': SQL query to get options"),
// Validation
minLength: z.number().optional().describe("Minimum length for text fields"),
maxLength: z.number().optional().describe("Maximum length for text fields"),
// Upload field options
allowedExtensions: z.string().optional().describe("For 'upload' type: comma-separated file extensions"),
maxUploads: z.number().optional().describe("For 'upload' type: maximum number of files"),
createThumbnails: z.union([z.number(), z.boolean()]).optional().describe("For 'upload' type: create thumbnails (0/1)"),
maxThumbnailWidth: z.number().optional().describe("For 'upload' type: thumbnail width"),
maxThumbnailHeight: z.number().optional().describe("For 'upload' type: thumbnail height"),
// Advanced options
isSystemField: z.union([z.number(), z.boolean()]).optional().describe("System field, cannot be edited by users (0/1)"),
adminOnly: z.union([z.number(), z.boolean()]).optional().describe("Only admin can modify (0/1)"),
fieldWidth: z.number().optional().describe("Field width in pixels"),
fieldHeight: z.number().optional().describe("Field height in pixels (for textbox, wysiwyg, codigo)"),
}).passthrough()).describe("Array of field configurations. Each field can include any properties from fieldData.json."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, fields }, extra) => {
const startTime = Date.now();
console.error(`[Tool] edit_table_field - START: tableName=${tableName}, fieldCount=${fields.length}, sessionId=${extra.sessionId}`);
try {
// Validate required parameters
const validationError = validateRequired(
{ tableName, fields },
['tableName', 'fields'],
'edit_table_field'
);
if (validationError) {
console.error(`[Tool] edit_table_field - VALIDATION ERROR: ${validationError.content[0].text}`);
return validationError;
}
// Load fieldData.json as template (from server root directory)
const fieldDataPath = path.join(__dirname, '..', '..', 'fieldData.json');
let fieldDataTemplate;
try {
const fieldDataRaw = await fsPromises.readFile(fieldDataPath, 'utf-8');
fieldDataTemplate = JSON.parse(fieldDataRaw);
} catch (error) {
return {
content: [{ type: "text", text: `Error loading fieldData.json template: ${error.message}. Make sure fieldData.json exists in the server directory.` }],
isError: true,
};
}
// Build multipleFields array
const multipleFields = fields.map(fieldConfig => {
const { fieldname, newFieldname, ...restConfig } = fieldConfig;
// Build the complete field data by merging template with provided config
const fieldData = {
...fieldDataTemplate,
...restConfig,
fieldname: fieldname,
newFieldname: newFieldname || fieldname,
};
// Convert boolean values to 0/1 for compatibility
Object.keys(fieldData).forEach(key => {
if (typeof fieldData[key] === 'boolean') {
fieldData[key] = fieldData[key] ? 1 : 0;
}
});
return fieldData;
});
// Create URLSearchParams with root parameters using centralized builder
const params = FormParamsBuilder.buildFieldEditParams(`${tableName}`, multipleFields);
const credentials = await getSessionCredentials(extra.sessionId);
// Send to Acai CMS admin.php using centralized HTTP client
const response = await AcaiHttpClient.postAdminForm(
credentials.website,
params,
credentials.token
);
// Check for error response
if (response.data && typeof response.data === 'string' && response.data.trim().length > 0) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
message: "Field operation completed with message",
serverResponse: response.data,
tableName: tableName,
fieldsCount: fields.length
}, null, 2)
}],
};
}
const elapsedTime = Date.now() - startTime;
console.error(`[Tool] edit_table_field - SUCCESS: completed in ${elapsedTime}ms, fieldsCount=${fields.length}`);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: fields.length === 1
? `Field '${fields[0].fieldname}' processed successfully`
: `${fields.length} fields processed successfully`,
tableName: tableName,
fieldsProcessed: fields.map(f => f.newFieldname || f.fieldname),
debugResponse: response.data
}, null, 2)
}],
};
} catch (error) {
const elapsedTime = Date.now() - startTime;
console.error(`[Tool] edit_table_field - ERROR after ${elapsedTime}ms: ${error.message}`);
return handleToolError(error, 'edit_table_field', { tableName, fieldCount: fields.length });
}
})
);
}
export function registerDeleteTableFieldTool(server) {
server.tool(
"delete_table_field",
"Delete a field from a database table structure. WARNING: This will delete all data in this column. Table names are WITHOUT the 'cms_' prefix.",
withAuthParams({
tableName: z.string().describe("Name of the table (without 'cms_' prefix)"),
fieldname: z.string().describe("Name of the field to delete"),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, fieldname }, extra) => {
try {
// Build delete field parameters using centralized builder
const params = FormParamsBuilder.buildFieldDeleteParams(`cms_${tableName}`, fieldname);
const credentials = await getSessionCredentials(extra.sessionId);
// Delete field via Acai CMS admin using centralized HTTP client
const response = await AcaiHttpClient.postAdminForm(
credentials.website,
params,
credentials.token
);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Field '${fieldname}' deleted from table '${tableName}'`,
tableName: tableName
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'delete_table_field', { tableName, fieldname });
}
})
);
}

View File

@@ -1,7 +1,25 @@
import { registerListTablesTool } from './list.js';
import { registerGetTableSchemaTool } from './schema.js';
import { registerCreateTableTool } from './create.js';
import { registerUpdateTableMetadataTool } from './updateMetadata.js';
import { registerDeleteTableTool } from './delete.js';
import { registerReorderTablesTool } from './reorderTables.js';
import { registerCreateFieldTool } from './createField.js';
import { registerUpdateFieldTool } from './updateField.js';
import { registerDeleteFieldTool } from './deleteField.js';
import { registerReorderFieldsTool } from './reorderFields.js';
import { registerRegenerateEnlacesTool } from './regenerateEnlaces.js';
export function registerTableTools(server) {
registerListTablesTool(server);
registerGetTableSchemaTool(server);
registerCreateTableTool(server);
registerUpdateTableMetadataTool(server);
registerDeleteTableTool(server);
registerReorderTablesTool(server);
registerCreateFieldTool(server);
registerUpdateFieldTool(server);
registerDeleteFieldTool(server);
registerReorderFieldsTool(server);
registerRegenerateEnlacesTool(server);
}

View File

@@ -0,0 +1,45 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: regenerate_enlaces
// Recalcula el campo `enlace` (slug) de todos los registros de una tabla.
// Cambia URLs publicas — destructivo para SEO / links externos. Si
// generateAlias=true se crean entradas en `alias_urls` para que los links
// viejos sigan funcionando.
export function registerRegenerateEnlacesTool(server) {
server.tool(
"regenerate_enlaces",
`Regenerate the 'enlace' (URL slug) of every record in a table.
Changes PUBLIC URLs — anything linking to the old slugs (external sites, saved
bookmarks, search engines) will 404 unless you opt into alias redirects.
- generateAlias=false (default): only updates the 'enlace' column. Old URLs
return 404.
- generateAlias=true: also writes entries into 'alias_urls' so old URLs
redirect to the new ones. Safer choice when the table is already public.
Table names WITHOUT 'cms_' prefix.`,
withAuthParams({
tableName: z.string().describe("Table name without 'cms_' prefix"),
generateAlias: z.boolean().optional().describe("If true, write redirects into alias_urls. Default false."),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, generateAlias }, _extra) => {
try {
const body = {
tableName,
generateAlias: generateAlias === true,
};
const { mcp } = await callSchemaEndpoint("/api/schema/regenerate-enlaces", body);
return mcp;
} catch (error) {
return handleToolError(error, "regenerate_enlaces", { tableName, generateAlias: generateAlias === true });
}
})
);
}

View File

@@ -0,0 +1,30 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: reorder_fields
// Reasigna el orden de los campos editables de una tabla. Los campos de
// sistema (num, creationDate, etc.) se ignoran; solo afecta al orden visual
// en el formulario del admin.
export function registerReorderFieldsTool(server) {
server.tool(
"reorder_fields",
`Reorder fields inside a table's admin form. Pass the full ordered list of fieldNames; system fields (num, creationDate, etc.) are ignored by the backend. Data is untouched. Table names WITHOUT 'cms_' prefix.`,
withAuthParams({
tableName: z.string().describe("Table name without 'cms_' prefix"),
order: z.array(z.string().min(1)).min(1).describe("Ordered list of fieldNames"),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ tableName, order }, _extra) => {
try {
const { mcp } = await callSchemaEndpoint("/api/schema/reorder-fields", { tableName, order });
return mcp;
} catch (error) {
return handleToolError(error, "reorder_fields", { tableName, count: Array.isArray(order) ? order.length : 0 });
}
})
);
}

View File

@@ -0,0 +1,28 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: reorder_tables
// Reasigna el menuOrder de cada tabla segun el orden de la lista recibida.
// Idempotente. No afecta datos, solo el orden de presentacion en el admin.
export function registerReorderTablesTool(server) {
server.tool(
"reorder_tables",
`Reorder tables in the admin sidebar. Pass the full ordered list of tableNames; the backend reassigns menuOrder sequentially. Only the sidebar order changes — data and schemas are untouched. Table names WITHOUT 'cms_' prefix.`,
withAuthParams({
order: z.array(z.string().min(1)).min(1).describe("Ordered list of tableNames (the new sidebar order)"),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ order }, _extra) => {
try {
const { mcp } = await callSchemaEndpoint("/api/schema/reorder-tables", { order });
return mcp;
} catch (error) {
return handleToolError(error, "reorder_tables", { count: Array.isArray(order) ? order.length : 0 });
}
})
);
}

View File

@@ -0,0 +1,52 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: update_field
// Actualiza props de un campo. Puede renombrar la columna MySQL (newFieldName).
// Cambios de 'type' pueden truncar datos; el backend devuelve warnings que
// propagamos intactos — son info critica para el LLM.
export function registerUpdateFieldTool(server) {
server.tool(
"update_field",
`Update properties of an existing field.
Common 'props' keys (not exhaustive; passthrough accepted):
label, type, description, isRequired, isUnique, defaultValue,
minLength, maxLength, listType, optionsType, optionsText,
optionsTablename, optionsValueField, optionsLabelField, optionsQuery,
filterField, allowedExtensions, maxUploads, createThumbnails,
maxThumbnailWidth, maxThumbnailHeight, fieldWidth, fieldHeight,
adminOnly, charsetRule, charset, tipoTags, tipoAtributo.
Destructive cases:
- 'newFieldName' renames the MySQL column (data preserved, but any hardcoded
reference breaks).
- Changing 'type' may coerce/truncate existing data (e.g. wysiwyg -> textfield
drops HTML). The backend returns 'warnings' in the response — surface them
to the user.
Table names WITHOUT 'cms_' prefix.`,
withAuthParams({
tableName: z.string().describe("Table name without 'cms_' prefix"),
fieldName: z.string().describe("Current field name"),
newFieldName: z.string().optional().describe("If set, rename the column. Data is preserved but hardcoded references break."),
props: z.object({}).passthrough().describe("Partial props object with the keys to update"),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, fieldName, newFieldName, props }, _extra) => {
try {
const body = { tableName, fieldName, props };
if (newFieldName) body.newFieldName = newFieldName;
const { mcp } = await callSchemaEndpoint("/api/schema/update-field", body);
return mcp;
} catch (error) {
return handleToolError(error, "update_field", { tableName, fieldName, newFieldName });
}
})
);
}

View File

@@ -0,0 +1,44 @@
import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { handleToolError } from "../helpers/errorHandler.js";
import { callSchemaEndpoint } from "./_schemaEndpoint.js";
// Tool: update_table_metadata
// Edita el bloque [meta] de un schema (menuName, menuType, listPage*, etc.) y
// opcionalmente renombra la tabla en MySQL. Delegamos en /api/schema/update-table-meta.
export function registerUpdateTableMetadataTool(server) {
server.tool(
"update_table_metadata",
`Update the metadata block of a table (the [meta] section of its schema).
Accepted keys in 'meta' include:
menuName, menuDesc, menuType, menuOrder, menuDisplay, menuHidden,
controller, breadcrumbField, breadcrumbByLink, breadcrumbParentNum,
listPageFields (csv), listPageOrder, listPageSearchFields.
If 'newTableName' is provided the underlying MySQL table is RENAMED. This is
destructive because any hardcoded references (custom controllers, module SQL,
embedded queries in content) WILL break — audit the codebase before renaming.
Table names are WITHOUT the 'cms_' prefix.`,
withAuthParams({
tableName: z.string().describe("Current table name, without 'cms_' prefix"),
meta: z.object({}).passthrough().describe("Partial meta object with the keys you want to change"),
newTableName: z.string().optional().describe("If set, rename the table. Breaks hardcoded references — confirm with user first."),
}),
{ readOnlyHint: false, destructiveHint: true },
withAuth(async ({ tableName, meta, newTableName }, _extra) => {
try {
const body = { tableName, meta };
if (newTableName) body.newTableName = newTableName;
const { mcp } = await callSchemaEndpoint("/api/schema/update-table-meta", body);
return mcp;
} catch (error) {
return handleToolError(error, "update_table_metadata", { tableName, newTableName });
}
})
);
}