Initial commit
This commit is contained in:
78
mcp-server/tools/modules/check.js
Normal file
78
mcp-server/tools/modules/check.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { z } from "zod";
|
||||
import { withAuth, getSessionCredentials } from "../../auth/index.js";
|
||||
import { handleToolError, handleApiResponse, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
|
||||
import { withAuthParams } from "../helpers/authSchema.js";
|
||||
|
||||
export function registerCheckModuleTool(server) {
|
||||
server.tool(
|
||||
"check_module",
|
||||
"Preview how a module renders with sample data. Returns a preview (first 50 lines + summary) by default — use fullRender=true for complete output. Always shows errors in full.",
|
||||
withAuthParams({
|
||||
moduleName: z.string().describe("Module ID/name to check"),
|
||||
vars: z.record(z.string(), z.any()).describe("Object with builder variable values. Keys should match the variable names from data-field-label (without spaces/special chars)"),
|
||||
fullRender: z.boolean().optional().describe("If true, returns complete rendered HTML. Default: false (preview — first 50 lines + summary, saves tokens)."),
|
||||
}),
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
withAuth(async ({ moduleName, vars, fullRender }, extra) => {
|
||||
const startTime = Date.now();
|
||||
console.error(`[Tool] check_module - START: moduleName=${moduleName}, varsCount=${Object.keys(vars || {}).length}, sessionId=${extra.sessionId}`);
|
||||
|
||||
try {
|
||||
// Validate required parameters
|
||||
const validationError = validateRequired(
|
||||
{ moduleName, vars },
|
||||
['moduleName', 'vars'],
|
||||
'check_module'
|
||||
);
|
||||
if (validationError) {
|
||||
console.error(`[Tool] check_module - VALIDATION ERROR: ${validationError.content[0].text}`);
|
||||
return validationError;
|
||||
}
|
||||
|
||||
const credentials = await getSessionCredentials(extra.sessionId);
|
||||
|
||||
const payload = {
|
||||
moduleName: moduleName,
|
||||
vars: vars
|
||||
};
|
||||
|
||||
console.error(`[Tool] check_module - Calling AcaiHttpClient.checkModuleCode...`);
|
||||
const response = await AcaiHttpClient.checkModuleCode(credentials, credentials.token, payload);
|
||||
|
||||
// Check for API errors in response
|
||||
/*const apiError = handleApiResponse(response.data, 'check_module');
|
||||
if (apiError) {
|
||||
console.error(`[Tool] check_module - API ERROR: ${apiError.content[0].text}`);
|
||||
return apiError;
|
||||
}*/
|
||||
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
console.error(`[Tool] check_module - SUCCESS: completed in ${elapsedTime}ms`);
|
||||
|
||||
let outputText = `Module Preview for "${moduleName}":\n\n${JSON.stringify(response.data, null, 2)}`;
|
||||
|
||||
// Preview mode (default): truncate to first 50 lines + summary
|
||||
if (!fullRender) {
|
||||
const lines = outputText.split('\n');
|
||||
const PREVIEW_LINES = 50;
|
||||
if (lines.length > PREVIEW_LINES) {
|
||||
const preview = lines.slice(0, PREVIEW_LINES).join('\n');
|
||||
outputText = `${preview}\n\n--- PREVIEW: showing ${PREVIEW_LINES} of ${lines.length} lines. Use fullRender=true for complete output. ---`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: outputText
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
console.error(`[Tool] check_module - ERROR after ${elapsedTime}ms: ${error.message}`);
|
||||
return handleToolError(error, 'check_module', { moduleName, varsCount: Object.keys(vars || {}).length });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
62
mcp-server/tools/modules/checkUsage.js
Normal file
62
mcp-server/tools/modules/checkUsage.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { z } from "zod";
|
||||
import { withAuth, getSessionCredentials, getApiClient } 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 registerCheckModuleUsageTool(server) {
|
||||
server.tool(
|
||||
"check_module_usage",
|
||||
"Check which pages/URLs use a module. Call BEFORE delete_module to verify it's safe to remove.",
|
||||
withAuthParams({
|
||||
id: z.string().describe("Module ID to check usage for"),
|
||||
}),
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
withAuth(async ({ id }, extra) => {
|
||||
try {
|
||||
// Validate required parameters
|
||||
const validationError = validateRequired({ id }, ['id'], 'check_module_usage');
|
||||
if (validationError) return validationError;
|
||||
|
||||
const credentials = await getSessionCredentials(extra.sessionId);
|
||||
|
||||
// Build the request payload
|
||||
const payload = {
|
||||
action_ws: "checkModuleInWeb",
|
||||
module: id,
|
||||
token: credentials.token,
|
||||
tokenHash: credentials.tokenHash
|
||||
};
|
||||
|
||||
// Make the request to the client's website
|
||||
const response = await AcaiHttpClient.postViewerFunctions(
|
||||
await getApiClient(extra.sessionId),
|
||||
payload
|
||||
);
|
||||
|
||||
// Check for API errors in response
|
||||
const apiError = handleApiResponse(response.data, 'check_module_usage');
|
||||
if (apiError) return apiError;
|
||||
|
||||
// Extract usage information
|
||||
const usageData = response.data.data || response.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text", text: JSON.stringify({
|
||||
success: true,
|
||||
moduleId: id,
|
||||
usage: usageData,
|
||||
canDelete: !usageData || Object.keys(usageData).length === 0,
|
||||
message: Object.keys(usageData || {}).length === 0
|
||||
? "Module is not used anywhere - safe to delete"
|
||||
: `Module is used in ${Object.keys(usageData || {}).length} location(s)`
|
||||
}, null, 2)
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'check_module_usage', { id });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
82
mcp-server/tools/modules/compile.js
Normal file
82
mcp-server/tools/modules/compile.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { z } from "zod";
|
||||
import axios from "axios";
|
||||
import path from "path";
|
||||
import { withAuth, getSessionCredentials } from "../../auth/index.js";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { withAuthParams } from "../helpers/authSchema.js";
|
||||
import { LOCAL_SERVER_URL } from "../../config/index.js";
|
||||
|
||||
export function registerCompileModuleTool(server) {
|
||||
server.tool(
|
||||
"compile_module",
|
||||
`Manually recompile a module or general section when generated files may be out of sync and you need to force compilation without editing index-base.tpl.
|
||||
Do not use this as part of the normal editing flow: most index-base.tpl edits made through the Acai file tools compile automatically.
|
||||
This is a recovery / resync tool, not a required step after routine changes.
|
||||
It parses the HTML into Twig, generates builder vars, and syncs with the Docker CMS.
|
||||
|
||||
Pass the full path to the index-base.tpl file and the project directory.`,
|
||||
withAuthParams({
|
||||
filePath: z.string().describe("Full absolute path to the index-base.tpl file that was edited"),
|
||||
projectDir: z.string().describe("Full absolute path to the project root directory"),
|
||||
}),
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
withAuth(async ({ filePath, projectDir }, extra) => {
|
||||
try {
|
||||
const validationError = validateRequired(
|
||||
{ filePath, projectDir },
|
||||
['filePath', 'projectDir'],
|
||||
'compile_module'
|
||||
);
|
||||
if (validationError) return validationError;
|
||||
|
||||
const normalizedProjectDir = path.resolve(projectDir);
|
||||
const normalizedFilePath = path.resolve(filePath);
|
||||
const projectSlug = path.basename(normalizedProjectDir);
|
||||
const relativePath = path.relative(normalizedProjectDir, normalizedFilePath);
|
||||
const canUseSlugMode =
|
||||
!!projectSlug &&
|
||||
!!relativePath &&
|
||||
relativePath !== "" &&
|
||||
!relativePath.startsWith("..") &&
|
||||
!path.isAbsolute(relativePath);
|
||||
|
||||
const payload = canUseSlugMode
|
||||
? { project: projectSlug, relativePath, project_dir: projectDir }
|
||||
: { file: filePath, project_dir: projectDir };
|
||||
|
||||
// Call the Python server compile endpoint
|
||||
const response = await axios.post(
|
||||
`${LOCAL_SERVER_URL}/api/compile-module`,
|
||||
payload,
|
||||
{ headers: { "Content-Type": "application/json" }, timeout: 30000 }
|
||||
);
|
||||
|
||||
if (response.data?.ok) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: "Module compiled successfully",
|
||||
output: response.data.output || "",
|
||||
}, null, 2)
|
||||
}],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: response.data?.error || "Compilation failed",
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'compile_module', { filePath, projectDir });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
75
mcp-server/tools/modules/create.js
Normal file
75
mcp-server/tools/modules/create.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { z } from "zod";
|
||||
import axios from "axios";
|
||||
import { withAuth } from "../../auth/index.js";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { withAuthParams } from "../helpers/authSchema.js";
|
||||
import { LOCAL_SERVER_URL } from "../../config/index.js";
|
||||
|
||||
export function registerCreateModuleTool(server) {
|
||||
server.tool(
|
||||
"create_module",
|
||||
`Create a new builder module in the project. This creates the module directory with index-base.tpl, style.css, and script.js, then compiles it automatically.
|
||||
|
||||
After creating the module, use add_module_to_record to place it on a page, then set_module_config_vars to fill its variables with content.
|
||||
|
||||
Parameters:
|
||||
- moduleId: unique identifier (lowercase, underscores, e.g. "hero_banner")
|
||||
- html: the Twig/HTML content for index-base.tpl
|
||||
- css: optional CSS for style.css
|
||||
- js: optional JavaScript for script.js
|
||||
- php: optional PHP code for module hook file .php
|
||||
- label: human-readable name (e.g. "Hero Banner V2")
|
||||
- description: brief description of what the module does`,
|
||||
withAuthParams({
|
||||
moduleId: z.string().describe("Module identifier (lowercase, underscores, e.g. 'hero_banner')"),
|
||||
html: z.string().describe("HTML/Twig content for index-base.tpl ( needed for compile module )"),
|
||||
css: z.string().optional().default("").describe("CSS content for style.css ( optional, you can also add CSS later on file )"),
|
||||
js: z.string().optional().default("").describe("JavaScript content for script.js ( optional, you can also add CSS later on file )"),
|
||||
php: z.string().optional().default("").describe("PHP code for module hook file .php ( optional, you can also add CSS later on file )"),
|
||||
label: z.string().optional().default("").describe("Human-readable module name"),
|
||||
description: z.string().optional().default("").describe("Brief description"),
|
||||
}),
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
withAuth(async ({ moduleId, html, css, js, php, label, description }, extra) => {
|
||||
try {
|
||||
const validationError = validateRequired({ moduleId, html }, ['moduleId', 'html'], 'create_module');
|
||||
if (validationError) return validationError;
|
||||
|
||||
const projectDir = process.env.ACAI_PROJECT_DIR || "";
|
||||
if (!projectDir) {
|
||||
return { content: [{ type: "text", text: "Error: ACAI_PROJECT_DIR not set" }], isError: true };
|
||||
}
|
||||
|
||||
moduleId = moduleId.toLowerCase().replace(/\s+/g, '_'); // Ensure moduleId is lowercase and uses underscores
|
||||
moduleId = moduleId + "_" + (Math.random().toString(36).substring(2, 8).toUpperCase());
|
||||
|
||||
const response = await axios.post(
|
||||
`${LOCAL_SERVER_URL}/api/create-module`,
|
||||
{ project_dir: projectDir, module_id: moduleId, html, css: css || "", js: js || "", label, description, php: php || "" },
|
||||
{ headers: { "Content-Type": "application/json" }, timeout: 30000 }
|
||||
);
|
||||
|
||||
if (response.data?.success) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
moduleId,
|
||||
path: response.data.path,
|
||||
compiled: response.data.compiled,
|
||||
note: response.data.compiled
|
||||
? "Module created and compiled. Use add_module_to_record to place it on a page, then set_module_config_vars to fill its variables."
|
||||
: "Module created but compilation failed: " + (response.data.compile_output || "unknown error"),
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} else {
|
||||
return { content: [{ type: "text", text: JSON.stringify(response.data) }], isError: true };
|
||||
}
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'create_module', { moduleId });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
9
mcp-server/tools/modules/index.js
Normal file
9
mcp-server/tools/modules/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerCheckModuleTool } from './check.js';
|
||||
import { registerCheckModuleUsageTool } from './checkUsage.js';
|
||||
import { registerCompileModuleTool } from './compile.js';
|
||||
|
||||
export function registerModuleTools(server) {
|
||||
registerCheckModuleTool(server);
|
||||
registerCheckModuleUsageTool(server);
|
||||
registerCompileModuleTool(server);
|
||||
}
|
||||
93
mcp-server/tools/modules/setExampleData.js
Normal file
93
mcp-server/tools/modules/setExampleData.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { z } from "zod";
|
||||
import { withAuth, getSessionCredentials, getApiClient, getCommonParams } from "../../auth/index.js";
|
||||
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
|
||||
import { withAuthParams } from "../helpers/authSchema.js";
|
||||
|
||||
export function registerSetModuleExampleDataTool(server) {
|
||||
server.tool(
|
||||
"set_module_example_data",
|
||||
`Set example data for a module's editor preview. MANDATORY: call get_module first to get the schema, then fill EVERY variable.
|
||||
|
||||
Critical: uploads ALWAYS as [{urlPath: "..."}] (NEVER strings), multiv2 as array with 2+ items, var names from data-field-label (no spaces, lowercase). Use generate_image or placehold.co for image URLs.
|
||||
|
||||
See resource 'acai-cheat-sheet' → "Example Data Formatting" for type-specific value formats.`,
|
||||
withAuthParams({
|
||||
moduleId: z.string().describe("Module ID"),
|
||||
moduleSchema: z.object({}).passthrough().describe("Complete module schema (obtained from get_module)"),
|
||||
exampleData: z.object({}).passthrough().describe("Example data for EVERY variable in the module schema. Structure must match the schema exactly. Fill ALL variables without exception."),
|
||||
}),
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
withAuth(async ({ moduleId, moduleSchema, exampleData }, extra) => {
|
||||
try {
|
||||
// Validate required parameters
|
||||
const validationError = validateRequired({ moduleId, exampleData }, ['moduleId', 'exampleData'], 'set_module_example_data');
|
||||
if (validationError) return validationError;
|
||||
|
||||
// Validate that all schema variables are present in exampleData
|
||||
if (moduleSchema && moduleSchema.codeVars) {
|
||||
const schemaVars = Object.keys(moduleSchema.codeVars);
|
||||
const dataVars = Object.keys(exampleData);
|
||||
const missingVars = schemaVars.filter(v => !dataVars.includes(v));
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
console.warn(`[set_module_example_data] WARNING: Missing variables in exampleData: ${missingVars.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for upload fields that are not arrays
|
||||
for (const [varName, varInfo] of Object.entries(moduleSchema.codeVars)) {
|
||||
if (varInfo.type === 'upload' && exampleData[varName]) {
|
||||
if (!Array.isArray(exampleData[varName])) {
|
||||
console.error(`[set_module_example_data] ERROR: Upload field '${varName}' is not an array! Current value: ${JSON.stringify(exampleData[varName])}`);
|
||||
console.error(`[set_module_example_data] Upload fields MUST be arrays with urlPath objects: [{"urlPath": "..."}]`);
|
||||
} else if (exampleData[varName].length > 0 && !exampleData[varName][0].urlPath) {
|
||||
console.error(`[set_module_example_data] ERROR: Upload field '${varName}' items missing 'urlPath' property!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await getSessionCredentials(extra.sessionId);
|
||||
const client = await getApiClient(extra.sessionId);
|
||||
|
||||
// Log data for debugging
|
||||
console.error(`[set_module_example_data] Module ID: ${moduleId}`);
|
||||
console.error(`[set_module_example_data] Module Schema:`, JSON.stringify(moduleSchema, null, 2));
|
||||
console.error(`[set_module_example_data] Example Data:`, JSON.stringify(exampleData, null, 2));
|
||||
|
||||
// Prepare payload for setStaticVars action
|
||||
const payload = await getCommonParams(extra.sessionId, {
|
||||
action_ws: "setStaticVars",
|
||||
moduleId: moduleId,
|
||||
staticVars: exampleData,
|
||||
schema: moduleSchema
|
||||
});
|
||||
|
||||
console.error(`[set_module_example_data] Full Payload:`, JSON.stringify(payload, null, 2));
|
||||
|
||||
// Send to viewer_functions
|
||||
const response = await client.post("/cms/lib/viewer_functions.php", payload);
|
||||
|
||||
console.error(`[set_module_example_data] Response:`, JSON.stringify(response.data, null, 2));
|
||||
|
||||
// Check for API errors in response
|
||||
const apiError = handleApiResponse(response.data, 'set_module_example_data');
|
||||
if (apiError) return apiError;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text", text: JSON.stringify({
|
||||
success: true,
|
||||
message: `Example data set successfully for module '${moduleId}'`,
|
||||
moduleId: moduleId,
|
||||
dataCount: Object.keys(exampleData).length,
|
||||
schemaVarsCount: moduleSchema?.codeVars ? Object.keys(moduleSchema.codeVars).length : 0,
|
||||
response: response.data
|
||||
}, null, 2)
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'set_module_example_data', { moduleId });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user