From 362666295fd58a30191a201e9353c295a2a7695b Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Tue, 21 Apr 2026 09:09:14 +0000 Subject: [PATCH] Header y footer v1 --- mcp-server/tools/index.js | 2 + mcp-server/tools/layout/get_layout_field.js | 56 +++++++++++++++++++ mcp-server/tools/layout/index.js | 15 ++++++ mcp-server/tools/layout/set_layout_field.js | 60 +++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 mcp-server/tools/layout/get_layout_field.js create mode 100644 mcp-server/tools/layout/index.js create mode 100644 mcp-server/tools/layout/set_layout_field.js diff --git a/mcp-server/tools/index.js b/mcp-server/tools/index.js index 8993960..a9d5316 100644 --- a/mcp-server/tools/index.js +++ b/mcp-server/tools/index.js @@ -9,6 +9,7 @@ import { registerProjectTools } from './project/index.js'; import { registerFileTools } from './files/index.js'; import { registerHookTools } from './hooks/index.js'; import { registerLibrariesTools } from './libraries/index.js'; +import { registerLayoutTools } from './layout/index.js'; /** * Register all tools on the MCP server @@ -25,4 +26,5 @@ export function registerTools(server) { registerFileTools(server); registerHookTools(server); registerLibrariesTools(server); + registerLayoutTools(server); } diff --git a/mcp-server/tools/layout/get_layout_field.js b/mcp-server/tools/layout/get_layout_field.js new file mode 100644 index 0000000..6e94793 --- /dev/null +++ b/mcp-server/tools/layout/get_layout_field.js @@ -0,0 +1,56 @@ +import { z } from "zod"; +import { withAuth } from "../../auth/index.js"; +import { withAuthParams } from "../helpers/authSchema.js"; +import { handleToolError } from "../helpers/errorHandler.js"; +import { pythonGet } from "../helpers/pythonServerClient.js"; +import { getCurrentProjectInfo } from "../files/helpers.js"; + +// Tool: get_layout_field +// Reads a global layout field (style or javascript) from the project's layout.json +// via the Python endpoint. Read-only — delegates to the backend which parses +// layout.json and returns the raw content string plus layoutExists flag. + +export function registerGetLayoutFieldTool(server) { + server.tool( + "get_layout_field", + `Get the content of a global layout field of the project. Supported: 'style' (global CSS injected in all pages), 'javascript' (global JS), 'header' (HTML/Twig source of the site header), 'footer' (HTML/Twig source of the site footer). Use this before set_layout_field to see the current content.`, + withAuthParams({ + field: z.enum(["style", "javascript", "header", "footer"]).describe("Which layout field: 'style', 'javascript', 'header' or 'footer'"), + }), + { readOnlyHint: true, destructiveHint: false }, + withAuth(async ({ field }, _extra) => { + try { + const { projectSlug } = getCurrentProjectInfo(); + const result = await pythonGet("/api/project/layout-field", { + project: projectSlug, + field, + }); + if (!result?.success) { + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: false, + error: result?.error || "Could not read layout field", + }), + }], + isError: true, + }; + } + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + field: result.field || field, + content: result.content || "", + layoutExists: !!result.layoutExists, + }, null, 2), + }], + }; + } catch (error) { + return handleToolError(error, "get_layout_field", { field }); + } + }) + ); +} diff --git a/mcp-server/tools/layout/index.js b/mcp-server/tools/layout/index.js new file mode 100644 index 0000000..9ca53c0 --- /dev/null +++ b/mcp-server/tools/layout/index.js @@ -0,0 +1,15 @@ +import { canEditCode } from "../helpers/roleCheck.js"; +import { registerGetLayoutFieldTool } from "./get_layout_field.js"; +import { registerSetLayoutFieldTool } from "./set_layout_field.js"; + +/** + * Tools to read/write global layout fields (style, javascript) of the project. + * The read tool is always exposed (read only); the mutating tool is gated by + * canEditCode() — same pattern used by libraries/ and hooks/. + */ +export function registerLayoutTools(server) { + registerGetLayoutFieldTool(server); // read-only, todos + if (canEditCode()) { + registerSetLayoutFieldTool(server); + } +} diff --git a/mcp-server/tools/layout/set_layout_field.js b/mcp-server/tools/layout/set_layout_field.js new file mode 100644 index 0000000..9efd060 --- /dev/null +++ b/mcp-server/tools/layout/set_layout_field.js @@ -0,0 +1,60 @@ +import { z } from "zod"; +import { withAuth } from "../../auth/index.js"; +import { withAuthParams } from "../helpers/authSchema.js"; +import { handleToolError } from "../helpers/errorHandler.js"; +import { pythonPost } from "../helpers/pythonServerClient.js"; +import { getCurrentProjectInfo } from "../files/helpers.js"; + +// Tool: set_layout_field +// Replaces the content of a global layout field (style or javascript) in the +// project's layout.json. Destructive — overwrites the existing content. The +// backend enforces a 500KB size limit and returns 400 if exceeded; that error +// is propagated via handleToolError. + +export function registerSetLayoutFieldTool(server) { + server.tool( + "set_layout_field", + `Replace the content of a global layout field. 'style'/'javascript' are simple string fields injected via CDN-like URLs (no regeneration needed). 'header'/'footer' are more complex: saving them triggers a server-side pipeline that regenerates the compiled PHP, Twig module files, and TWIG-compiled templates — changes are visible immediately. Destructive: overwrites existing content. Prefer reading with get_layout_field first.`, + withAuthParams({ + field: z.enum(["style", "javascript", "header", "footer"]).describe("Which layout field: 'style', 'javascript', 'header' or 'footer'"), + content: z.string().describe("Full replacement content. Max 500KB."), + }), + { readOnlyHint: false, destructiveHint: true }, + withAuth(async ({ field, content }, _extra) => { + try { + const { projectSlug } = getCurrentProjectInfo(); + const result = await pythonPost("/api/project/layout-field/save", { + project: projectSlug, + field, + content, + }); + if (!result?.success) { + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: false, + error: result?.error || "Could not save layout field", + }), + }], + isError: true, + }; + } + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + field: result.field || field, + }, null, 2), + }], + }; + } catch (error) { + return handleToolError(error, "set_layout_field", { + field, + contentLength: typeof content === "string" ? content.length : 0, + }); + } + }) + ); +}