bricks-mcp-server 0.14.0 → 0.15.0-beta.2
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 +97 -85
- package/dist/contracts.js +38 -4
- package/dist/index.js +58 -24
- package/dist/profiles.js +40 -0
- package/dist/wp-client.js +58 -13
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,89 +1,101 @@
|
|
|
1
|
-
# bricks-mcp-server
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
# bricks-mcp-server
|
|
2
|
+
|
|
3
|
+
> Local beta 0.15.0-beta.2 requires Bridge 0.18.0-beta.2 (`guarded-v2`) for writes.
|
|
4
|
+
> New in beta.2: `bricks_update_media`, `bricks_get_page` with `fields`/`subtree`/`compact`,
|
|
5
|
+
> `op: duplicate`, `verbose` on `bricks_start_here`/`bricks_ping`, and `server_upgrade_required`
|
|
6
|
+
> when the bridge announces a newer `min_server_version`.
|
|
7
|
+
> This version has not been published. Run the compiled `dist/index.js` to test it.
|
|
8
|
+
> `BRICKS_PROFILES_FILE` supports explicit per-call `site_id` profiles with
|
|
9
|
+
> `{url,user,password_env}` entries. Credentials stay in environment variables.
|
|
10
|
+
> Use `bricks_list_sites` to discover profiles; no shared active-site state exists.
|
|
11
|
+
> Element property patches need `expected_exists`/`expected` and a stable
|
|
12
|
+
> `operation_id`. Structural edits additionally require `expected_modified` and
|
|
13
|
+
> `expected_hash`. Read back every change. Full replacements require explicit permission.
|
|
14
|
+
|
|
15
|
+
Provider-agnostic **MCP server** that exposes [Bricks Builder](https://bricksbuilder.io/) data — pages, templates, global classes and theme styles — as tools for any MCP-compatible AI client (Claude Code, Codex CLI, etc.).
|
|
16
|
+
|
|
17
|
+
It talks to a companion WordPress plugin (**Bricks MCP Bridge**) over the REST API, authenticated with a WordPress Application Password. No AI API key ever lives in WordPress; all model calls happen in your client.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
No clone or build needed — `npx` runs it on demand. The fastest path is the guided wizard:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx -y bricks-mcp-server setup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It asks for your site URL, opens the Application Password authorization flow, tests the connection, and writes your client config.
|
|
28
|
+
|
|
29
|
+
### Manual registration (Claude Code)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add bricks -s user \
|
|
33
|
+
-e WP_URL=https://yoursite.tld \
|
|
34
|
+
-e WP_USER=your-wp-username \
|
|
35
|
+
-e WP_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \
|
|
36
|
+
-- npx -y bricks-mcp-server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or in `~/.claude.json` under `mcpServers`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"bricks": {
|
|
45
|
+
"command": "npx",
|
|
46
|
+
"args": ["-y", "bricks-mcp-server"],
|
|
47
|
+
"env": {
|
|
48
|
+
"WP_URL": "https://yoursite.tld",
|
|
49
|
+
"WP_USER": "your-wp-username",
|
|
50
|
+
"WP_APP_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Node.js ≥ 18
|
|
60
|
+
- A WordPress site running **Bricks Builder** with the **Bricks MCP Bridge** plugin installed and active.
|
|
61
|
+
- A WordPress Application Password for a user with the `edit_pages` capability.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
| Command | Purpose |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `npx -y bricks-mcp-server` | Run the MCP stdio server (what your client invokes). |
|
|
68
|
+
| `npx -y bricks-mcp-server setup` | Interactive setup wizard. |
|
|
69
|
+
| `npx -y bricks-mcp-server doctor` | Diagnose connection problems with specific fixes. |
|
|
70
|
+
|
|
71
|
+
## Diagnostics
|
|
72
|
+
|
|
73
|
+
If the connection fails, `doctor` pinpoints the exact cause — unreachable site, stripped `Authorization` header, bad credentials, missing capability, or plugin not installed:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
WP_URL=https://yoursite.tld WP_USER=you WP_APP_PASSWORD="…" npx -y bricks-mcp-server doctor
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Tools
|
|
80
|
+
|
|
81
|
+
`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.
|
|
82
|
+
|
|
83
|
+
Highlights (server ≥0.5.0, plugin ≥0.7.0):
|
|
84
|
+
|
|
85
|
+
- **Optimistic locking** — `bricks_get_page` returns `modified`; pass it as `expected_modified` on `bricks_update_page` to get a 409 instead of overwriting edits made in the builder since you read the page.
|
|
86
|
+
- **`bricks_create_page`** — create pages/posts/CPT entries (draft by default), optionally with Bricks content.
|
|
87
|
+
- **`bricks_ping`** echoes the effective `WP_URL`/user and warns when the configured host doesn't match the site that answered (stale config, redirect, proxy).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
GPL-2.0-or-later
|
|
80
92
|
|
|
81
93
|
### Media safety
|
|
82
94
|
|
|
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`.
|
|
95
|
+
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.
|
|
96
|
+
### Lote 6 safety
|
|
97
|
+
|
|
98
|
+
- Call `bricks_manage_global_data` with `action: "get"`, then reuse its `hash` in `action: "replace"`.
|
|
99
|
+
- Prefer `bricks_apply_global_class_operations` over full-array replacement.
|
|
100
|
+
- `bricks_manage_template` supports get/update/clone/delete; updates require `expected_modified`.
|
|
89
101
|
- Signed preview URLs expire after 60–900 seconds and must be treated as bearer secrets.
|
package/dist/contracts.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export const
|
|
2
|
+
export const SERVER_VERSION = "0.15.0-beta.2";
|
|
3
|
+
export const updatePageSchema = z.object({ id: z.number().int().positive(), expected_modified: z.string().min(1), expected_version: z.string().length(64), replace_document: z.boolean().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
4
|
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
|
|
5
|
-
export const
|
|
5
|
+
export const getPageSchema = z.object({ id: z.number().int().positive(), fields: z.array(z.enum(["content", "header", "footer", "settings"])).min(1).optional(), subtree: z.string().min(1).optional(), compact: z.boolean().optional() });
|
|
6
|
+
export const updateMediaSchema = z.object({ id: z.number().int().positive(), alt: z.string().max(1000).optional(), title: z.string().max(1000).optional(), caption: z.string().max(5000).optional(), description: z.string().max(20000).optional() }).refine(v => [v.alt, v.title, v.caption, v.description].some(x => x !== undefined), "Provide at least one of: alt, title, caption, description.");
|
|
7
|
+
export const verboseSchema = z.object({ verbose: z.boolean().optional() });
|
|
8
|
+
export const elementPositionSchema = z.union([z.number().int().nonnegative(), z.enum(["first", "start", "top", "prepend", "last", "end", "bottom", "append", "before", "after"])]);
|
|
9
|
+
export const propertyPatchSchema = z.object({ path: z.string().regex(/^\/(settings\/[^/].*|label)$/), expected_exists: z.boolean(), expected: z.unknown().optional(), value: z.unknown().optional(), remove: z.boolean().optional() }).strict().superRefine((v, ctx) => { if (v.expected_exists && !Object.hasOwn(v, "expected"))
|
|
10
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected is required" }); if (!v.remove && !Object.hasOwn(v, "value"))
|
|
11
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "value or remove:true is required" }); });
|
|
12
|
+
export const elementOperationSchema = z.discriminatedUnion("op", [z.object({ op: z.literal("patch"), id: z.string().min(1), patches: z.array(propertyPatchSchema).min(1).max(100) }), z.object({ op: z.literal("insert"), element: z.record(z.any()), parent: z.union([z.string(), z.number()]).optional(), position: elementPositionSchema.optional(), target: z.string().min(1).optional() }), z.object({ op: z.literal("update"), id: z.string().min(1), changes: z.record(z.any()), mode: z.enum(["merge", "deep", "replace"]).optional() }), z.object({ op: z.literal("move"), id: z.string().min(1), parent: z.union([z.string(), z.number()]).optional(), position: elementPositionSchema.optional(), target: z.string().min(1).optional() }), z.object({ op: z.literal("delete"), id: z.string().min(1) }), z.object({ op: z.literal("duplicate"), id: z.string().min(1), parent: z.union([z.string(), z.number()]).optional(), position: elementPositionSchema.optional(), target: z.string().min(1).optional() })]).superRefine((v, ctx) => { if ((v.op === "insert" || v.op === "move" || v.op === "duplicate") && (v.position === "before" || v.position === "after") && !v.target)
|
|
13
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `position "${v.position}" requires a sibling id in "target"` }); });
|
|
14
|
+
export const applyElementOperationsSchema = z.object({ id: z.number().int().positive(), expected_modified: z.string().min(1).optional(), expected_hash: z.string().length(64).optional(), operation_id: z.string().regex(/^[a-zA-Z0-9_-]{16,80}$/).optional(), dry_run: z.boolean().optional(), field: z.enum(["content", "header", "footer"]).optional(), operations: z.array(elementOperationSchema).min(1).max(100) }).superRefine((v, ctx) => { if (!v.dry_run && !v.operation_id)
|
|
15
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "operation_id is required" }); if (v.operations.some(o => o.op !== "patch") && (!v.expected_modified || !v.expected_hash))
|
|
16
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected_modified and expected_hash are required for structural/merge operations" }); });
|
|
6
17
|
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
18
|
export const globalScopeSchema = z.enum(["global-variables", "color-palettes", "theme-styles", "components", "breakpoints"]);
|
|
8
19
|
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))
|
|
@@ -68,6 +79,20 @@ export function buildToolRequest(name, args) {
|
|
|
68
79
|
const query = { scope, ...(post_id ? { post_id: String(post_id) } : {}) };
|
|
69
80
|
return action === "get" ? { path: "/custom-css", method: "GET", query } : { path: "/custom-css", method: "PUT", query, body };
|
|
70
81
|
}
|
|
82
|
+
if (name === "bricks_get_page") {
|
|
83
|
+
const parsed = getPageSchema.parse(args);
|
|
84
|
+
const query = { ...(parsed.fields ? { fields: parsed.fields.join(",") } : {}), ...(parsed.subtree ? { subtree: parsed.subtree } : {}), ...(parsed.compact ? { compact: "true" } : {}) };
|
|
85
|
+
return { path: `/pages/${parsed.id}`, method: "GET", query: Object.keys(query).length ? query : undefined };
|
|
86
|
+
}
|
|
87
|
+
if (name === "bricks_update_media") {
|
|
88
|
+
const parsed = updateMediaSchema.parse(args);
|
|
89
|
+
const { id, ...body } = parsed;
|
|
90
|
+
return { path: `/media/${id}`, method: "PATCH", body };
|
|
91
|
+
}
|
|
92
|
+
if (name === "bricks_start_here" || name === "bricks_ping") {
|
|
93
|
+
const parsed = verboseSchema.parse(args);
|
|
94
|
+
return { path: name === "bricks_start_here" ? "/start-here" : "/ping", method: "GET", query: parsed.verbose ? { verbose: "true" } : undefined };
|
|
95
|
+
}
|
|
71
96
|
if (name === "bricks_upload_media")
|
|
72
97
|
return { path: "/media", method: "POST", body: uploadMediaSchema.parse(args) };
|
|
73
98
|
if (name === "bricks_find_media_usage")
|
|
@@ -76,4 +101,13 @@ export function buildToolRequest(name, args) {
|
|
|
76
101
|
return { path: `/media/${z.number().int().positive().parse(args.id)}`, method: "DELETE", query: args.force ? { force: "true" } : undefined };
|
|
77
102
|
throw new Error(`Unsupported tool request: ${name}`);
|
|
78
103
|
}
|
|
79
|
-
|
|
104
|
+
/** Compares dotted versions with an optional prerelease: 0.15.0-beta.2 < 0.15.0 < 0.15.1. */
|
|
105
|
+
export function compareVersions(a, b) { const split = (v) => { const [core, pre] = v.split("-", 2); return { core: core.split(".").map(n => parseInt(n, 10) || 0), pre }; }; const x = split(a), y = split(b); for (let i = 0; i < Math.max(x.core.length, y.core.length); i++) {
|
|
106
|
+
const d = (x.core[i] ?? 0) - (y.core[i] ?? 0);
|
|
107
|
+
if (d)
|
|
108
|
+
return Math.sign(d);
|
|
109
|
+
} if (x.pre === y.pre)
|
|
110
|
+
return 0; if (x.pre === undefined)
|
|
111
|
+
return 1; if (y.pre === undefined)
|
|
112
|
+
return -1; return x.pre.localeCompare(y.pre, undefined, { numeric: true }) < 0 ? -1 : x.pre.localeCompare(y.pre, undefined, { numeric: true }) > 0 ? 1 : 0; }
|
|
113
|
+
export function negotiateCapabilities(startHere) { const c = startHere.capabilities ?? {}; return { canWrite: !c.dry_run && !c.allowlist?.read_only, canUpdateMedia: Array.isArray(c.allowlist?.enabled) ? c.allowlist.enabled.includes("media_update") : c.allowlist?.items?.media_update?.enabled === true, 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
|
@@ -3,7 +3,10 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { wpRequest, loadConfig, diagnose, formatDiagnosis } from "./wp-client.js";
|
|
6
|
+
import { wpRequest, loadConfig, diagnose, formatDiagnosis, WPError } from "./wp-client.js";
|
|
7
|
+
import { SERVER_VERSION, getPageSchema, updateMediaSchema, verboseSchema, elementOperationSchema, applyElementOperationsSchema, updatePageSchema, uploadMediaSchema, manageTemplateSchema, revisionsSchema, customCssSchema, globalDataUpdateSchema, globalClassOperationsSchema, dynamicDataSchema, previewUrlSchema, designContextSchema } from "./contracts.js";
|
|
8
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
9
|
+
import { listSites, selectProfile, requestContext } from './profiles.js';
|
|
7
10
|
/** Extract the hostname from a URL, or null if it can't be parsed. */
|
|
8
11
|
function hostOf(url) {
|
|
9
12
|
try {
|
|
@@ -33,26 +36,26 @@ if (subcommand === "doctor" || subcommand === "diagnose") {
|
|
|
33
36
|
if (process.env.WP_URL) {
|
|
34
37
|
console.error(`[bricks-mcp] target WP_URL=${process.env.WP_URL} WP_USER=${process.env.WP_USER ?? "?"}`);
|
|
35
38
|
}
|
|
36
|
-
const server = new Server({ name: "bricks-mcp-server", version:
|
|
39
|
+
const server = new Server({ name: "bricks-mcp-server", version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
37
40
|
const tools = [
|
|
38
41
|
{
|
|
39
42
|
name: "bricks_start_here",
|
|
40
43
|
description: "CALL THIS FIRST, once per session, before any other bricks_* tool. Returns everything needed to work on this site safely: site identity, capabilities (dry-run state, ACF, query filters), the content model (post types + taxonomies + ACF fields), a design-system summary, existing templates, and the working rules that prevent the classic Bricks failure modes (write conflicts, wrong template fields, un-indexed filters, clobbered global classes).",
|
|
41
44
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
42
|
-
schema:
|
|
43
|
-
handler: async () => wpRequest("/start-here"),
|
|
45
|
+
schema: verboseSchema,
|
|
46
|
+
handler: async (args) => wpRequest("/start-here", { query: args.verbose ? { verbose: "true" } : undefined }),
|
|
44
47
|
},
|
|
45
48
|
{
|
|
46
49
|
name: "bricks_ping",
|
|
47
|
-
description: "Verify the WordPress connection and report which site you're connected to (wp_url + site that answered), Bricks + WP version.",
|
|
50
|
+
description: "Verify the WordPress connection and report which site you're connected to (wp_url + site that answered), Bricks + WP + plugin version. verbose:true includes the full capability registry.",
|
|
48
51
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
49
|
-
schema:
|
|
50
|
-
handler: async () => {
|
|
52
|
+
schema: verboseSchema,
|
|
53
|
+
handler: async (args) => {
|
|
51
54
|
// Echo the config this *process* was launched with, up front. If Claude
|
|
52
55
|
// Code is still holding a stale config (old process not restarted after a
|
|
53
56
|
// site switch), wp_url makes it obvious without guessing from permalinks.
|
|
54
57
|
const { baseUrl, user } = loadConfig();
|
|
55
|
-
const res = (await wpRequest("/ping"));
|
|
58
|
+
const res = (await wpRequest("/ping", { query: args.verbose ? { verbose: "true" } : undefined }));
|
|
56
59
|
const configHost = hostOf(baseUrl);
|
|
57
60
|
const siteHost = typeof res.site_url === "string" ? hostOf(res.site_url) : null;
|
|
58
61
|
const mismatch = configHost !== null && siteHost !== null && configHost !== siteHost;
|
|
@@ -110,15 +113,15 @@ const tools = [
|
|
|
110
113
|
},
|
|
111
114
|
{
|
|
112
115
|
name: "bricks_get_page",
|
|
113
|
-
description: "Get a page/post/template by ID, including its Bricks content/header/footer/settings JSON. The response includes `modified` — pass it as expected_modified on bricks_update_page to avoid clobbering edits made in the builder since you read it.",
|
|
116
|
+
description: "Get a page/post/template by ID, including its Bricks content/header/footer/settings JSON. On large pages narrow the read: fields (e.g. [\"content\"]), subtree (an element id: only it and its descendants) and compact:true (structure without settings). Hashes always describe the full tree. The response includes `modified` — pass it as expected_modified on bricks_update_page to avoid clobbering edits made in the builder since you read it.",
|
|
114
117
|
inputSchema: {
|
|
115
118
|
type: "object",
|
|
116
119
|
properties: { id: { type: "number" } },
|
|
117
120
|
required: ["id"],
|
|
118
121
|
additionalProperties: false,
|
|
119
122
|
},
|
|
120
|
-
schema:
|
|
121
|
-
handler: async (args) => wpRequest(`/pages/${args.id}
|
|
123
|
+
schema: getPageSchema,
|
|
124
|
+
handler: async (args) => wpRequest(`/pages/${args.id}`, { query: { fields: args.fields?.join(","), subtree: args.subtree, compact: args.compact ? "true" : undefined } }),
|
|
122
125
|
},
|
|
123
126
|
{
|
|
124
127
|
name: "bricks_update_page",
|
|
@@ -302,6 +305,13 @@ const tools = [
|
|
|
302
305
|
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
306
|
handler: async (args) => wpRequest("/media", { method: "POST", body: args }),
|
|
304
307
|
},
|
|
308
|
+
{
|
|
309
|
+
name: "bricks_update_media",
|
|
310
|
+
description: "Edit alt text, title, caption or description of an existing attachment. Pass expected_version from bricks_list_media; a 409 write_conflict means list again and retry.",
|
|
311
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
312
|
+
schema: updateMediaSchema,
|
|
313
|
+
handler: async (args) => { const { id, ...body } = args; return wpRequest(`/media/${id}`, { method: "PATCH", body }); },
|
|
314
|
+
},
|
|
305
315
|
{
|
|
306
316
|
name: "bricks_find_media_usage",
|
|
307
317
|
description: "Find posts and Bricks data that reference an attachment. Call before deletion.",
|
|
@@ -324,9 +334,9 @@ const tools = [
|
|
|
324
334
|
},
|
|
325
335
|
{
|
|
326
336
|
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(
|
|
337
|
+
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.\n\nOrdering: `position` accepts an index, or first/last, or before/after combined with `target` (a sibling id). Omitting it appends last. Root sections (parent 0) are ordered too — they render in tree order, so use position/target there as well.\n\ninsert: {op, element, parent?, position?, target?} — parent defaults to element.parent (0 = root).\nupdate: {op, id, changes, mode?} — changes may carry settings/label and `children` ONLY to reorder the existing ids; changing parent/id needs move or delete. mode \"merge\" (default) replaces each top-level key inside a group, so passing settings.link swaps the whole link object and drops stale flags; \"deep\" merges recursively; \"replace\" swaps whole groups. null removes a key at any depth.\nmove: {op, id, parent?, position?, target?} — parent defaults to the element's CURRENT parent; pass 0 explicitly to promote to root.\ndelete: {op, id} — removes the element and all its descendants.",
|
|
338
|
+
inputSchema: { type: "object", properties: { id: { type: "number" }, expected_modified: { type: "string" }, field: { type: "string", description: "content (default), header, or footer" }, operations: { type: "array", items: { type: "object", properties: { op: { type: "string", enum: ["insert", "update", "move", "delete"] }, id: { type: "string", description: "target element id (update/move/delete)" }, element: { type: "object", description: "element object to insert" }, parent: { description: "parent id, or 0 for root" }, position: { description: "index, or first/last, or before/after (needs target)" }, target: { type: "string", description: "sibling id for before/after" }, changes: { type: "object", description: "fields to apply (update)" }, mode: { type: "string", enum: ["merge", "deep", "replace"], description: "how changes are applied; default merge" } }, required: ["op"] } } }, required: ["id", "expected_modified", "operations"], additionalProperties: false },
|
|
339
|
+
schema: 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) }),
|
|
330
340
|
handler: async (args) => { const { id, ...body } = args; return wpRequest(`/pages/${id}/element-operations`, { method: "POST", body }); },
|
|
331
341
|
}, {
|
|
332
342
|
name: "bricks_get_design_context",
|
|
@@ -456,6 +466,8 @@ const tools = [
|
|
|
456
466
|
schema: z.object({
|
|
457
467
|
classes: z.array(z.any()).optional(),
|
|
458
468
|
categories: z.array(z.any()).optional(),
|
|
469
|
+
expected_hash: z.string().length(64),
|
|
470
|
+
expected_categories_hash: z.string().length(64).optional(),
|
|
459
471
|
}),
|
|
460
472
|
handler: async (args) => wpRequest("/global-classes", { method: "PUT", body: args }),
|
|
461
473
|
},
|
|
@@ -494,6 +506,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
494
506
|
bricks_list_media: { readOnlyHint: true },
|
|
495
507
|
bricks_find_media_usage: { readOnlyHint: true },
|
|
496
508
|
bricks_upload_media: { readOnlyHint: false, destructiveHint: false },
|
|
509
|
+
bricks_update_media: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
497
510
|
bricks_delete_media: { readOnlyHint: false, destructiveHint: true },
|
|
498
511
|
bricks_create_page: { readOnlyHint: false, destructiveHint: false },
|
|
499
512
|
bricks_create_template: { readOnlyHint: false, destructiveHint: false },
|
|
@@ -502,31 +515,52 @@ const TOOL_ANNOTATIONS = {
|
|
|
502
515
|
bricks_update_global_classes: { readOnlyHint: false, destructiveHint: true },
|
|
503
516
|
bricks_delete_page: { readOnlyHint: false, destructiveHint: true },
|
|
504
517
|
};
|
|
518
|
+
const changeSchema = z.object({ id: z.number().int().positive(), action: z.enum(['list', 'get', 'propose', 'apply', 'undo']), change_id: z.number().int().positive().optional(), field: z.enum(['content', 'header', 'footer']).optional(), expected_modified: z.string().optional(), expected_hash: z.string().length(64).optional(), operations: z.array(elementOperationSchema).min(1).max(100).optional(), page: z.number().int().positive().optional() }).superRefine((v, c) => { if (v.action === 'propose' && !v.operations)
|
|
519
|
+
c.addIssue({ code: z.ZodIssueCode.custom, message: 'operations required' }); if (['get', 'apply', 'undo'].includes(v.action) && !v.change_id)
|
|
520
|
+
c.addIssue({ code: z.ZodIssueCode.custom, message: 'change_id required' }); });
|
|
521
|
+
const schemaOverrides = {
|
|
522
|
+
bricks_update_page: updatePageSchema, bricks_apply_element_operations: applyElementOperationsSchema,
|
|
523
|
+
bricks_upload_media: uploadMediaSchema, bricks_manage_template: manageTemplateSchema,
|
|
524
|
+
bricks_manage_revisions: revisionsSchema, bricks_manage_custom_css: customCssSchema,
|
|
525
|
+
bricks_manage_global_data: globalDataUpdateSchema, bricks_apply_global_class_operations: globalClassOperationsSchema,
|
|
526
|
+
bricks_resolve_dynamic_data: dynamicDataSchema, bricks_get_preview_url: previewUrlSchema, bricks_get_design_context: designContextSchema,
|
|
527
|
+
bricks_get_page: getPageSchema, bricks_update_media: updateMediaSchema, bricks_start_here: verboseSchema, bricks_ping: verboseSchema,
|
|
528
|
+
};
|
|
529
|
+
const catalog = tools.map(t => ({ ...t, schema: schemaOverrides[t.name] ?? t.schema }));
|
|
530
|
+
const extraTools = [
|
|
531
|
+
{ name: 'bricks_list_sites', description: 'List configured site profiles without credentials. With profiles enabled, pass an explicit site_id to every other tool.', schema: z.object({}), handler: async () => ({ sites: listSites() }), annotations: { readOnlyHint: true } },
|
|
532
|
+
{ name: 'bricks_manage_changes', description: 'List/get element history, propose changes for human review in WordPress, apply approved proposals, or undo property changes conditionally. Cannot approve proposals. Structural undo requires revision recovery. Propose does not change page content.', schema: changeSchema, handler: async (args) => { const { id, ...body } = args; return wpRequest(`/pages/${id}/changes`, args.action === 'list' ? { query: args.page ? { page: String(args.page) } : undefined } : { method: 'POST', body }); }, annotations: { readOnlyHint: false, destructiveHint: true } },
|
|
533
|
+
];
|
|
534
|
+
function publicSchema(schema) {
|
|
535
|
+
const json = zodToJsonSchema(schema, { $refStrategy: 'none' });
|
|
536
|
+
delete json.$schema;
|
|
537
|
+
json.properties = { ...json.properties, site_id: { type: 'string', description: 'Required with BRICKS_PROFILES_FILE; explicit target, never a global active site.' }, expected_version: { type: 'string', description: 'Version returned by get_page, required for non-granular existing-post writes.' }, replace_document: { type: 'boolean', description: 'Explicit whole-document replacement, requires separate WordPress permission.' } };
|
|
538
|
+
return json;
|
|
539
|
+
}
|
|
505
540
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
506
|
-
tools:
|
|
507
|
-
name,
|
|
508
|
-
description,
|
|
509
|
-
inputSchema,
|
|
510
|
-
annotations: TOOL_ANNOTATIONS[name],
|
|
511
|
-
})),
|
|
541
|
+
tools: [...catalog.map(t => ({ name: t.name, description: t.name === 'bricks_apply_element_operations' ? 'Edit by ID; prefer op:patch with JSON Pointer, expected_exists and expected old value. It preserves unrelated changes. Stable operation_id required for writes. op:duplicate {id, parent?, position?, target?} copies an element with its descendants under new ids, right after the original by default. Other operations require expected_modified plus expected_hash from the same read. Use dry_run for preview.' : t.description, inputSchema: publicSchema(t.schema), annotations: TOOL_ANNOTATIONS[t.name] })), ...extraTools.map(t => ({ name: t.name, description: t.description, inputSchema: publicSchema(t.schema), annotations: t.annotations }))],
|
|
512
542
|
}));
|
|
513
543
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
514
|
-
const tool =
|
|
544
|
+
const tool = [...catalog, ...extraTools].find((t) => t.name === request.params.name);
|
|
515
545
|
if (!tool) {
|
|
516
546
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
517
547
|
}
|
|
518
|
-
const parsed = tool.schema.parse(request.params.arguments ?? {});
|
|
519
548
|
try {
|
|
520
|
-
const
|
|
549
|
+
const raw = request.params.arguments ?? {};
|
|
550
|
+
const parsed = tool.schema.parse(raw);
|
|
551
|
+
const run = async () => tool.handler(parsed);
|
|
552
|
+
const client = server.getClientVersion();
|
|
553
|
+
const result = tool.name === 'bricks_list_sites' ? await run() : await requestContext.run({ ...selectProfile(typeof raw.site_id === 'string' ? raw.site_id : undefined), expectedVersion: typeof raw.expected_version === 'string' ? raw.expected_version : undefined, replaceDocument: raw.replace_document === true, client: `${client?.name ?? 'unknown'} ${client?.version ?? ''} / bricks-mcp-server` }, run);
|
|
521
554
|
return {
|
|
522
555
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
556
|
+
structuredContent: result,
|
|
523
557
|
};
|
|
524
558
|
}
|
|
525
559
|
catch (err) {
|
|
526
560
|
const message = err instanceof Error ? err.message : String(err);
|
|
527
561
|
return {
|
|
528
562
|
isError: true,
|
|
529
|
-
content: [{ type: "text", text: message }],
|
|
563
|
+
content: [{ type: "text", text: JSON.stringify(err instanceof WPError ? { code: err.code, message, http_status: err.httpStatus, ...err.details } : { code: err instanceof z.ZodError ? 'validation_failed' : 'client_error', message, retryable: false }) }],
|
|
530
564
|
};
|
|
531
565
|
}
|
|
532
566
|
});
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
export const requestContext = new AsyncLocalStorage();
|
|
5
|
+
function validate(profile) {
|
|
6
|
+
const url = new URL(profile.url);
|
|
7
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
8
|
+
throw new Error('Invalid site profile URL.');
|
|
9
|
+
if (url.protocol !== 'https:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
10
|
+
throw new Error('Remote sites require HTTPS.');
|
|
11
|
+
if (!profile.user || !profile.password)
|
|
12
|
+
throw new Error('Site profile credentials are missing.');
|
|
13
|
+
return { ...profile, url: profile.url.replace(/\/$/, '') };
|
|
14
|
+
}
|
|
15
|
+
/** Read once per call, never mutate process.env or a global active site. */
|
|
16
|
+
export function readProfiles() {
|
|
17
|
+
const file = process.env.BRICKS_PROFILES_FILE;
|
|
18
|
+
if (!file)
|
|
19
|
+
return { default: validate({ url: process.env.WP_URL ?? '', user: process.env.WP_USER ?? '', password: process.env.WP_APP_PASSWORD ?? '' }) };
|
|
20
|
+
if (!isAbsolute(file))
|
|
21
|
+
throw new Error('BRICKS_PROFILES_FILE must be an absolute path outside the repository.');
|
|
22
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
23
|
+
const profiles = Object.create(null);
|
|
24
|
+
for (const [id, profile] of Object.entries(raw)) {
|
|
25
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id) || !profile.password_env)
|
|
26
|
+
throw new Error('Invalid profile: use a stable id and password_env.');
|
|
27
|
+
profiles[id] = validate({ url: profile.url, user: profile.user, password: process.env[profile.password_env] ?? '' });
|
|
28
|
+
}
|
|
29
|
+
return profiles;
|
|
30
|
+
}
|
|
31
|
+
export function selectProfile(siteId) {
|
|
32
|
+
const profiles = readProfiles();
|
|
33
|
+
const id = siteId ?? (process.env.BRICKS_PROFILES_FILE ? '' : 'default');
|
|
34
|
+
if (!id || !Object.hasOwn(profiles, id))
|
|
35
|
+
throw new Error('Choose an explicit site_id from bricks_list_sites.');
|
|
36
|
+
return { site: profiles[id], siteId: id };
|
|
37
|
+
}
|
|
38
|
+
export function listSites() {
|
|
39
|
+
return Object.entries(readProfiles()).map(([site_id, p]) => ({ site_id, url: p.url, user: p.user }));
|
|
40
|
+
}
|
package/dist/wp-client.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
import { requestContext } from './profiles.js';
|
|
2
|
+
import { SERVER_VERSION, compareVersions } from './contracts.js';
|
|
1
3
|
const REQUIRED_ENV = ["WP_URL", "WP_USER", "WP_APP_PASSWORD"];
|
|
4
|
+
export class WPError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
httpStatus;
|
|
7
|
+
details;
|
|
8
|
+
constructor(code, message, httpStatus, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.httpStatus = httpStatus;
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
2
15
|
/** Read config from env. Throws a friendly error listing what's missing. */
|
|
3
16
|
export function loadConfig() {
|
|
17
|
+
const selected = requestContext.getStore()?.site;
|
|
18
|
+
if (selected) {
|
|
19
|
+
const appPassword = selected.password.replace(/\s+/g, '');
|
|
20
|
+
return { baseUrl: selected.url, user: selected.user, appPassword, auth: Buffer.from(`${selected.user}:${appPassword}`).toString('base64') };
|
|
21
|
+
}
|
|
4
22
|
const missing = REQUIRED_ENV.filter((k) => !process.env[k]);
|
|
5
23
|
if (missing.length) {
|
|
6
24
|
throw new Error(`Missing required env vars: ${missing.join(", ")}. Set them in your MCP server config.`);
|
|
@@ -11,8 +29,39 @@ export function loadConfig() {
|
|
|
11
29
|
const auth = Buffer.from(`${user}:${appPassword}`).toString("base64");
|
|
12
30
|
return { baseUrl, user, appPassword, auth };
|
|
13
31
|
}
|
|
32
|
+
/** Bridge identity per site, reused for a minute so each write does not pay a bootstrap round trip. */
|
|
33
|
+
const protocolChecks = new Map();
|
|
34
|
+
const PROTOCOL_CHECK_TTL_MS = 60_000;
|
|
35
|
+
/** Forgets verified sites, e.g. after reconnecting or in tests. */
|
|
36
|
+
export function clearWriteProtocolCache() {
|
|
37
|
+
protocolChecks.clear();
|
|
38
|
+
}
|
|
39
|
+
async function checkWriteProtocol(baseUrl) {
|
|
40
|
+
const checkedAt = protocolChecks.get(baseUrl);
|
|
41
|
+
if (checkedAt && Date.now() - checkedAt < PROTOCOL_CHECK_TTL_MS)
|
|
42
|
+
return;
|
|
43
|
+
const bootstrap = await wpRequest('/start-here');
|
|
44
|
+
if (bootstrap.site?.write_protocol !== 'guarded-v2' || bootstrap.site.site_url?.replace(/\/$/, '') !== baseUrl) {
|
|
45
|
+
throw new WPError('bridge_upgrade_required', 'This server requires a guarded-v2 bridge on the selected site before sending writes.', 409, { retryable: false, next_action: 'Update the Bricks MCP Bridge plugin on the site, then run bricks_start_here.' });
|
|
46
|
+
}
|
|
47
|
+
const minimum = bootstrap.site.min_server_version;
|
|
48
|
+
if (minimum && compareVersions(SERVER_VERSION, minimum) < 0) {
|
|
49
|
+
throw new WPError('server_upgrade_required', `The bridge on this site (${bootstrap.site.plugin_version ?? 'unknown'}) requires bricks-mcp-server ${minimum} or newer; this is ${SERVER_VERSION}.`, 426, { retryable: false, next_action: `Update bricks-mcp-server to ${minimum} or newer and restart the MCP client.` });
|
|
50
|
+
}
|
|
51
|
+
protocolChecks.set(baseUrl, Date.now());
|
|
52
|
+
}
|
|
14
53
|
export async function wpRequest(path, opts = {}) {
|
|
15
54
|
const { baseUrl, auth } = loadConfig();
|
|
55
|
+
const context = requestContext.getStore();
|
|
56
|
+
// Fail closed when an older bridge cannot enforce the new write contract.
|
|
57
|
+
if (opts.method && opts.method !== 'GET') {
|
|
58
|
+
await checkWriteProtocol(baseUrl);
|
|
59
|
+
}
|
|
60
|
+
const body = opts.method && opts.method !== 'GET' ? {
|
|
61
|
+
...(opts.body && typeof opts.body === 'object' ? opts.body : {}),
|
|
62
|
+
...(context?.expectedVersion ? { expected_version: context.expectedVersion } : {}),
|
|
63
|
+
...(context?.replaceDocument ? { replace_document: true } : {}),
|
|
64
|
+
} : opts.body;
|
|
16
65
|
const url = new URL(`${baseUrl}/wp-json/bricks-mcp/v1${path}`);
|
|
17
66
|
if (opts.query) {
|
|
18
67
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -24,12 +73,16 @@ export async function wpRequest(path, opts = {}) {
|
|
|
24
73
|
try {
|
|
25
74
|
res = await fetch(url, {
|
|
26
75
|
method: opts.method ?? "GET",
|
|
76
|
+
redirect: 'error',
|
|
77
|
+
signal: AbortSignal.timeout(30000),
|
|
27
78
|
headers: {
|
|
28
79
|
Authorization: `Basic ${auth}`,
|
|
29
80
|
"Content-Type": "application/json",
|
|
30
81
|
Accept: "application/json",
|
|
82
|
+
'X-Bricks-Site': baseUrl,
|
|
83
|
+
'X-Bricks-Client': context?.client ?? 'bricks-mcp-server',
|
|
31
84
|
},
|
|
32
|
-
body:
|
|
85
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
33
86
|
});
|
|
34
87
|
}
|
|
35
88
|
catch (err) {
|
|
@@ -44,7 +97,7 @@ export async function wpRequest(path, opts = {}) {
|
|
|
44
97
|
json = text ? JSON.parse(text) : null;
|
|
45
98
|
}
|
|
46
99
|
catch {
|
|
47
|
-
throw new Error(`WP returned non-JSON (status ${res.status})
|
|
100
|
+
throw new Error(`WP returned non-JSON (status ${res.status}). Check the site's REST endpoint and server logs.`);
|
|
48
101
|
}
|
|
49
102
|
if (!res.ok) {
|
|
50
103
|
const code = json && typeof json === "object" && "code" in json
|
|
@@ -53,21 +106,13 @@ export async function wpRequest(path, opts = {}) {
|
|
|
53
106
|
const message = (json && typeof json === "object" && "message" in json
|
|
54
107
|
? json.message
|
|
55
108
|
: null) ?? `HTTP ${res.status}`;
|
|
56
|
-
|
|
57
|
-
// actionable cause instead of the opaque WordPress error.
|
|
58
|
-
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
59
|
-
const diag = await diagnose().catch(() => null);
|
|
60
|
-
if (diag && diag.problem) {
|
|
61
|
-
throw new Error(formatDiagnosis(diag));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
throw new Error(`WP error: ${message}${code ? ` (${code})` : ""}`);
|
|
109
|
+
throw new WPError(code ?? 'http_error', message, res.status, json?.data ?? {});
|
|
65
110
|
}
|
|
66
111
|
return json;
|
|
67
112
|
}
|
|
68
113
|
async function tryFetch(url, headers = {}) {
|
|
69
114
|
try {
|
|
70
|
-
const res = await fetch(url, { headers: { Accept: "application/json", ...headers } });
|
|
115
|
+
const res = await fetch(url, { headers: { Accept: "application/json", ...headers }, redirect: 'error', signal: AbortSignal.timeout(15000) });
|
|
71
116
|
let body;
|
|
72
117
|
try {
|
|
73
118
|
body = (await res.json());
|
|
@@ -87,7 +132,7 @@ async function tryFetch(url, headers = {}) {
|
|
|
87
132
|
* credentials valid? → user has capability? → plugin installed?
|
|
88
133
|
*/
|
|
89
134
|
export async function diagnose() {
|
|
90
|
-
const missing = REQUIRED_ENV.filter((k) => !process.env[k]);
|
|
135
|
+
const missing = requestContext.getStore() ? [] : REQUIRED_ENV.filter((k) => !process.env[k]);
|
|
91
136
|
if (missing.length) {
|
|
92
137
|
return {
|
|
93
138
|
step: "missing_env",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bricks-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0-beta.2",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
40
|
-
"
|
|
41
|
-
"zod": "^3.23.
|
|
40
|
+
"zod": "^3.23.8",
|
|
41
|
+
"zod-to-json-schema": "^3.23.5"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^20.12.0",
|