bricks-mcp-server 0.9.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ WP_URL=https://yoursite.tld WP_USER=you WP_APP_PASSWORD="…" npx -y bricks-mcp-
66
66
 
67
67
  ## Tools
68
68
 
69
- `bricks_ping`, `bricks_list_pages`, `bricks_get_page`, `bricks_update_page`, `bricks_create_page`, `bricks_list_templates`, `bricks_create_template`, `bricks_get_global_classes`, `bricks_update_global_classes`, `bricks_get_theme_styles`, `bricks_list_media`, `bricks_reindex_query_filters`.
69
+ `bricks_start_here`, `bricks_ping`, page/element/media tools, `bricks_get_design_context`, `bricks_manage_global_data`, `bricks_apply_global_class_operations`, `bricks_manage_template`, `bricks_get_preview_url`, and Query Filters reindexing.
70
70
 
71
71
  Highlights (server ≥0.5.0, plugin ≥0.7.0):
72
72
 
@@ -77,3 +77,13 @@ Highlights (server ≥0.5.0, plugin ≥0.7.0):
77
77
  ## License
78
78
 
79
79
  GPL-2.0-or-later
80
+
81
+ ### Media safety
82
+
83
+ Use bricks_upload_media for URL/base64 uploads, bricks_find_media_usage before deletion, and bricks_delete_media only after references are cleared. Private URLs, SVG, unsafe MIME types, and files over 10 MB are rejected.
84
+ ### Lote 6 safety
85
+
86
+ - Call `bricks_manage_global_data` with `action: "get"`, then reuse its `hash` in `action: "replace"`.
87
+ - Prefer `bricks_apply_global_class_operations` over full-array replacement.
88
+ - `bricks_manage_template` supports get/update/clone/delete; updates require `expected_modified`.
89
+ - Signed preview URLs expire after 60–900 seconds and must be treated as bearer secrets.
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ export const updatePageSchema = z.object({ id: z.number().int().positive(), expected_modified: z.string().optional(), title: z.string().optional(), status: z.enum(["draft", "publish", "private", "pending"]).optional(), post_content: z.string().optional(), excerpt: z.string().optional(), date: z.string().optional(), featured_media: z.number().int().nonnegative().optional(), terms: z.record(z.any()).optional(), acf: z.record(z.any()).optional(), meta: z.record(z.any()).optional(), content: z.array(z.any()).optional(), header: z.array(z.any()).optional(), footer: z.array(z.any()).optional(), settings: z.record(z.any()).optional() });
3
+ export const uploadMediaSchema = z.object({ url: z.string().url().optional(), base64: z.string().min(1).optional(), filename: z.string().min(1).optional(), title: z.string().optional(), alt: z.string().optional() }).refine(v => Boolean(v.url) !== Boolean(v.base64), "Provide exactly one source: url or base64.");
4
+ export const elementOperationSchema = z.discriminatedUnion("op", [z.object({ op: z.literal("insert"), element: z.record(z.any()), parent: z.union([z.string(), z.number()]).optional(), position: z.number().int().nonnegative().optional() }), z.object({ op: z.literal("update"), id: z.string().min(1), changes: z.record(z.any()) }), z.object({ op: z.literal("move"), id: z.string().min(1), parent: z.union([z.string(), z.number()]), position: z.number().int().nonnegative().optional() }), z.object({ op: z.literal("delete"), id: z.string().min(1) })]);
5
+ export const applyElementOperationsSchema = z.object({ id: z.number().int().positive(), expected_modified: z.string().min(1), field: z.enum(["content", "header", "footer"]).optional(), operations: z.array(elementOperationSchema).min(1) });
6
+ export const designContextSchema = z.object({ include: z.array(z.enum(["classes", "variables", "palettes", "theme_styles", "breakpoints", "components", "global_settings"])).optional(), compact: z.boolean().optional() });
7
+ export const globalScopeSchema = z.enum(["global-variables", "color-palettes", "theme-styles", "components", "breakpoints"]);
8
+ export const globalDataUpdateSchema = z.object({ scope: globalScopeSchema, action: z.enum(["get", "replace"]), expected_hash: z.string().length(64).optional(), value: z.array(z.any()).or(z.record(z.any())).optional() }).superRefine((v, ctx) => { if (v.action === "replace" && (!v.expected_hash || v.value === undefined))
9
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected_hash and value are required for replace" }); });
10
+ export const globalClassOperationSchema = z.discriminatedUnion("op", [z.object({ op: z.literal("add"), class: z.record(z.any()) }), z.object({ op: z.literal("update"), id: z.string().min(1), changes: z.record(z.any()) }), z.object({ op: z.literal("delete"), id: z.string().min(1) })]);
11
+ export const globalClassOperationsSchema = z.object({ expected_hash: z.string().length(64), operations: z.array(globalClassOperationSchema).min(1) });
12
+ export const manageTemplateSchema = z.object({ id: z.number().int().positive(), action: z.enum(["get", "update", "clone", "delete"]), expected_modified: z.string().optional(), title: z.string().optional(), status: z.enum(["draft", "publish", "private"]).optional(), type: z.string().optional(), content: z.array(z.any()).optional(), settings: z.record(z.any()).optional(), conditions: z.array(z.any()).optional(), force: z.boolean().optional() }).superRefine((v, ctx) => { if (v.action === "update" && !v.expected_modified)
13
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected_modified is required for update" }); });
14
+ export const previewUrlSchema = z.object({ id: z.number().int().positive(), ttl: z.number().int().min(60).max(900).optional() });
15
+ export function buildToolRequest(name, args) {
16
+ if (name === "bricks_update_page") {
17
+ const parsed = updatePageSchema.parse(args);
18
+ const { id, ...body } = parsed;
19
+ return { path: `/pages/${id}`, method: "PUT", body };
20
+ }
21
+ if (name === "bricks_apply_element_operations") {
22
+ const parsed = applyElementOperationsSchema.parse(args);
23
+ const { id, ...body } = parsed;
24
+ return { path: `/pages/${id}/element-operations`, method: "POST", body };
25
+ }
26
+ if (name === "bricks_get_element") {
27
+ const id = z.number().int().positive().parse(args.id);
28
+ const elementId = z.string().min(1).parse(args.element_id);
29
+ return { path: `/pages/${id}/elements/${elementId}`, method: "GET", query: args.field ? { field: String(args.field) } : undefined };
30
+ }
31
+ if (name === "bricks_get_design_context") {
32
+ const parsed = designContextSchema.parse(args);
33
+ return { path: "/design-context", method: "GET", query: { ...(parsed.include ? { include: parsed.include.join(",") } : {}), ...(parsed.compact ? { compact: "true" } : {}) } };
34
+ }
35
+ if (name === "bricks_manage_global_data") {
36
+ const parsed = globalDataUpdateSchema.parse(args);
37
+ const { scope, action, ...body } = parsed;
38
+ return action === "get" ? { path: `/global-data/${scope}`, method: "GET" } : { path: `/global-data/${scope}`, method: "PUT", body };
39
+ }
40
+ if (name === "bricks_apply_global_class_operations")
41
+ return { path: "/global-classes/operations", method: "POST", body: globalClassOperationsSchema.parse(args) };
42
+ if (name === "bricks_manage_template") {
43
+ const parsed = manageTemplateSchema.parse(args);
44
+ const { id, ...body } = parsed;
45
+ return { path: `/templates/${id}/manage`, method: "POST", body };
46
+ }
47
+ if (name === "bricks_get_preview_url") {
48
+ const parsed = previewUrlSchema.parse(args);
49
+ return { path: `/preview/${parsed.id}`, method: "GET", query: parsed.ttl ? { ttl: String(parsed.ttl) } : undefined };
50
+ }
51
+ if (name === "bricks_upload_media")
52
+ return { path: "/media", method: "POST", body: uploadMediaSchema.parse(args) };
53
+ if (name === "bricks_find_media_usage")
54
+ return { path: `/media/${z.number().int().positive().parse(args.id)}/usage`, method: "GET" };
55
+ if (name === "bricks_delete_media")
56
+ return { path: `/media/${z.number().int().positive().parse(args.id)}`, method: "DELETE", query: args.force ? { force: "true" } : undefined };
57
+ throw new Error(`Unsupported tool request: ${name}`);
58
+ }
59
+ export function negotiateCapabilities(startHere) { const c = startHere.capabilities ?? {}; return { canWrite: !c.dry_run && !c.allowlist?.read_only, canUploadMedia: c.media_upload !== false, canForceDelete: c.force_delete_allowed === true, acf: c.acf_active === true, queryFilters: c.query_filters === true }; }
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ if (subcommand === "doctor" || subcommand === "diagnose") {
33
33
  if (process.env.WP_URL) {
34
34
  console.error(`[bricks-mcp] target WP_URL=${process.env.WP_URL} WP_USER=${process.env.WP_USER ?? "?"}`);
35
35
  }
36
- const server = new Server({ name: "bricks-mcp-server", version: "0.9.0" }, { capabilities: { tools: {} } });
36
+ const server = new Server({ name: "bricks-mcp-server", version: "0.13.0" }, { capabilities: { tools: {} } });
37
37
  const tools = [
38
38
  {
39
39
  name: "bricks_start_here",
@@ -296,6 +296,45 @@ const tools = [
296
296
  handler: async (args) => wpRequest("/media", { query: args }),
297
297
  },
298
298
  {
299
+ name: "bricks_upload_media",
300
+ description: "Upload media from one public URL or base64. Blocks SSRF, unsafe MIME, files over 10 MB, and SVG.",
301
+ inputSchema: { type: "object", properties: { url: { type: "string" }, base64: { type: "string" }, filename: { type: "string" }, title: { type: "string" }, alt: { type: "string" } }, additionalProperties: false },
302
+ schema: z.object({ url: z.string().url().optional(), base64: z.string().min(1).optional(), filename: z.string().min(1).optional(), title: z.string().optional(), alt: z.string().optional() }).refine(v => Boolean(v.url) !== Boolean(v.base64), "Provide exactly one source."),
303
+ handler: async (args) => wpRequest("/media", { method: "POST", body: args }),
304
+ },
305
+ {
306
+ name: "bricks_find_media_usage",
307
+ description: "Find posts and Bricks data that reference an attachment. Call before deletion.",
308
+ inputSchema: { type: "object", properties: { id: { type: "number" } }, required: ["id"], additionalProperties: false },
309
+ schema: z.object({ id: z.number().int().positive() }),
310
+ handler: async (args) => wpRequest(`/media/${args.id}/usage`),
311
+ },
312
+ {
313
+ name: "bricks_delete_media",
314
+ description: "Delete an unused attachment. Trash by default; force:true is permanent and default-off.",
315
+ inputSchema: { type: "object", properties: { id: { type: "number" }, force: { type: "boolean" } }, required: ["id"], additionalProperties: false },
316
+ schema: z.object({ id: z.number().int().positive(), force: z.boolean().optional() }),
317
+ handler: async (args) => wpRequest(`/media/${args.id}`, { method: "DELETE", query: args.force ? { force: "true" } : undefined }),
318
+ }, {
319
+ name: "bricks_get_element",
320
+ description: "Read one Bricks element and its complete descendant subtree without loading the full page tree.",
321
+ inputSchema: { type: "object", properties: { id: { type: "number" }, element_id: { type: "string" }, field: { type: "string", description: "content (default), header, or footer" } }, required: ["id", "element_id"], additionalProperties: false },
322
+ schema: z.object({ id: z.number().int().positive(), element_id: z.string().min(1), field: z.enum(["content", "header", "footer"]).optional() }),
323
+ handler: async (args) => wpRequest(`/pages/${args.id}/elements/${args.element_id}`, { query: args.field ? { field: args.field } : undefined }),
324
+ },
325
+ {
326
+ name: "bricks_apply_element_operations",
327
+ description: "Atomically apply insert/update/move/delete operations to one Bricks element tree. Requires expected_modified. The whole batch validates before one snapshot and one write; dry-run returns the diff without persistence.",
328
+ inputSchema: { type: "object", properties: { id: { type: "number" }, expected_modified: { type: "string" }, field: { type: "string" }, operations: { type: "array", items: { type: "object" } } }, required: ["id", "expected_modified", "operations"], additionalProperties: false },
329
+ schema: z.object({ id: z.number().int().positive(), expected_modified: z.string().min(1), field: z.enum(["content", "header", "footer"]).optional(), operations: z.array(z.object({ op: z.enum(["insert", "update", "move", "delete"]) }).passthrough()).min(1) }),
330
+ handler: async (args) => { const { id, ...body } = args; return wpRequest(`/pages/${id}/element-operations`, { method: "POST", body }); },
331
+ }, {
332
+ name: "bricks_get_design_context",
333
+ description: "Read selected Bricks design-system sections: classes, variables, palettes, theme styles, breakpoints, components, and global settings. Use compact:true for counts/names only and include to control token usage.",
334
+ inputSchema: { type: "object", properties: { include: { type: "array", items: { type: "string" } }, compact: { type: "boolean" } }, additionalProperties: false },
335
+ schema: z.object({ include: z.array(z.enum(["classes", "variables", "palettes", "theme_styles", "breakpoints", "components", "global_settings"])).optional(), compact: z.boolean().optional() }),
336
+ handler: async (args) => wpRequest("/design-context", { query: { include: args.include?.join(","), compact: args.compact ? "true" : undefined } }),
337
+ }, {
299
338
  name: "bricks_reindex_query_filters",
300
339
  description: "Regenerate the Bricks Query Filters index. Call this after writing/editing any filter-* element via MCP — filters don't render until the index is rebuilt.",
301
340
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
@@ -334,6 +373,33 @@ const tools = [
334
373
  handler: async (args) => wpRequest("/templates", { method: "POST", body: args }),
335
374
  },
336
375
  {
376
+ name: "bricks_manage_global_data",
377
+ description: "Read or safely replace one versioned Bricks global-data scope. Call action:get first for the current value/hash, then action:replace with expected_hash; dry-run and snapshot policy apply.",
378
+ inputSchema: { type: "object", properties: { scope: { type: "string", enum: ["global-variables", "color-palettes", "theme-styles", "components", "breakpoints"] }, action: { type: "string", enum: ["get", "replace"] }, expected_hash: { type: "string" }, value: { type: ["array", "object"] } }, required: ["scope", "action"], additionalProperties: false },
379
+ schema: z.object({ scope: z.enum(["global-variables", "color-palettes", "theme-styles", "components", "breakpoints"]), action: z.enum(["get", "replace"]), expected_hash: z.string().length(64).optional(), value: z.union([z.array(z.any()), z.record(z.any())]).optional() }).refine(v => v.action !== "replace" || (Boolean(v.expected_hash) && v.value !== undefined), "expected_hash and value are required for replace"),
380
+ handler: async (args) => { const { scope, action, ...body } = args; return action === "get" ? wpRequest(`/global-data/${scope}`) : wpRequest(`/global-data/${scope}`, { method: "PUT", body }); },
381
+ },
382
+ {
383
+ name: "bricks_apply_global_class_operations",
384
+ description: "Atomically add, update, or delete individual global classes without replacing the entire class library. Requires the hash from bricks_get_global_classes and rejects duplicate names or IDs.",
385
+ inputSchema: { type: "object", properties: { expected_hash: { type: "string" }, operations: { type: "array", items: { type: "object" } } }, required: ["expected_hash", "operations"], additionalProperties: false },
386
+ schema: z.object({ expected_hash: z.string().length(64), operations: z.array(z.object({ op: z.enum(["add", "update", "delete"]) }).passthrough()).min(1) }),
387
+ handler: async (args) => wpRequest("/global-classes/operations", { method: "POST", body: args }),
388
+ },
389
+ {
390
+ name: "bricks_manage_template",
391
+ description: "Long-tail template dispatcher: get full template data, update atomically with expected_modified, clone, or trash/delete. Updates create a Bricks-compatible WordPress revision and snapshots.",
392
+ inputSchema: { type: "object", properties: { id: { type: "number" }, action: { type: "string", enum: ["get", "update", "clone", "delete"] }, expected_modified: { type: "string" }, title: { type: "string" }, status: { type: "string" }, type: { type: "string" }, content: { type: "array" }, settings: { type: "object" }, conditions: { type: "array" }, force: { type: "boolean" } }, required: ["id", "action"], additionalProperties: false },
393
+ schema: z.object({ id: z.number().int().positive(), action: z.enum(["get", "update", "clone", "delete"]), expected_modified: z.string().optional(), title: z.string().optional(), status: z.enum(["draft", "publish", "private"]).optional(), type: z.string().optional(), content: z.array(z.any()).optional(), settings: z.record(z.any()).optional(), conditions: z.array(z.any()).optional(), force: z.boolean().optional() }).refine(v => v.action !== "update" || Boolean(v.expected_modified), "expected_modified is required for update"),
394
+ handler: async (args) => { const { id, action, ...body } = args; return action === "get" ? wpRequest(`/templates/${id}`) : wpRequest(`/templates/${id}/manage`, { method: "POST", body: { action, ...body } }); },
395
+ },
396
+ {
397
+ name: "bricks_get_preview_url",
398
+ description: "Generate a short-lived signed bearer URL (60–900 seconds) for visually checking a draft, private page, or template in a browser. Treat the URL as a secret until it expires.",
399
+ inputSchema: { type: "object", properties: { id: { type: "number" }, ttl: { type: "number", minimum: 60, maximum: 900 } }, required: ["id"], additionalProperties: false },
400
+ schema: z.object({ id: z.number().int().positive(), ttl: z.number().int().min(60).max(900).optional() }),
401
+ handler: async (args) => wpRequest(`/preview/${args.id}`, { query: args.ttl ? { ttl: String(args.ttl) } : undefined }),
402
+ }, {
337
403
  name: "bricks_get_global_classes",
338
404
  description: "Read Bricks global classes and their categories — useful before editing pages so styling stays consistent.",
339
405
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
@@ -376,9 +442,19 @@ const TOOL_ANNOTATIONS = {
376
442
  bricks_list_pages: { readOnlyHint: true },
377
443
  bricks_get_page: { readOnlyHint: true },
378
444
  bricks_list_templates: { readOnlyHint: true },
445
+ bricks_manage_global_data: { readOnlyHint: false, destructiveHint: true },
446
+ bricks_apply_global_class_operations: { readOnlyHint: false, destructiveHint: true },
447
+ bricks_manage_template: { readOnlyHint: false, destructiveHint: true },
448
+ bricks_get_preview_url: { readOnlyHint: true },
379
449
  bricks_get_global_classes: { readOnlyHint: true },
380
450
  bricks_get_theme_styles: { readOnlyHint: true },
451
+ bricks_get_design_context: { readOnlyHint: true },
452
+ bricks_get_element: { readOnlyHint: true },
453
+ bricks_apply_element_operations: { readOnlyHint: false, destructiveHint: true },
381
454
  bricks_list_media: { readOnlyHint: true },
455
+ bricks_find_media_usage: { readOnlyHint: true },
456
+ bricks_upload_media: { readOnlyHint: false, destructiveHint: false },
457
+ bricks_delete_media: { readOnlyHint: false, destructiveHint: true },
382
458
  bricks_create_page: { readOnlyHint: false, destructiveHint: false },
383
459
  bricks_create_template: { readOnlyHint: false, destructiveHint: false },
384
460
  bricks_reindex_query_filters: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
package/package.json CHANGED
@@ -1,45 +1,47 @@
1
- {
2
- "name": "bricks-mcp-server",
3
- "version": "0.9.0",
4
- "description": "Provider-agnostic MCP server that exposes Bricks Builder pages, templates, global classes and theme styles as tools. Works with any MCP-compatible client (Claude Code, Codex CLI, etc.).",
5
- "type": "module",
6
- "bin": {
7
- "bricks-mcp-server": "dist/index.js",
8
- "bricks-mcp": "dist/index.js"
9
- },
10
- "files": [
11
- "dist",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "build": "tsc",
16
- "start": "node dist/index.js",
17
- "dev": "tsc --watch",
18
- "setup": "node dist/index.js setup",
19
- "doctor": "node dist/index.js doctor",
20
- "prepublishOnly": "npm run build"
21
- },
22
- "keywords": [
23
- "mcp",
24
- "model-context-protocol",
25
- "bricks",
26
- "bricks-builder",
27
- "wordpress",
28
- "claude",
29
- "claude-code",
30
- "ai"
31
- ],
32
- "author": "Juan Leonardo",
33
- "license": "GPL-2.0-or-later",
34
- "engines": {
35
- "node": ">=18"
36
- },
37
- "dependencies": {
38
- "@modelcontextprotocol/sdk": "^1.0.4",
39
- "zod": "^3.23.8"
40
- },
41
- "devDependencies": {
42
- "@types/node": "^20.12.0",
43
- "typescript": "^5.5.0"
44
- }
45
- }
1
+ {
2
+ "name": "bricks-mcp-server",
3
+ "version": "0.13.0",
4
+ "description": "Provider-agnostic MCP server that exposes Bricks Builder pages, templates, global classes and theme styles as tools. Works with any MCP-compatible client (Claude Code, Codex CLI, etc.).",
5
+ "type": "module",
6
+ "bin": {
7
+ "bricks-mcp-server": "dist/index.js",
8
+ "bricks-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsc --watch",
18
+ "setup": "node dist/index.js setup",
19
+ "doctor": "node dist/index.js doctor",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "npm run build && node --test test/*.test.mjs"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "bricks",
27
+ "bricks-builder",
28
+ "wordpress",
29
+ "claude",
30
+ "claude-code",
31
+ "ai"
32
+ ],
33
+ "author": "Juan Leonardo",
34
+ "license": "GPL-2.0-or-later",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.4",
40
+ "bricks-ai-workspace": "file:..",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.12.0",
45
+ "typescript": "^5.5.0"
46
+ }
47
+ }