Initial commit
This commit is contained in:
345
mcp-server/tools/orchestrator/detector.js
Normal file
345
mcp-server/tools/orchestrator/detector.js
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Workflow auto-detection engine.
|
||||
* Keyword-based pattern matching with weighted scoring + contextual adjustments.
|
||||
* No LLM call needed — fast and deterministic.
|
||||
*/
|
||||
|
||||
const WORKFLOW_PATTERNS = {
|
||||
create_section: {
|
||||
keywords: [
|
||||
"crear seccion", "create section", "nueva seccion", "new section",
|
||||
"anadir seccion", "add section", "crear tabla", "create table",
|
||||
"nueva pagina", "new page", "nueva seccion web", "new web section",
|
||||
"montar seccion", "set up section", "configurar seccion",
|
||||
// Additional English patterns
|
||||
"build section", "build page", "make section", "make page",
|
||||
"set up page", "create page", "new table",
|
||||
"section for", "seccion de", "seccion para",
|
||||
// Natural phrasing
|
||||
"want section", "need section", "quiero seccion",
|
||||
"necesito seccion", "hacer seccion", "hacer pagina"
|
||||
],
|
||||
boost: [
|
||||
"categoria", "category", "productos", "products", "blog", "noticias",
|
||||
"news", "equipo", "team", "servicios", "services", "galeria", "gallery",
|
||||
"portfolio", "testimonios", "testimonials", "faq", "preguntas",
|
||||
"clientes", "clients", "proyectos", "projects",
|
||||
"restaurante", "restaurant", "tienda", "store", "shop",
|
||||
"eventos", "events", "cursos", "courses"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
populate_content: {
|
||||
keywords: [
|
||||
"anadir contenido", "add content", "crear registros", "create records",
|
||||
"poblar", "populate", "rellenar", "fill", "bulk", "masivo",
|
||||
"insertar datos", "insert data", "meter datos", "cargar contenido",
|
||||
"load content", "contenido de ejemplo", "sample content",
|
||||
"crear entradas", "create entries", "anadir registros", "add records",
|
||||
"registros de ejemplo", "sample records", "meter registros",
|
||||
"fill with data", "fill with content", "add sample", "add examples",
|
||||
"anadir ejemplos", "contenido de prueba", "test content"
|
||||
],
|
||||
boost: [
|
||||
"imagenes", "images", "fotos", "photos", "stock", "ejemplo", "sample",
|
||||
"demo", "placeholder", "varios", "multiple", "lote", "batch"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
create_module: {
|
||||
keywords: [
|
||||
"crear modulo", "create module", "nuevo modulo", "new module",
|
||||
"disenar modulo", "design module", "hacer modulo", "make module",
|
||||
"componente", "component", "crear componente", "create component",
|
||||
"nuevo componente", "new component", "montar modulo",
|
||||
"build module", "build component", "make component"
|
||||
],
|
||||
boost: [
|
||||
"hero", "slider", "card", "grid", "lista", "list", "banner",
|
||||
"footer", "header", "navbar", "cta", "call to action",
|
||||
"carousel", "accordion", "tabs", "pricing", "features"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
edit_module: {
|
||||
keywords: [
|
||||
"editar modulo", "edit module", "modificar modulo", "modify module",
|
||||
"cambiar modulo", "change module", "actualizar modulo", "update module",
|
||||
"arreglar modulo", "fix module", "mejorar modulo", "improve module",
|
||||
"corregir modulo", "ajustar modulo", "adjust module"
|
||||
],
|
||||
boost: [
|
||||
"css", "html", "javascript", "js", "estilo", "style", "variable",
|
||||
"campo", "field", "diseno", "design", "responsive", "movil", "mobile",
|
||||
"color", "fuente", "font", "espaciado", "spacing",
|
||||
"hero", "slider", "card", "grid", "banner", "footer", "header",
|
||||
"navbar", "cta", "carousel", "accordion", "tabs", "pricing"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
manage_records: {
|
||||
keywords: [
|
||||
"editar registro", "edit record", "actualizar registro", "update record",
|
||||
"borrar registro", "delete record", "buscar registro", "search record",
|
||||
"listar registros", "list records", "modificar registro", "modify record",
|
||||
"ver registros", "view records", "consultar registros", "query records",
|
||||
"cambiar datos", "change data", "eliminar registro", "remove record",
|
||||
// CRUD-oriented English patterns
|
||||
"update data", "delete data", "edit data", "modify data",
|
||||
"update field", "change field", "edit entry", "delete entry",
|
||||
"update price", "change price", "update name", "change name",
|
||||
"remove records", "remove entries", "crud",
|
||||
"insert record", "insert entry", "create record", "add entry",
|
||||
"find record", "find records", "search records", "search data"
|
||||
],
|
||||
boost: [
|
||||
"filtrar", "filter", "where", "campo", "field", "valor", "value",
|
||||
"pagina", "page", "ordenar", "sort", "buscar", "search",
|
||||
"precio", "price", "nombre", "name", "fecha", "date",
|
||||
"estado", "status", "activo", "active"
|
||||
],
|
||||
weight: 8
|
||||
},
|
||||
manage_media: {
|
||||
// Only specific action phrases — generic words like "image/foto" are in boost, not keywords
|
||||
keywords: [
|
||||
"subir imagen", "upload image", "subir foto", "upload photo",
|
||||
"buscar imagen stock", "search stock image", "buscar fotos stock",
|
||||
"generar imagen", "generate image", "generar foto",
|
||||
"reemplazar imagen", "replace image", "cambiar imagen", "change image",
|
||||
"borrar imagen", "delete image", "eliminar imagen", "remove image",
|
||||
"gestionar media", "manage media", "gestionar imagenes", "manage images",
|
||||
"buscar stock", "search stock", "stock photos", "fotos stock",
|
||||
"subir archivo", "upload file"
|
||||
],
|
||||
boost: [
|
||||
"stock", "pixabay", "pexels", "ai", "inteligencia artificial",
|
||||
"resize", "thumbnail", "miniatura", "s3", "assets",
|
||||
"comprimir", "compress", "optimizar", "optimize",
|
||||
// Generic image words are boosts, NOT keywords
|
||||
"imagen", "image", "foto", "photo", "galeria", "gallery", "media"
|
||||
],
|
||||
weight: 5 // Reduced from 8 — media is usually a step, not a workflow
|
||||
},
|
||||
seo_setup: {
|
||||
keywords: [
|
||||
"seo", "meta tags", "meta descripcion", "meta description",
|
||||
"enlace", "slug", "url amigable", "friendly url", "sitemap",
|
||||
"schema markup", "posicionamiento", "ranking",
|
||||
"meta titulo", "meta title", "configurar seo", "setup seo",
|
||||
"set up seo", "configure seo"
|
||||
],
|
||||
boost: [
|
||||
"google", "keywords", "palabras clave", "busqueda", "search",
|
||||
"indexar", "index", "robots", "canonical", "og:image"
|
||||
],
|
||||
weight: 6
|
||||
},
|
||||
explore_site: {
|
||||
keywords: [
|
||||
"explorar", "explore", "que tiene", "what's in", "listar todo",
|
||||
"list all", "mostrar", "show me", "overview", "resumen",
|
||||
"que hay", "que secciones", "what sections", "ver todo",
|
||||
"show everything", "estructura", "structure", "inventario",
|
||||
"mapa del sitio", "site map", "what modules", "que modulos"
|
||||
],
|
||||
boost: [
|
||||
"estructura", "structure", "mapa", "map", "resumen", "summary",
|
||||
"completo", "complete", "todas", "all"
|
||||
],
|
||||
weight: 5
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize text for matching: lowercase, remove accents, strip common articles, trim.
|
||||
*/
|
||||
function normalizeText(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare task text for matching: normalize + strip common filler words (articles, prepositions)
|
||||
* that break keyword matching (e.g., "editar el módulo" should match "editar módulo").
|
||||
*/
|
||||
function prepareTaskForMatching(text) {
|
||||
const normalized = normalizeText(text);
|
||||
// Strip common Spanish/English articles and short prepositions that break adjacent keyword matching
|
||||
return normalized.replace(/\b(el|la|los|las|un|una|unos|unas|del|al|the|a|an)\b/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// ── Contextual adjustment patterns ──────────────────────────────────────────
|
||||
// These use regex word matching to detect intent combinations that substring
|
||||
// matching misses (e.g., "create a new products section" has words separated).
|
||||
|
||||
const CREATION_VERBS = /\b(crear|create|nueva?o?|new|build|make|set up|montar|anadir|add|disenar|design|hacer)\b/;
|
||||
const EDIT_VERBS = /\b(editar|edit|modificar|modify|cambiar|change|actualizar|update|arreglar|fix|mejorar|improve|ajustar|adjust|corregir)\b/;
|
||||
const CRUD_VERBS = /\b(editar|edit|borrar|delete|eliminar|remove|actualizar|update|crear|create|insertar|insert|modificar|modify|buscar|search|listar|list|consultar|query|cambiar|change|find|get|ver|view)\b/;
|
||||
const SECTION_WORDS = /\b(seccion|section|pagina|page|tabla|table|web|sitio|site)\b/;
|
||||
const MODULE_WORDS = /\b(modulo|module|componente|component)\b/;
|
||||
const RECORD_WORDS = /\b(registro|registros|record|records|datos|data|entrada|entradas|entry|entries|contenido|content|precio|price|campo|field)\b/;
|
||||
const MEDIA_ONLY_WORDS = /\b(subir|upload|reemplazar|replace|descargar|download)\b/;
|
||||
const IMAGE_WORDS = /\b(imagen|imagenes|image|images|foto|fotos|photo|photos|galeria|gallery)\b/;
|
||||
// Words that indicate the task is about content/records, not creating a new section
|
||||
const CONTENT_INTENT_WORDS = /\b(contenido|content|rellenar|fill|poblar|populate|registros|records|sample|ejemplo|articulos|articles|entradas|entries|anadir contenido|add content)\b/;
|
||||
// Words that indicate the task is about SEO, not creating a new section
|
||||
const SEO_INTENT_WORDS = /\b(seo|meta tags?|meta descripcion|meta description|meta titulo|meta title|sitemap|slug|posicionamiento|ranking|canonical)\b/;
|
||||
|
||||
/**
|
||||
* Post-scoring contextual adjustments.
|
||||
* Uses regex word matching (not substring) to detect intent patterns the keyword
|
||||
* phase may miss due to non-adjacent words.
|
||||
*/
|
||||
function applyContextAdjustments(scores, normalizedTask) {
|
||||
const hasCreationVerb = CREATION_VERBS.test(normalizedTask);
|
||||
const hasEditVerb = EDIT_VERBS.test(normalizedTask);
|
||||
const hasCrudVerb = CRUD_VERBS.test(normalizedTask);
|
||||
const hasSection = SECTION_WORDS.test(normalizedTask);
|
||||
const hasModule = MODULE_WORDS.test(normalizedTask);
|
||||
const hasRecord = RECORD_WORDS.test(normalizedTask);
|
||||
const hasMediaAction = MEDIA_ONLY_WORDS.test(normalizedTask);
|
||||
const hasImageWord = IMAGE_WORDS.test(normalizedTask);
|
||||
const hasContentIntent = CONTENT_INTENT_WORDS.test(normalizedTask);
|
||||
const hasSeoIntent = SEO_INTENT_WORDS.test(normalizedTask);
|
||||
|
||||
// ── Section creation intent ──
|
||||
// "create" + "section/page/table" = strong signal for create_section
|
||||
// BUT NOT when the real intent is populating content or configuring SEO
|
||||
if (hasCreationVerb && hasSection && !hasContentIntent && !hasSeoIntent) {
|
||||
scores.create_section = scores.create_section || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.create_section.score += 20;
|
||||
}
|
||||
|
||||
// ── Module creation intent ──
|
||||
// "create/new" + "module/component" = strong signal for create_module
|
||||
if (hasCreationVerb && hasModule) {
|
||||
scores.create_module = scores.create_module || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.create_module.score += 20;
|
||||
}
|
||||
|
||||
// ── Module edit intent ──
|
||||
// "edit/modify/change" + "module/component" = strong signal for edit_module
|
||||
if (hasEditVerb && hasModule) {
|
||||
scores.edit_module = scores.edit_module || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.edit_module.score += 20;
|
||||
}
|
||||
|
||||
// ── Decisive create vs edit for modules ──
|
||||
// When both create_module and edit_module have scores, apply decisive differentiation
|
||||
if (hasModule && scores.create_module && scores.edit_module) {
|
||||
if (hasCreationVerb && !hasEditVerb) {
|
||||
// Clearly creation intent → penalize edit
|
||||
scores.edit_module.score = Math.max(0, scores.edit_module.score - 15);
|
||||
} else if (hasEditVerb && !hasCreationVerb) {
|
||||
// Clearly edit intent → penalize create
|
||||
scores.create_module.score = Math.max(0, scores.create_module.score - 15);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Record CRUD intent ──
|
||||
// Any CRUD verb + "record/data/entry" = signal for manage_records
|
||||
if (hasCrudVerb && hasRecord) {
|
||||
scores.manage_records = scores.manage_records || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.manage_records.score += 15;
|
||||
}
|
||||
|
||||
// ── Penalize manage_media when context is clearly about something else ──
|
||||
// If the task mentions section/module/record context, media is a step not the workflow
|
||||
if (scores.manage_media && (hasSection || hasModule || hasRecord)) {
|
||||
// Only keep media score if there's an explicit media action verb ("upload", "replace")
|
||||
if (!hasMediaAction) {
|
||||
scores.manage_media.score = Math.max(0, Math.floor(scores.manage_media.score * 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Boost manage_media only when it's the clear primary intent ──
|
||||
// "upload/replace" + "image/photo" WITHOUT section/module/record context
|
||||
if (hasMediaAction && hasImageWord && !hasSection && !hasModule && !hasRecord) {
|
||||
scores.manage_media = scores.manage_media || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.manage_media.score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the best workflow for a given task description.
|
||||
* Returns the top match with confidence, or suggestions if ambiguous.
|
||||
*
|
||||
* @param {string} task - The user's task description
|
||||
* @returns {{ workflow: string, confidence: number, alternatives: Array }}
|
||||
*/
|
||||
export function detectWorkflow(task) {
|
||||
const normalizedTask = prepareTaskForMatching(task);
|
||||
const scores = {};
|
||||
|
||||
// ── Phase 1: Keyword + boost scoring ──
|
||||
for (const [workflowId, pattern] of Object.entries(WORKFLOW_PATTERNS)) {
|
||||
let score = 0;
|
||||
let keywordHits = 0;
|
||||
let boostHits = 0;
|
||||
|
||||
// Check keyword matches
|
||||
for (const keyword of pattern.keywords) {
|
||||
if (normalizedTask.includes(normalizeText(keyword))) {
|
||||
keywordHits++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check boost matches
|
||||
for (const boost of pattern.boost) {
|
||||
if (normalizedTask.includes(normalizeText(boost))) {
|
||||
boostHits++;
|
||||
}
|
||||
}
|
||||
|
||||
score = (keywordHits * pattern.weight) + (boostHits * 3);
|
||||
scores[workflowId] = { score, keywordHits, boostHits };
|
||||
}
|
||||
|
||||
// ── Phase 2: Contextual adjustments ──
|
||||
// Uses regex word matching to catch intent patterns that substring matching misses
|
||||
applyContextAdjustments(scores, normalizedTask);
|
||||
|
||||
// Sort by score descending
|
||||
const ranked = Object.entries(scores)
|
||||
.filter(([, data]) => data.score > 0)
|
||||
.sort(([, a], [, b]) => b.score - a.score);
|
||||
|
||||
if (ranked.length === 0) {
|
||||
return {
|
||||
workflow: null,
|
||||
confidence: 0,
|
||||
alternatives: []
|
||||
};
|
||||
}
|
||||
|
||||
const [topId, topData] = ranked[0];
|
||||
const maxPossibleScore = WORKFLOW_PATTERNS[topId].keywords.length * WORKFLOW_PATTERNS[topId].weight
|
||||
+ WORKFLOW_PATTERNS[topId].boost.length * 3;
|
||||
const confidence = Math.min(topData.score / Math.max(maxPossibleScore * 0.15, 1), 1);
|
||||
|
||||
// Check if top 2 are close (ambiguous)
|
||||
const alternatives = ranked.slice(1, 3).map(([id, data]) => ({
|
||||
workflow: id,
|
||||
score: data.score,
|
||||
confidence: Math.min(data.score / Math.max(
|
||||
WORKFLOW_PATTERNS[id].keywords.length * WORKFLOW_PATTERNS[id].weight * 0.15, 1
|
||||
), 1)
|
||||
}));
|
||||
|
||||
const isAmbiguous = alternatives.length > 0
|
||||
&& alternatives[0].score > 0
|
||||
&& (topData.score - alternatives[0].score) < (topData.score * 0.2);
|
||||
|
||||
return {
|
||||
workflow: topId,
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
ambiguous: isAmbiguous,
|
||||
alternatives
|
||||
};
|
||||
}
|
||||
|
||||
export { WORKFLOW_PATTERNS };
|
||||
5
mcp-server/tools/orchestrator/index.js
Normal file
5
mcp-server/tools/orchestrator/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { registerOrchestrateTool } from "./orchestrate.js";
|
||||
|
||||
export function registerOrchestratorTools(server) {
|
||||
registerOrchestrateTool(server);
|
||||
}
|
||||
165
mcp-server/tools/orchestrator/orchestrate.js
Normal file
165
mcp-server/tools/orchestrator/orchestrate.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import { z } from "zod";
|
||||
import { detectWorkflow } from "./detector.js";
|
||||
import { getWorkflow, listWorkflows } from "./workflows/index.js";
|
||||
|
||||
/**
|
||||
* Register the orchestrate_task tool on the MCP server.
|
||||
*/
|
||||
export function registerOrchestrateTool(server) {
|
||||
server.tool(
|
||||
"orchestrate_task",
|
||||
"Provides workflow context, domain rules, and step-by-step guidance for Acai CMS tasks. " +
|
||||
"Returns relevant warnings, resource pointers, and suggested tool order. " +
|
||||
"Optional but recommended for multi-step tasks — helps avoid common mistakes. " +
|
||||
"Available workflows: create_section, populate_content, create_module, edit_module, " +
|
||||
"manage_records, manage_media, seo_setup, explore_site.",
|
||||
{
|
||||
task: z.string().describe(
|
||||
"The user's task or request in their own words. " +
|
||||
"Example: 'Crear una sección de productos con categorías e imágenes'"
|
||||
),
|
||||
forceWorkflow: z.string().optional().describe(
|
||||
"Optional: force a specific workflow instead of auto-detecting. " +
|
||||
"Use when auto-detection is wrong or you know exactly which workflow to use. " +
|
||||
"Values: create_section, populate_content, create_module, edit_module, " +
|
||||
"manage_records, manage_media, seo_setup, explore_site"
|
||||
)
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ task, forceWorkflow }) => {
|
||||
try {
|
||||
let workflowId;
|
||||
let confidence;
|
||||
let detectionInfo;
|
||||
|
||||
if (forceWorkflow) {
|
||||
// Forced workflow — skip detection
|
||||
workflowId = forceWorkflow;
|
||||
confidence = 1.0;
|
||||
detectionInfo = { method: "forced", forceWorkflow };
|
||||
} else {
|
||||
// Auto-detect workflow from task description
|
||||
const detection = detectWorkflow(task);
|
||||
|
||||
if (!detection.workflow) {
|
||||
// No workflow matched — return general orientation
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
workflow: "none_detected",
|
||||
message: "Could not determine a specific workflow for this task. " +
|
||||
"You can proceed freely using available tools, or specify a workflow with forceWorkflow.",
|
||||
availableWorkflows: listWorkflows(),
|
||||
generalRules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields are ALWAYS arrays of objects with urlPath property",
|
||||
"Use ONLY Twig FILTERS (pipe syntax), not Twig functions",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)"
|
||||
]
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
if (detection.ambiguous) {
|
||||
// Ambiguous — return top suggestions
|
||||
const topWorkflow = getWorkflow(detection.workflow);
|
||||
const altWorkflows = detection.alternatives
|
||||
.map(a => getWorkflow(a.workflow))
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
workflow: "ambiguous",
|
||||
message: "Multiple workflows could match this task. " +
|
||||
"Pick the most appropriate one using forceWorkflow, or proceed with the top match.",
|
||||
topMatch: {
|
||||
id: topWorkflow.id,
|
||||
name: topWorkflow.name,
|
||||
description: topWorkflow.description,
|
||||
confidence: detection.confidence
|
||||
},
|
||||
alternatives: altWorkflows.map((w, i) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
description: w.description,
|
||||
confidence: detection.alternatives[i].confidence
|
||||
}))
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
workflowId = detection.workflow;
|
||||
confidence = detection.confidence;
|
||||
detectionInfo = {
|
||||
method: "auto",
|
||||
confidence: detection.confidence,
|
||||
alternatives: detection.alternatives
|
||||
};
|
||||
}
|
||||
|
||||
// Load the workflow
|
||||
const workflow = getWorkflow(workflowId);
|
||||
if (!workflow) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: `Unknown workflow: '${workflowId}'`,
|
||||
availableWorkflows: listWorkflows()
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Build the response
|
||||
const response = {
|
||||
success: true,
|
||||
workflow: workflow.id,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
confidence,
|
||||
detection: detectionInfo,
|
||||
totalSteps: workflow.steps.length,
|
||||
steps: workflow.steps,
|
||||
context: workflow.context,
|
||||
rules: workflow.rules,
|
||||
warnings: workflow.warnings,
|
||||
resources: workflow.resources
|
||||
};
|
||||
|
||||
console.error(`[Orchestrator] Detected workflow: ${workflow.id} (confidence: ${confidence}) for task: "${task.substring(0, 80)}..."`);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}]
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Orchestrator] Error:", error);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
85
mcp-server/tools/orchestrator/workflows/createModule.js
Normal file
85
mcp-server/tools/orchestrator/workflows/createModule.js
Normal file
@@ -0,0 +1,85 @@
|
||||
export const createModuleWorkflow = {
|
||||
id: "create_module",
|
||||
name: "Create Module",
|
||||
description: "Design and create an HTML module by writing project files directly, then compile it in the CMS.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Understand the design",
|
||||
description: "Clarify with user: what does the module show? Is it a hero, grid, list, slider, CTA, form?",
|
||||
tool: null,
|
||||
critical: "Get clear requirements before writing code. Ask about: layout, colors, responsive behavior, editable fields."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Review project styling and patterns",
|
||||
description: "Use the saved project styles and nearby modules as reference before writing code.",
|
||||
tool: "save_project_styles",
|
||||
critical: "Align typography, spacing, colors, and component patterns with the existing project."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Create the module files",
|
||||
description: "Write index-base.tpl, style.css, script.js, and optional hook.php directly in the module folder.",
|
||||
tool: "acai-write",
|
||||
critical: "Use project-relative paths. Create complete files. Keep variable names lowercase, descriptive, and stable."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Refine targeted blocks if needed",
|
||||
description: "Use incremental replacements for small fixes instead of rewriting whole files.",
|
||||
tool: "acai-line-replace",
|
||||
critical: "Prefer block edits for existing files to reduce token usage and avoid accidental rewrites."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Compile the module",
|
||||
description: "Compile after editing index-base.tpl so the CMS syncs index.tpl and builder metadata.",
|
||||
tool: "compile_module",
|
||||
critical: "This is mandatory after every index-base.tpl change."
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
action: "Set example data",
|
||||
description: "Set example/static data for module preview. MUST call get_module first to discover the variable schema.",
|
||||
tool: "set_module_example_data",
|
||||
critical: "Call get_module first to get ALL variable names. Then fill EVERY variable with realistic example data. Missing variables = blank preview."
|
||||
},
|
||||
{
|
||||
step: 7,
|
||||
action: "Check module rendering",
|
||||
description: "Test the module with specific variable values to verify it renders correctly.",
|
||||
tool: "check_module",
|
||||
critical: "Test with realistic values. Check for Twig syntax errors, broken images, layout issues."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
builder_vars: "data-field-type attribute on elements creates editable fields. Types: textfield (single line text), headfield (heading), textbox (multiline), wysiwyg (rich HTML), link (URL), upload (single image), uploadBackground (background image), uploadMulti (gallery), list (dropdown options), multiv2 (repeatable block).",
|
||||
component_syntax: "c-if='varname' shows/hides element based on variable. c-for='item in items' loops over array. c-hidden='true' makes element invisible (for config vars). c-else after c-if for alternative content.",
|
||||
module_structure: "Create index-base.tpl, style.css, script.js, and optional hook.php in the module directory. Compile to generate builder.json and the public templates.",
|
||||
css_conventions: "Use TailwindCSS by default. For custom CSS: BEM naming with kebab-case. Root class should match module name. Avoid !important.",
|
||||
upload_in_modules: "Upload fields are arrays. Single image: {{ varname[0].urlPath | imagec(WIDTH) }}. Background: style=\"background-image: url('{{ varname[0].urlPath | imagec(1920) }}')\". Gallery: {% for img in varname %}{{ img.urlPath }}{% endfor %}."
|
||||
},
|
||||
rules: [
|
||||
"Variable names: lowercase, no spaces, no accents, no special characters",
|
||||
"Labels must be UNIQUE — duplicate labels create shared fields",
|
||||
"Upload fields are ALWAYS arrays — access with [0].urlPath",
|
||||
"Use ONLY Twig FILTERS (pipe syntax), not Twig functions",
|
||||
"c-if='varname' for conditional rendering of optional fields",
|
||||
"c-hidden='true' for configuration variables not shown to end user",
|
||||
"data-field-width on upload elements to set image optimization width",
|
||||
"For multiv2 (repeatable): parent element needs data-field-type='multiv2', children are the repeated fields"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT use duplicate labels — they create shared/linked fields",
|
||||
"DO NOT forget to set example data — the module will appear blank in the editor",
|
||||
"DO NOT use Twig functions (range, random, etc.) — only filters work",
|
||||
"DO NOT access upload vars as strings — always use varname[0].urlPath (array)",
|
||||
"DO NOT mix React/Vue syntax — use Twig for templating, vanilla JS for interactivity"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-builder-vars",
|
||||
"acai://resources/guia-atributos-acai",
|
||||
"acai://resources/guia-programacion-acai"
|
||||
]
|
||||
};
|
||||
110
mcp-server/tools/orchestrator/workflows/createSection.js
Normal file
110
mcp-server/tools/orchestrator/workflows/createSection.js
Normal file
@@ -0,0 +1,110 @@
|
||||
export const createSectionWorkflow = {
|
||||
id: "create_section",
|
||||
name: "Create New Section",
|
||||
description: "Full workflow for creating a new website section: table + fields + module + template + content.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Understand requirements",
|
||||
description: "Clarify with user: section name, type (multi/single/category), fields needed, whether it needs URL (enlace), SEO meta tags.",
|
||||
tool: null,
|
||||
critical: "Ask before acting. Multi = multiple records (blog, products). Single = one record (about page). Category = grouping for other sections."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Check existing tables",
|
||||
description: "List current tables to avoid naming conflicts and understand existing structure.",
|
||||
tool: "list_tables",
|
||||
critical: "Table names must be unique. Check if a similar section already exists."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Create the table",
|
||||
description: "Create the database table with correct type and configuration.",
|
||||
tool: "create_table",
|
||||
critical: "type must be: 'multi' (multiple records), 'single' (one record), 'category' (grouping), or 'separador' (menu separator). Set enlace=true if records need their own URL page."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Add fields to the table",
|
||||
description: "Create all necessary fields with correct types and configuration.",
|
||||
tool: "edit_table_field",
|
||||
critical: "Can batch multiple fields in one call. Field types: textfield, textbox, wysiwyg, date, checkbox, list, upload, multitext, codigo, separator."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify table schema",
|
||||
description: "Get the complete schema to confirm all fields were created correctly.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Verify all fields exist with correct types before proceeding to module creation."
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
action: "Design and create the listing module",
|
||||
description: "Create an HTML module that displays a list/grid of records from this section.",
|
||||
tool: "save_module",
|
||||
critical: "Use Twig syntax. Access records with the 'get' filter. Primary key is 'num' not 'id'. Upload fields are ALWAYS arrays: use record.field[0].urlPath | imagec(width)."
|
||||
},
|
||||
{
|
||||
step: 7,
|
||||
action: "Set module example data",
|
||||
description: "Set example/static data for module preview. MUST call get_module first to discover ALL variables.",
|
||||
tool: "set_module_example_data",
|
||||
critical: "Every builder variable must have example data. Missing variables cause blank previews."
|
||||
},
|
||||
{
|
||||
step: 8,
|
||||
action: "Add sample content",
|
||||
description: "Create 2-3 sample records with realistic content and images. If table has enlace=true, include the 'enlace' field with a URL slug.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "Date format: YYYY-MM-DD HH:mm:ss. Checkbox: 1 or 0. Upload fields: use upload_record_image separately. For sections with enlace, creating records first ensures directory structure is ready."
|
||||
},
|
||||
{
|
||||
step: 9,
|
||||
action: "Create detail template (if enlace=true)",
|
||||
description: "If the section has enlace enabled, create the detail page template that shows when navigating to a record's URL.",
|
||||
tool: "save_general_section",
|
||||
critical: "Use 'thisrecord' variable to access the current record. Same Twig rules apply. Note: save_general_section will auto-initialize the directory if needed."
|
||||
},
|
||||
{
|
||||
step: 10,
|
||||
action: "Verify the result",
|
||||
description: "Check module rendering with actual variable values.",
|
||||
tool: "check_module",
|
||||
critical: "Test with actual variable values to ensure no rendering errors."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
twig_filters: "Use 'get' filter for DB queries: {% set items = 'tablename' | get('WHERE active=1', 'ORDER BY num DESC', 10) %}. Use 'imagec' for image resize: {{ path | imagec(400) }}. Use 'module' to include other modules: {{ 'modulename' | module(vars) }}.",
|
||||
field_types: "textfield (single line), textbox (multiline), wysiwyg (rich HTML), date (YYYY-MM-DD), checkbox (0/1), list (dropdown/radio/checkbox), upload (files/images), multitext (key-value pairs), codigo (code editor), separator (visual divider).",
|
||||
list_field_config: "Static options: optionsType='text', optionsText='value1|Label 1\\nvalue2|Label 2'. Table relation: optionsType='table', optionsTablename='tablename', optionsValueField='num', optionsLabelField='name'. SQL: optionsType='query', optionsText='SELECT num,name FROM cms_tablename'.",
|
||||
builder_vars: "data-field-type attribute on HTML elements creates editable fields. Types: textfield, headfield, textbox, wysiwyg, link, upload, uploadBackground, uploadMulti, list, multiv2. Variable names derived from labels (lowercase, no spaces/accents).",
|
||||
upload_rules: "Upload fields ALWAYS return arrays. Single image: {{ record.imagen[0].urlPath | imagec(WIDTH) }}. Gallery loop: {% for img in record.galeria %}{{ img.urlPath }}{% endfor %}. Check existence: {% if record.imagen and record.imagen|length > 0 %}."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields are ALWAYS arrays of objects with urlPath property",
|
||||
"Use ONLY Twig FILTERS (pipe syntax), not Twig functions",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)",
|
||||
"Enlace (URL slug): auto-formatted to /path/ with slashes",
|
||||
"Variable names in modules: lowercase, no spaces, no accents, no special chars",
|
||||
"c-if='varname' for conditional rendering, c-hidden='true' for invisible config vars",
|
||||
"When using 'get' filter: SQL string syntax, NOT objects. Example: 'WHERE num > 5'"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT use record.imagen.urlPath — it's record.imagen[0].urlPath (array!)",
|
||||
"DO NOT use 'id' as primary key — Acai uses 'num'",
|
||||
"DO NOT forget to set example data after creating a module — it will look blank",
|
||||
"DO NOT create a detail template if enlace is false — there's no URL to navigate to",
|
||||
"DO NOT use Twig functions like range() — only filters (pipe syntax) are available",
|
||||
"For best results with new enlace sections, create records BEFORE calling save_general_section to ensure directory structure exists"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-builder-vars",
|
||||
"acai://resources/guia-twig-filters",
|
||||
"acai://resources/guia-atributos-acai",
|
||||
"acai://resources/guia-registros"
|
||||
]
|
||||
};
|
||||
64
mcp-server/tools/orchestrator/workflows/editModule.js
Normal file
64
mcp-server/tools/orchestrator/workflows/editModule.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export const editModuleWorkflow = {
|
||||
id: "edit_module",
|
||||
name: "Edit Module",
|
||||
description: "Modify an existing HTML module: update code, styles, variables, or structure.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get current module code",
|
||||
description: "Read the current HTML, CSS, JS, and PHP of the module.",
|
||||
tool: "get_module",
|
||||
critical: "ALWAYS read the current code before modifying. Understand existing variables, structure, and styling."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Check where it's used",
|
||||
description: "Find all pages and records using this module to understand impact.",
|
||||
tool: "check_module_usage",
|
||||
critical: "Know the blast radius of your changes — how many live pages will be affected."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Make changes",
|
||||
description: "Update the module code with the required modifications.",
|
||||
tool: "save_module",
|
||||
critical: "Pass the module 'id' parameter to update (not create). save_module REPLACES the entire module — include ALL html/css/js, not just the changed parts."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Update example data if needed",
|
||||
description: "If you added or renamed variables, update the example data to match.",
|
||||
tool: "set_module_example_data",
|
||||
critical: "Call get_module first to discover new variable names. Fill ALL variables, including new ones."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify rendering",
|
||||
description: "Test the modified module with variable values to confirm changes work.",
|
||||
tool: "check_module",
|
||||
critical: "Test with realistic values. Compare rendering before and after changes."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
builder_vars: "data-field-type attribute on elements creates editable fields. Types: textfield, headfield, textbox, wysiwyg, link, upload, uploadBackground, uploadMulti, list, multiv2.",
|
||||
component_syntax: "c-if='varname' shows/hides element. c-for='item in items' loops. c-hidden='true' invisible config. c-else after c-if.",
|
||||
save_behavior: "save_module with 'id' parameter = UPDATE. Without 'id' = CREATE new. The tool replaces the ENTIRE module code, not a diff."
|
||||
},
|
||||
rules: [
|
||||
"ALWAYS include the full html/css/js when saving — save_module replaces everything",
|
||||
"Pass the 'id' parameter to update an existing module",
|
||||
"Variable names: lowercase, no spaces, no accents",
|
||||
"Labels must be UNIQUE across the module",
|
||||
"Upload fields are ALWAYS arrays — access with [0].urlPath"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT remove existing variables without checking usage — they may have data on live pages",
|
||||
"DO NOT rename variables — it breaks existing data bindings. Add new ones instead if needed",
|
||||
"DO NOT save partial code (just HTML without CSS) — save_module replaces ALL sections",
|
||||
"DO NOT forget to update example data when adding new variables"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-builder-vars",
|
||||
"acai://resources/guia-atributos-acai"
|
||||
]
|
||||
};
|
||||
48
mcp-server/tools/orchestrator/workflows/exploreSite.js
Normal file
48
mcp-server/tools/orchestrator/workflows/exploreSite.js
Normal file
@@ -0,0 +1,48 @@
|
||||
export const exploreSiteWorkflow = {
|
||||
id: "explore_site",
|
||||
name: "Explore Site",
|
||||
description: "Get an overview of the current Acai site: sections, modules, content.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "List all tables/sections",
|
||||
description: "Get the complete site structure with all sections, their types, and menu order.",
|
||||
tool: "list_tables",
|
||||
critical: "This returns the site's skeleton: all sections with type (multi/single/category/separador), menu name, and order."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Inspect sections of interest",
|
||||
description: "Get the full schema of specific sections to understand their fields and configuration.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Look at field types, required fields, list configurations, and upload fields."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "List all modules",
|
||||
description: "See all available design components/modules.",
|
||||
tool: "list_modules",
|
||||
critical: "Modules are the visual building blocks. Each has HTML, CSS, JS, and builder variables."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Sample content",
|
||||
description: "Preview records in key sections to understand what content exists.",
|
||||
tool: "list_table_records",
|
||||
critical: "Use limit=5 to get a representative sample without overwhelming the response."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
orientation: "list_tables returns all sections with their type: 'multi' (multiple records like blog/products), 'single' (one record like about page), 'category' (grouping for other sections), 'separador' (menu separator). This is the site's architecture.",
|
||||
modules_overview: "list_modules shows all components. Use get_module on specific ones to see their HTML/CSS/JS code and builder variables."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix",
|
||||
"Primary key is 'num', never 'id'"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT modify anything during exploration — this workflow is read-only",
|
||||
"DO NOT assume field names — always check the schema first"
|
||||
],
|
||||
resources: []
|
||||
};
|
||||
44
mcp-server/tools/orchestrator/workflows/index.js
Normal file
44
mcp-server/tools/orchestrator/workflows/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createSectionWorkflow } from "./createSection.js";
|
||||
import { populateContentWorkflow } from "./populateContent.js";
|
||||
import { createModuleWorkflow } from "./createModule.js";
|
||||
import { editModuleWorkflow } from "./editModule.js";
|
||||
import { manageRecordsWorkflow } from "./manageRecords.js";
|
||||
import { manageMediaWorkflow } from "./manageMedia.js";
|
||||
import { seoSetupWorkflow } from "./seoSetup.js";
|
||||
import { exploreSiteWorkflow } from "./exploreSite.js";
|
||||
|
||||
/**
|
||||
* Registry of all available workflows.
|
||||
* Keyed by workflow ID for fast lookup.
|
||||
*/
|
||||
export const WORKFLOWS = {
|
||||
create_section: createSectionWorkflow,
|
||||
populate_content: populateContentWorkflow,
|
||||
create_module: createModuleWorkflow,
|
||||
edit_module: editModuleWorkflow,
|
||||
manage_records: manageRecordsWorkflow,
|
||||
manage_media: manageMediaWorkflow,
|
||||
seo_setup: seoSetupWorkflow,
|
||||
explore_site: exploreSiteWorkflow,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a workflow by ID.
|
||||
* @param {string} id - Workflow identifier
|
||||
* @returns {object|null} The workflow definition or null
|
||||
*/
|
||||
export function getWorkflow(id) {
|
||||
return WORKFLOWS[id] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary list of all available workflows (for help/listing).
|
||||
*/
|
||||
export function listWorkflows() {
|
||||
return Object.values(WORKFLOWS).map((w) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
description: w.description,
|
||||
totalSteps: w.steps.length,
|
||||
}));
|
||||
}
|
||||
53
mcp-server/tools/orchestrator/workflows/manageMedia.js
Normal file
53
mcp-server/tools/orchestrator/workflows/manageMedia.js
Normal file
@@ -0,0 +1,53 @@
|
||||
export const manageMediaWorkflow = {
|
||||
id: "manage_media",
|
||||
name: "Manage Media",
|
||||
description: "Image upload, generation, replacement, and management.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Prepare or generate images",
|
||||
description: "Use an existing image URL/asset or generate an AI image for the content.",
|
||||
tool: "generate_image",
|
||||
critical: "generate_image uses Nano Banana AI. Existing remote image URLs can also be passed directly to upload tools."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Upload to record",
|
||||
description: "Attach images to a record's upload field.",
|
||||
tool: "upload_record_image",
|
||||
critical: "Requires: tableName, recordId, fieldName, imageUrl. The image is downloaded server-side and attached to the record."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "List current uploads",
|
||||
description: "Check what's already uploaded in a field to know if replacing or adding.",
|
||||
tool: "list_record_uploads",
|
||||
critical: "Returns array of upload objects with uploadId needed for replace/delete operations."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Replace or delete if needed",
|
||||
description: "Replace an existing image or delete an upload.",
|
||||
tool: "replace_record_image OR delete_record_upload",
|
||||
critical: "Both require the uploadId from list_record_uploads. replace_record_image downloads new image and swaps it."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
upload_structure: "Upload fields store arrays of objects: [{urlPath, fileName, fileSize, mimeType, uploadDate}]. Access in Twig templates: record.field[0].urlPath | imagec(width).",
|
||||
image_sources: "Use existing remote image URLs, project assets, or Nano Banana AI image generation.",
|
||||
assets_upload: "upload_image_to_assets: uploads to website /images/ folder (not tied to a record). Accepts base64, data URI, or URL. Can resize and compress.",
|
||||
s3_upload: "upload_image_to_s3: uploads to Amazon S3. Returns public S3 URL. Accepts URL, local path, base64, or data URI."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix",
|
||||
"Primary key is 'num', never 'id'",
|
||||
"Upload fields are ALWAYS arrays of objects with urlPath property",
|
||||
"Use imagec filter for resizing: {{ path | imagec(width_in_pixels) }}"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT try to upload before creating the record — the record must exist first",
|
||||
"DO NOT confuse upload_record_image (attaches to record) with upload_image_to_assets (saves to /images/ folder)",
|
||||
"DO NOT delete uploads without confirming — the image will be removed from the live page"
|
||||
],
|
||||
resources: []
|
||||
};
|
||||
64
mcp-server/tools/orchestrator/workflows/manageRecords.js
Normal file
64
mcp-server/tools/orchestrator/workflows/manageRecords.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export const manageRecordsWorkflow = {
|
||||
id: "manage_records",
|
||||
name: "Manage Records",
|
||||
description: "CRUD operations on existing records: query, create, update, and delete data.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get table schema",
|
||||
description: "Understand field names, types, and constraints before querying or modifying.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Know the exact field names and types. Upload fields require special handling."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Query records",
|
||||
description: "List or search records to find the ones to work with.",
|
||||
tool: "list_table_records",
|
||||
critical: "Use 'where' param for SQL WHERE filtering. Use 'limit' for pagination. Use 'page' for page navigation."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Create or update records",
|
||||
description: "Create new records or update existing ones with correct field values.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "Pass 'recordId' for update, omit for create. Only included fields are modified on update. Field values must match field types."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Handle uploads if needed",
|
||||
description: "Upload images or files to record fields.",
|
||||
tool: "upload_record_image",
|
||||
critical: "Separate call per image per field per record. Cannot set upload fields via create_or_update_record."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify changes",
|
||||
description: "Query the records again to confirm changes were applied correctly.",
|
||||
tool: "list_table_records",
|
||||
critical: "Confirm all fields have the expected values, including upload fields."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
querying: "list_table_records supports: where='campo = \"valor\"' (SQL WHERE), page=1 (pagination), limit=20 (records per page). WHERE clause uses SQL string syntax.",
|
||||
updating: "Pass recordId + fields object to update. Only the fields included in the object are modified — other fields are left unchanged.",
|
||||
creating: "Omit recordId to create. Can batch insert by passing fields as an array of objects.",
|
||||
deleting: "delete_table_records requires tableName and recordIds (array of IDs). Use deleteAll=true to delete everything (DANGEROUS)."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields CANNOT be set via create_or_update_record — use upload_record_image",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)",
|
||||
"WHERE clauses use SQL string syntax: where='nombre = \"valor\"'"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT use 'id' to reference records — use 'num'",
|
||||
"DO NOT set upload fields via create_or_update_record — it will not work",
|
||||
"DO NOT delete records without confirming with the user first"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-registros"
|
||||
]
|
||||
};
|
||||
70
mcp-server/tools/orchestrator/workflows/populateContent.js
Normal file
70
mcp-server/tools/orchestrator/workflows/populateContent.js
Normal file
@@ -0,0 +1,70 @@
|
||||
export const populateContentWorkflow = {
|
||||
id: "populate_content",
|
||||
name: "Populate Content",
|
||||
description: "Bulk record creation with images for an existing section.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get table schema",
|
||||
description: "Understand all fields and their types before creating records.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Know the exact field names and types. Upload fields cannot be set via create_or_update_record."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "List existing records",
|
||||
description: "Check what already exists to avoid duplicates.",
|
||||
tool: "list_table_records",
|
||||
critical: "Review existing content before adding new records."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Generate images if needed",
|
||||
description: "Create AI images for the content being created when existing assets are not available.",
|
||||
tool: "generate_image",
|
||||
critical: "Generate the image first and use the returned URL for upload later."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Create records",
|
||||
description: "Create all records with text content. Can batch insert multiple records in one call.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "Batch insert: pass an array of objects in 'fields' parameter. Date format: YYYY-MM-DD HH:mm:ss. Checkbox: 1 or 0."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Upload images to records",
|
||||
description: "Attach images to each record's upload fields.",
|
||||
tool: "upload_record_image",
|
||||
critical: "Must call SEPARATELY for each record+field combination. Cannot batch image uploads. Need the record's num/ID from step 4."
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
action: "Verify records",
|
||||
description: "Confirm all records were created with correct data.",
|
||||
tool: "list_table_records",
|
||||
critical: "Check that all fields are populated correctly including upload fields."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
batch_insert: "create_or_update_record supports batch: pass fields as an array of objects instead of a single object. Each object is one record. Returns an array of created record IDs.",
|
||||
image_sources: "Use existing project/client assets when available, or generate_image for AI-generated images via Nano Banana.",
|
||||
upload_flow: "1. Create record first (get its num/ID). 2. Then call upload_record_image with tableName, recordId, fieldName, imageUrl. 3. The image is downloaded server-side and attached to the record."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields CANNOT be set via create_or_update_record — use upload_record_image",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)",
|
||||
"Enlace field: auto-formatted to /path/ with slashes if not provided"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT try to set upload field values in create_or_update_record — use upload_record_image after creation",
|
||||
"DO NOT forget that batch insert returns an array of created record IDs — you need these for image uploads",
|
||||
"DO NOT upload images before creating the record — the record must exist first"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-registros"
|
||||
]
|
||||
};
|
||||
58
mcp-server/tools/orchestrator/workflows/seoSetup.js
Normal file
58
mcp-server/tools/orchestrator/workflows/seoSetup.js
Normal file
@@ -0,0 +1,58 @@
|
||||
export const seoSetupWorkflow = {
|
||||
id: "seo_setup",
|
||||
name: "SEO Setup",
|
||||
description: "Configure SEO for a section: meta tags, URL slugs, and structured data.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get current table schema",
|
||||
description: "Check if seo_metas is already enabled and if enlace (URL slug) exists.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Look for seo_metas flag and enlace configuration in the schema response."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Enable SEO meta tags",
|
||||
description: "Turn on seo_metas in the table schema to add meta title/description fields.",
|
||||
tool: "update_table_schema",
|
||||
critical: "Set seo_metas=true in the schema. This adds SEO fields to each record."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Enable enlace for URL slugs",
|
||||
description: "Enable enlace so records get their own URL-friendly pages.",
|
||||
tool: "update_table_schema",
|
||||
critical: "Set enlace=true. This auto-generates /section/record-name/ URLs for each record."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Update records with SEO data",
|
||||
description: "Fill in SEO fields for each record: meta title, meta description.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "SEO fields are typically: seo_title, seo_description. Check the schema for exact field names."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Create or update detail template",
|
||||
description: "Ensure the detail page template includes proper meta tags and structured data.",
|
||||
tool: "save_general_section",
|
||||
critical: "The template uses 'thisrecord' variable. Include meta tags in the template for SEO."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
enlace_behavior: "When enlace is enabled, Acai auto-generates URL slugs in /section/record-name/ format. The enlace field value is auto-formatted with slashes.",
|
||||
seo_fields: "Enabling seo_metas adds meta title and description fields to the record editor. These are used in the <head> of the detail page.",
|
||||
detail_template: "The general section template (save_general_section) defines what renders when a user visits a record's URL. Uses 'thisrecord' to access the current record's data."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix",
|
||||
"update_table_schema requires both tableName and the schema object",
|
||||
"Enlace values are auto-formatted to /path/ format",
|
||||
"SEO meta fields are only available after enabling seo_metas on the table"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT enable enlace on a 'single' type table — single tables have only one record and usually don't need individual URLs",
|
||||
"DO NOT forget to create a detail template after enabling enlace — without it, record URLs show blank pages"
|
||||
],
|
||||
resources: []
|
||||
};
|
||||
Reference in New Issue
Block a user