@diviops/mcp-server 0.1.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 +228 -0
- package/dist/compatibility.d.ts +26 -0
- package/dist/compatibility.js +68 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1270 -0
- package/dist/schema-optimizer.d.ts +16 -0
- package/dist/schema-optimizer.js +65 -0
- package/dist/wp-cli.d.ts +33 -0
- package/dist/wp-cli.js +315 -0
- package/dist/wp-client.d.ts +41 -0
- package/dist/wp-client.js +92 -0
- package/package.json +50 -0
- package/templates/cards-flex.json +30 -0
- package/templates/cta-gradient.json +16 -0
- package/templates/features-blurbs.json +34 -0
- package/templates/hero-centered.json +26 -0
- package/templates/hero-marquee.json +35 -0
- package/templates/hero-split.json +28 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Optimizer
|
|
3
|
+
*
|
|
4
|
+
* Strips CSS selectors, React component metadata, and VB UI hints from
|
|
5
|
+
* Divi module schemas. Reduces token usage by ~70% while preserving all
|
|
6
|
+
* content-relevant attribute information (attrName, label, description,
|
|
7
|
+
* settings hierarchy).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Optimize a module schema response for AI content generation.
|
|
11
|
+
* Strips ~70% of tokens from the `attributes` object (CSS selectors,
|
|
12
|
+
* React components, VB UI metadata). Top-level fields (name, title,
|
|
13
|
+
* category, description, supports) are preserved as-is — they are
|
|
14
|
+
* small and content-relevant.
|
|
15
|
+
*/
|
|
16
|
+
export declare function optimizeSchema(schema: Record<string, any>): Record<string, any>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Optimizer
|
|
3
|
+
*
|
|
4
|
+
* Strips CSS selectors, React component metadata, and VB UI hints from
|
|
5
|
+
* Divi module schemas. Reduces token usage by ~70% while preserving all
|
|
6
|
+
* content-relevant attribute information (attrName, label, description,
|
|
7
|
+
* settings hierarchy).
|
|
8
|
+
*/
|
|
9
|
+
const STRIP_KEYS = new Set([
|
|
10
|
+
'component', // React component type/name/props
|
|
11
|
+
'selector', // CSS selector templates
|
|
12
|
+
'customPostTypeSelector', // CPT-specific selectors
|
|
13
|
+
'styleProps', // CSS property → selector mappings
|
|
14
|
+
'elementType', // HTML element type hint (redundant with module.advanced.html)
|
|
15
|
+
'render', // Whether VB renders this field
|
|
16
|
+
'priority', // Field render order in VB
|
|
17
|
+
'groupSlug', // VB settings panel group
|
|
18
|
+
'category', // Field category in VB (not block category)
|
|
19
|
+
'features', // Hover/sticky/preset feature flags
|
|
20
|
+
'subName', // Sub-field identifier (redundant with structure)
|
|
21
|
+
'className', // WordPress block attribute — Divi 5 ignores it (use module.decoration.attributes)
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Recursively strip keys and remove empty containers.
|
|
25
|
+
*/
|
|
26
|
+
function stripAndClean(value) {
|
|
27
|
+
if (value === null || typeof value !== 'object') {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
const cleaned = value
|
|
32
|
+
.map((item) => stripAndClean(item))
|
|
33
|
+
.filter((item) => item != null);
|
|
34
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
35
|
+
}
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [key, val] of Object.entries(value)) {
|
|
38
|
+
if (STRIP_KEYS.has(key)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const cleaned = stripAndClean(val);
|
|
42
|
+
if (cleaned != null) {
|
|
43
|
+
result[key] = cleaned;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Optimize a module schema response for AI content generation.
|
|
50
|
+
* Strips ~70% of tokens from the `attributes` object (CSS selectors,
|
|
51
|
+
* React components, VB UI metadata). Top-level fields (name, title,
|
|
52
|
+
* category, description, supports) are preserved as-is — they are
|
|
53
|
+
* small and content-relevant.
|
|
54
|
+
*/
|
|
55
|
+
export function optimizeSchema(schema) {
|
|
56
|
+
const { attributes, ...rest } = schema;
|
|
57
|
+
if (!attributes) {
|
|
58
|
+
return schema;
|
|
59
|
+
}
|
|
60
|
+
const optimized = stripAndClean(attributes);
|
|
61
|
+
return {
|
|
62
|
+
...rest,
|
|
63
|
+
attributes: optimized ?? {},
|
|
64
|
+
};
|
|
65
|
+
}
|
package/dist/wp-cli.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WP-CLI Wrapper for Local by Flywheel
|
|
3
|
+
*
|
|
4
|
+
* Executes WP-CLI commands using Local's PHP/MySQL environment.
|
|
5
|
+
* Uses execFile (no shell) to prevent command injection.
|
|
6
|
+
* Auto-detects PHP/MySQL versions from Local's directory structure.
|
|
7
|
+
*/
|
|
8
|
+
interface WpCliConfig {
|
|
9
|
+
/** Absolute path to the WordPress installation */
|
|
10
|
+
wpPath: string;
|
|
11
|
+
/** Local by Flywheel site ID (e.g. "BKr7xMxlH"). Auto-detected from wpPath if omitted. */
|
|
12
|
+
localSiteId?: string;
|
|
13
|
+
/** Custom WP-CLI command prefix for containerized environments (e.g. "ddev wp"). */
|
|
14
|
+
wpCliCmd?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createWpCli(config: WpCliConfig): {
|
|
17
|
+
/**
|
|
18
|
+
* Execute a WP-CLI command. Returns stdout on success.
|
|
19
|
+
* Commands are parsed into args and validated against an allowlist.
|
|
20
|
+
* Uses execFile (no shell) to prevent command injection.
|
|
21
|
+
*/
|
|
22
|
+
run(command: string): Promise<{
|
|
23
|
+
success: boolean;
|
|
24
|
+
output: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** Return the list of allowed commands and available extensions. */
|
|
28
|
+
getAllowedCommands(): {
|
|
29
|
+
allowed: string[];
|
|
30
|
+
extendable: string[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export {};
|
package/dist/wp-cli.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WP-CLI Wrapper for Local by Flywheel
|
|
3
|
+
*
|
|
4
|
+
* Executes WP-CLI commands using Local's PHP/MySQL environment.
|
|
5
|
+
* Uses execFile (no shell) to prevent command injection.
|
|
6
|
+
* Auto-detects PHP/MySQL versions from Local's directory structure.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
/**
|
|
13
|
+
* Default WP-CLI commands — safe for public distribution.
|
|
14
|
+
* Read-only commands + non-destructive writes needed for core MCP functionality.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_COMMANDS = [
|
|
17
|
+
// Options (read-only)
|
|
18
|
+
'option get',
|
|
19
|
+
'option list',
|
|
20
|
+
// Posts (read + create/update)
|
|
21
|
+
'post list',
|
|
22
|
+
'post get',
|
|
23
|
+
'post create',
|
|
24
|
+
'post update',
|
|
25
|
+
'post meta get',
|
|
26
|
+
'post meta list',
|
|
27
|
+
'post meta set',
|
|
28
|
+
'post meta update',
|
|
29
|
+
// Users (read-only)
|
|
30
|
+
'user list',
|
|
31
|
+
// Cache (non-destructive maintenance)
|
|
32
|
+
'cache flush',
|
|
33
|
+
'transient delete',
|
|
34
|
+
'rewrite flush',
|
|
35
|
+
// Info (read-only)
|
|
36
|
+
'cron event list',
|
|
37
|
+
'plugin list',
|
|
38
|
+
'theme list',
|
|
39
|
+
'menu list',
|
|
40
|
+
'term list',
|
|
41
|
+
'term create',
|
|
42
|
+
'site url',
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Extended commands that require explicit opt-in via DIVIOPS_WP_CLI_ALLOW env var.
|
|
46
|
+
* These carry higher risk: destructive operations, arbitrary code execution,
|
|
47
|
+
* or the ability to disable security features.
|
|
48
|
+
*
|
|
49
|
+
* To enable, set: DIVIOPS_WP_CLI_ALLOW="option update,post delete,eval-file"
|
|
50
|
+
*/
|
|
51
|
+
const EXTENDED_COMMANDS = [
|
|
52
|
+
'option update', // Can change site URL, admin email, active plugins
|
|
53
|
+
'post delete', // Destructive — permanently removes content
|
|
54
|
+
'post meta delete', // Destructive — removes metadata
|
|
55
|
+
'plugin activate', // Can enable untrusted plugins
|
|
56
|
+
'plugin deactivate', // Can disable security plugins
|
|
57
|
+
'eval-file', // Executes arbitrary PHP from a file path
|
|
58
|
+
];
|
|
59
|
+
/** Build the effective allowlist from defaults + user opt-ins. */
|
|
60
|
+
function buildAllowlist() {
|
|
61
|
+
const extra = process.env.DIVIOPS_WP_CLI_ALLOW?.trim();
|
|
62
|
+
if (!extra)
|
|
63
|
+
return DEFAULT_COMMANDS;
|
|
64
|
+
const requested = extra.split(',').map((s) => s.trim()).filter(Boolean);
|
|
65
|
+
const granted = new Set(DEFAULT_COMMANDS);
|
|
66
|
+
for (const cmd of requested) {
|
|
67
|
+
if (EXTENDED_COMMANDS.includes(cmd)) {
|
|
68
|
+
granted.add(cmd);
|
|
69
|
+
}
|
|
70
|
+
else if (!granted.has(cmd)) {
|
|
71
|
+
console.warn(`[diviops] Ignoring unknown WP-CLI allow entry: "${cmd}"`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...granted];
|
|
75
|
+
}
|
|
76
|
+
const ALLOWED_COMMANDS = buildAllowlist();
|
|
77
|
+
/**
|
|
78
|
+
* Validate a parsed command against the allowlist.
|
|
79
|
+
* Checks the first 1-3 args against allowed command prefixes (supports
|
|
80
|
+
* 2-word commands like "post list" and 3-word like "post meta get").
|
|
81
|
+
*/
|
|
82
|
+
function isCommandAllowed(args) {
|
|
83
|
+
if (args.length === 0) {
|
|
84
|
+
return { allowed: false, reason: 'Empty command' };
|
|
85
|
+
}
|
|
86
|
+
// Build candidate prefixes: "post meta get", "post meta", "post"
|
|
87
|
+
const threeWord = args.slice(0, 3).join(' ');
|
|
88
|
+
const twoWord = args.slice(0, 2).join(' ');
|
|
89
|
+
const oneWord = args[0];
|
|
90
|
+
for (const allowed of ALLOWED_COMMANDS) {
|
|
91
|
+
if (threeWord === allowed || twoWord === allowed || oneWord === allowed) {
|
|
92
|
+
return { allowed: true };
|
|
93
|
+
}
|
|
94
|
+
// Allow commands with additional args/flags after the allowed prefix
|
|
95
|
+
if (threeWord.startsWith(allowed + ' ') || twoWord.startsWith(allowed + ' ')) {
|
|
96
|
+
return { allowed: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const extendable = EXTENDED_COMMANDS.filter((c) => !ALLOWED_COMMANDS.includes(c));
|
|
100
|
+
const hint = extendable.some((c) => twoWord === c || threeWord === c || oneWord === c.split(' ')[0])
|
|
101
|
+
? ` This command can be enabled via DIVIOPS_WP_CLI_ALLOW env var (see README).`
|
|
102
|
+
: extendable.length > 0
|
|
103
|
+
? ` Opt-in commands available: ${extendable.join(', ')}.`
|
|
104
|
+
: '';
|
|
105
|
+
return {
|
|
106
|
+
allowed: false,
|
|
107
|
+
reason: `Command "${twoWord}" not in allowlist.${hint}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse a command string into an array of arguments.
|
|
112
|
+
* Handles quoted strings (single and double quotes).
|
|
113
|
+
*/
|
|
114
|
+
function parseCommand(command) {
|
|
115
|
+
const args = [];
|
|
116
|
+
let current = '';
|
|
117
|
+
let inSingle = false;
|
|
118
|
+
let inDouble = false;
|
|
119
|
+
for (let i = 0; i < command.length; i++) {
|
|
120
|
+
const ch = command[i];
|
|
121
|
+
if (ch === "'" && !inDouble) {
|
|
122
|
+
inSingle = !inSingle;
|
|
123
|
+
}
|
|
124
|
+
else if (ch === '"' && !inSingle) {
|
|
125
|
+
inDouble = !inDouble;
|
|
126
|
+
}
|
|
127
|
+
else if (ch === ' ' && !inSingle && !inDouble) {
|
|
128
|
+
if (current.length > 0) {
|
|
129
|
+
args.push(current);
|
|
130
|
+
current = '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
current += ch;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (current.length > 0) {
|
|
138
|
+
args.push(current);
|
|
139
|
+
}
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Find the latest installed version of a Local lightning-service.
|
|
144
|
+
* Scans ~/Library/Application Support/Local/lightning-services/ for directories
|
|
145
|
+
* matching the prefix (e.g. "php-", "mysql-") and returns the latest.
|
|
146
|
+
*/
|
|
147
|
+
function findServiceDir(localSupport, prefix, platform) {
|
|
148
|
+
const servicesDir = join(localSupport, 'lightning-services');
|
|
149
|
+
try {
|
|
150
|
+
const dirs = readdirSync(servicesDir)
|
|
151
|
+
.filter((d) => d.startsWith(prefix))
|
|
152
|
+
.sort()
|
|
153
|
+
.reverse(); // Latest version first
|
|
154
|
+
for (const dir of dirs) {
|
|
155
|
+
const binDir = join(servicesDir, dir, 'bin', platform, 'bin');
|
|
156
|
+
try {
|
|
157
|
+
readdirSync(binDir); // Check it exists
|
|
158
|
+
return binDir;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Try without nested bin
|
|
162
|
+
const altDir = join(servicesDir, dir, 'bin', platform);
|
|
163
|
+
try {
|
|
164
|
+
readdirSync(altDir);
|
|
165
|
+
return altDir;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// lightning-services dir not found
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Detect the platform string for Local's binary directories.
|
|
180
|
+
*/
|
|
181
|
+
function detectPlatform() {
|
|
182
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
183
|
+
return `darwin-${arch}`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Auto-detect the Local by Flywheel site ID from a WordPress path.
|
|
187
|
+
* Reads ~/Library/Application Support/Local/sites.json and matches by path.
|
|
188
|
+
*/
|
|
189
|
+
function detectLocalSiteId(wpPath) {
|
|
190
|
+
const home = homedir();
|
|
191
|
+
const localSupport = join(home, 'Library', 'Application Support', 'Local');
|
|
192
|
+
const sitesFile = join(localSupport, 'sites.json');
|
|
193
|
+
if (!existsSync(sitesFile)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const sites = JSON.parse(readFileSync(sitesFile, 'utf-8'));
|
|
198
|
+
// Normalize the wpPath: strip trailing /app/public if present
|
|
199
|
+
const normalizedWp = wpPath.replace(/\/app\/public\/?$/, '');
|
|
200
|
+
for (const [siteId, site] of Object.entries(sites)) {
|
|
201
|
+
// site.path may use ~ for home dir
|
|
202
|
+
const rawPath = site.path ?? '';
|
|
203
|
+
if (!rawPath)
|
|
204
|
+
continue; // Skip entries with missing path.
|
|
205
|
+
const sitePath = rawPath.replace(/^~/, home);
|
|
206
|
+
if (normalizedWp === sitePath || wpPath.startsWith(sitePath + '/')) {
|
|
207
|
+
return siteId;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
console.error(`Error reading Local sites.json: ${e}`);
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Build the environment variables needed for Local by Flywheel's WP-CLI.
|
|
218
|
+
* Auto-detects PHP and MySQL versions from Local's directory structure.
|
|
219
|
+
*/
|
|
220
|
+
function buildLocalEnv(localSiteId) {
|
|
221
|
+
const localSupport = join(homedir(), 'Library', 'Application Support', 'Local');
|
|
222
|
+
const runDir = `${localSupport}/run/${localSiteId}`;
|
|
223
|
+
const platform = detectPlatform();
|
|
224
|
+
const phpDir = findServiceDir(localSupport, 'php-', platform);
|
|
225
|
+
const mysqlDir = findServiceDir(localSupport, 'mysql-', platform);
|
|
226
|
+
const wpCliDir = '/Applications/Local.app/Contents/Resources/extraResources/bin/wp-cli/posix';
|
|
227
|
+
const pathParts = [
|
|
228
|
+
mysqlDir,
|
|
229
|
+
phpDir,
|
|
230
|
+
wpCliDir,
|
|
231
|
+
process.env.PATH ?? '',
|
|
232
|
+
].filter(Boolean);
|
|
233
|
+
return {
|
|
234
|
+
...process.env,
|
|
235
|
+
MYSQL_HOME: `${runDir}/conf/mysql`,
|
|
236
|
+
PHPRC: `${runDir}/conf/php`,
|
|
237
|
+
PATH: pathParts.join(':'),
|
|
238
|
+
WP_CLI_DISABLE_AUTO_CHECK_UPDATE: '1',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export function createWpCli(config) {
|
|
242
|
+
let executable = 'wp';
|
|
243
|
+
let prefixArgs = [];
|
|
244
|
+
let env;
|
|
245
|
+
const customWpCliCmd = config.wpCliCmd?.trim();
|
|
246
|
+
const execOptions = {
|
|
247
|
+
env: process.env,
|
|
248
|
+
timeout: 30_000,
|
|
249
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
250
|
+
};
|
|
251
|
+
if (customWpCliCmd) {
|
|
252
|
+
[executable, ...prefixArgs] = parseCommand(customWpCliCmd);
|
|
253
|
+
if (!executable) {
|
|
254
|
+
throw new Error('WP_CLI_CMD must include an executable.');
|
|
255
|
+
}
|
|
256
|
+
env = { ...process.env };
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Auto-detect site ID if not provided.
|
|
260
|
+
const localSiteId = config.localSiteId || detectLocalSiteId(config.wpPath);
|
|
261
|
+
if (!localSiteId) {
|
|
262
|
+
throw new Error(`Could not detect Local by Flywheel site ID for path "${config.wpPath}". ` +
|
|
263
|
+
`Provide LOCAL_SITE_ID env var or ensure the site is registered in Local.`);
|
|
264
|
+
}
|
|
265
|
+
env = buildLocalEnv(localSiteId);
|
|
266
|
+
}
|
|
267
|
+
const runOptions = customWpCliCmd
|
|
268
|
+
? { ...execOptions, env, cwd: config.wpPath }
|
|
269
|
+
: { ...execOptions, env };
|
|
270
|
+
return {
|
|
271
|
+
/**
|
|
272
|
+
* Execute a WP-CLI command. Returns stdout on success.
|
|
273
|
+
* Commands are parsed into args and validated against an allowlist.
|
|
274
|
+
* Uses execFile (no shell) to prevent command injection.
|
|
275
|
+
*/
|
|
276
|
+
async run(command) {
|
|
277
|
+
const args = parseCommand(command);
|
|
278
|
+
const check = isCommandAllowed(args);
|
|
279
|
+
if (!check.allowed) {
|
|
280
|
+
return { success: false, output: '', error: check.reason };
|
|
281
|
+
}
|
|
282
|
+
const fullArgs = customWpCliCmd
|
|
283
|
+
? [...prefixArgs, ...args, '--no-color']
|
|
284
|
+
: [...args, `--path=${config.wpPath}`, '--no-color'];
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
execFile(executable, fullArgs, runOptions, (error, stdout, stderr) => {
|
|
287
|
+
// Filter PHP deprecation warnings from output
|
|
288
|
+
const output = (stdout + '\n' + stderr)
|
|
289
|
+
.split('\n')
|
|
290
|
+
.filter((line) => !line.includes('Deprecated:') && !line.includes('PHP Deprecated'))
|
|
291
|
+
.join('\n')
|
|
292
|
+
.trim();
|
|
293
|
+
if (error) {
|
|
294
|
+
const detail = error.killed
|
|
295
|
+
? 'Command timed out'
|
|
296
|
+
: error.signal
|
|
297
|
+
? `Killed by signal ${error.signal}`
|
|
298
|
+
: `Exit code ${error.code ?? 'unknown'}`;
|
|
299
|
+
resolve({ success: false, output, error: detail });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
resolve({ success: true, output });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
/** Return the list of allowed commands and available extensions. */
|
|
308
|
+
getAllowedCommands() {
|
|
309
|
+
return {
|
|
310
|
+
allowed: [...ALLOWED_COMMANDS],
|
|
311
|
+
extendable: EXTENDED_COMMANDS.filter((c) => !ALLOWED_COMMANDS.includes(c)),
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress REST API client with Application Password authentication.
|
|
3
|
+
*
|
|
4
|
+
* Uses WP Application Passwords (built into WP 5.6+) for auth.
|
|
5
|
+
* Generate one at: WP Admin → Users → Your Profile → Application Passwords.
|
|
6
|
+
*/
|
|
7
|
+
import { type HandshakeResult } from './compatibility.js';
|
|
8
|
+
export interface WPClientConfig {
|
|
9
|
+
siteUrl: string;
|
|
10
|
+
username: string;
|
|
11
|
+
applicationPassword: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class WPClient {
|
|
14
|
+
private baseUrl;
|
|
15
|
+
private authHeader;
|
|
16
|
+
constructor(config: WPClientConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Make a request to the diviops/v1 REST namespace.
|
|
19
|
+
*/
|
|
20
|
+
request<T = unknown>(endpoint: string, options?: {
|
|
21
|
+
method?: string;
|
|
22
|
+
body?: Record<string, unknown>;
|
|
23
|
+
params?: Record<string, string>;
|
|
24
|
+
}): Promise<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Test the connection to WordPress.
|
|
27
|
+
*/
|
|
28
|
+
testConnection(): Promise<{
|
|
29
|
+
ok: boolean;
|
|
30
|
+
message: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Perform version handshake with the WP plugin.
|
|
34
|
+
*
|
|
35
|
+
* Verifies that the plugin version is compatible with this server.
|
|
36
|
+
* Throws on:
|
|
37
|
+
* - Network errors or any non-2xx HTTP response (401/403/426/503).
|
|
38
|
+
* - Plugin version below {@link MIN_PLUGIN_VERSION}.
|
|
39
|
+
*/
|
|
40
|
+
handshake(serverVersion: string): Promise<HandshakeResult>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress REST API client with Application Password authentication.
|
|
3
|
+
*
|
|
4
|
+
* Uses WP Application Passwords (built into WP 5.6+) for auth.
|
|
5
|
+
* Generate one at: WP Admin → Users → Your Profile → Application Passwords.
|
|
6
|
+
*/
|
|
7
|
+
import { MIN_PLUGIN_VERSION, compareVersions, } from './compatibility.js';
|
|
8
|
+
export class WPClient {
|
|
9
|
+
baseUrl;
|
|
10
|
+
authHeader;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
// Strip trailing slash.
|
|
13
|
+
this.baseUrl = config.siteUrl.replace(/\/+$/, '');
|
|
14
|
+
// WP Application Passwords use Basic Auth.
|
|
15
|
+
const credentials = Buffer.from(`${config.username}:${config.applicationPassword}`).toString('base64');
|
|
16
|
+
this.authHeader = `Basic ${credentials}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Make a request to the diviops/v1 REST namespace.
|
|
20
|
+
*/
|
|
21
|
+
async request(endpoint, options = {}) {
|
|
22
|
+
const { method = 'GET', body, params } = options;
|
|
23
|
+
let url = `${this.baseUrl}/wp-json/diviops/v1${endpoint}`;
|
|
24
|
+
if (params) {
|
|
25
|
+
const searchParams = new URLSearchParams(params);
|
|
26
|
+
url += `?${searchParams.toString()}`;
|
|
27
|
+
}
|
|
28
|
+
const fetchOptions = {
|
|
29
|
+
method,
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: this.authHeader,
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
if (body && method !== 'GET') {
|
|
37
|
+
fetchOptions.body = JSON.stringify(body);
|
|
38
|
+
}
|
|
39
|
+
const response = await fetch(url, fetchOptions);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorBody = await response.text();
|
|
42
|
+
let errorMessage;
|
|
43
|
+
try {
|
|
44
|
+
const errorJson = JSON.parse(errorBody);
|
|
45
|
+
errorMessage = errorJson.message || errorBody;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
errorMessage = errorBody;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`WordPress API error (${response.status}): ${errorMessage}`);
|
|
51
|
+
}
|
|
52
|
+
return response.json();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Test the connection to WordPress.
|
|
56
|
+
*/
|
|
57
|
+
async testConnection() {
|
|
58
|
+
try {
|
|
59
|
+
const result = await this.request('/settings');
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
message: `Connected to Divi ${result.builder?.version ?? 'unknown'}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
message: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Perform version handshake with the WP plugin.
|
|
74
|
+
*
|
|
75
|
+
* Verifies that the plugin version is compatible with this server.
|
|
76
|
+
* Throws on:
|
|
77
|
+
* - Network errors or any non-2xx HTTP response (401/403/426/503).
|
|
78
|
+
* - Plugin version below {@link MIN_PLUGIN_VERSION}.
|
|
79
|
+
*/
|
|
80
|
+
async handshake(serverVersion) {
|
|
81
|
+
const result = await this.request('/handshake', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: { mcp_server_version: serverVersion },
|
|
84
|
+
});
|
|
85
|
+
// Server-side check passed — now verify plugin meets our minimum.
|
|
86
|
+
if (compareVersions(result.plugin_version, MIN_PLUGIN_VERSION) < 0) {
|
|
87
|
+
throw new Error(`WP plugin version ${result.plugin_version} is below the minimum required ${MIN_PLUGIN_VERSION}. ` +
|
|
88
|
+
'Please update the diviops-agent plugin.');
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diviops/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"diviops-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsc --watch",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"divi",
|
|
24
|
+
"wordpress",
|
|
25
|
+
"claude",
|
|
26
|
+
"visual-builder",
|
|
27
|
+
"model-context-protocol"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/oaris-dev/diviops.git",
|
|
33
|
+
"directory": "diviops-server"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/oaris-dev/diviops/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/oaris-dev/diviops/tree/main/diviops-server#readme",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
44
|
+
"zod": "^4.3.6"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.0.0",
|
|
48
|
+
"typescript": "^5.7.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cards-flex",
|
|
3
|
+
"description": "3-column card layout using flex row. Each card has rounded corners, background color, padding, heading, description, and price/label. Third card can be inverted (dark bg).",
|
|
4
|
+
"customizable": ["card backgrounds", "headings", "descriptions", "prices", "colors", "font family", "border radius"],
|
|
5
|
+
"markup": "<!-- wp:divi/section {\"module\":{\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{section_bg}}\"}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"80px\",\"right\":\"\",\"bottom\":\"80px\",\"left\":\"\",\"syncVertical\":\"on\",\"syncHorizontal\":\"off\"}}}},\"layout\":{\"desktop\":{\"value\":{\"display\":\"block\"}}}}},\"builderVersion\":\"5.0.3\"} -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_3\"}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexWrap\":\"nowrap\",\"gap\":\"30px\"}}}}},\"builderVersion\":\"5.0.3\"} -->\n<!-- wp:divi/column {\"module\":{\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"8_24\"}}},\"background\":{\"desktop\":{\"value\":{\"color\":\"{{card_bg}}\"}}},\"border\":{\"desktop\":{\"value\":{\"radius\":{\"topLeft\":\"16px\",\"topRight\":\"16px\",\"bottomLeft\":\"16px\",\"bottomRight\":\"16px\",\"sync\":\"on\"}}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"32px\",\"bottom\":\"32px\",\"left\":\"28px\",\"right\":\"28px\",\"syncVertical\":\"on\",\"syncHorizontal\":\"on\"}}}}}},\"builderVersion\":\"5.0.3\"} -->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"headingFont\":{\"h3\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"600\",\"color\":\"{{card_heading_color}}\",\"size\":\"24px\",\"lineHeight\":\"1.3em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003ch3\\u003e{{card_1_title}}\\u003c/h3\\u003e\"}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"300\",\"color\":\"{{card_text_color}}\",\"size\":\"15px\",\"lineHeight\":\"1.7em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003cp\\u003e{{card_1_text}}\\u003c/p\\u003e\"}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"headingFont\":{\"h4\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"700\",\"color\":\"{{accent_color}}\",\"size\":\"28px\",\"lineHeight\":\"1.2em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003ch4\\u003e{{card_1_price}}\\u003c/h4\\u003e\"}}},\"module\":{\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"margin\":{\"top\":\"16px\",\"right\":\"\",\"bottom\":\"\",\"left\":\"\",\"syncVertical\":\"off\",\"syncHorizontal\":\"off\"}}}}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- /wp:divi/column -->\n<!-- wp:divi/column {\"module\":{\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"8_24\"}}},\"background\":{\"desktop\":{\"value\":{\"color\":\"{{card_bg}}\"}}},\"border\":{\"desktop\":{\"value\":{\"radius\":{\"topLeft\":\"16px\",\"topRight\":\"16px\",\"bottomLeft\":\"16px\",\"bottomRight\":\"16px\",\"sync\":\"on\"}}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"32px\",\"bottom\":\"32px\",\"left\":\"28px\",\"right\":\"28px\",\"syncVertical\":\"on\",\"syncHorizontal\":\"on\"}}}}}},\"builderVersion\":\"5.0.3\"} -->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"headingFont\":{\"h3\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"600\",\"color\":\"{{card_heading_color}}\",\"size\":\"24px\",\"lineHeight\":\"1.3em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003ch3\\u003e{{card_2_title}}\\u003c/h3\\u003e\"}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"300\",\"color\":\"{{card_text_color}}\",\"size\":\"15px\",\"lineHeight\":\"1.7em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003cp\\u003e{{card_2_text}}\\u003c/p\\u003e\"}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"headingFont\":{\"h4\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"700\",\"color\":\"{{accent_color}}\",\"size\":\"28px\",\"lineHeight\":\"1.2em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003ch4\\u003e{{card_2_price}}\\u003c/h4\\u003e\"}}},\"module\":{\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"margin\":{\"top\":\"16px\",\"right\":\"\",\"bottom\":\"\",\"left\":\"\",\"syncVertical\":\"off\",\"syncHorizontal\":\"off\"}}}}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- /wp:divi/column -->\n<!-- wp:divi/column {\"module\":{\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"8_24\"}}},\"background\":{\"desktop\":{\"value\":{\"color\":\"{{card_dark_bg}}\"}}},\"border\":{\"desktop\":{\"value\":{\"radius\":{\"topLeft\":\"16px\",\"topRight\":\"16px\",\"bottomLeft\":\"16px\",\"bottomRight\":\"16px\",\"sync\":\"on\"}}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"32px\",\"bottom\":\"32px\",\"left\":\"28px\",\"right\":\"28px\",\"syncVertical\":\"on\",\"syncHorizontal\":\"on\"}}}}}},\"builderVersion\":\"5.0.3\"} -->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"headingFont\":{\"h3\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"600\",\"color\":\"#ffffff\",\"size\":\"24px\",\"lineHeight\":\"1.3em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003ch3\\u003e{{card_3_title}}\\u003c/h3\\u003e\"}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"300\",\"color\":\"rgba(255,255,255,0.7)\",\"size\":\"15px\",\"lineHeight\":\"1.7em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003cp\\u003e{{card_3_text}}\\u003c/p\\u003e\"}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- wp:divi/text {\"content\":{\"decoration\":{\"headingFont\":{\"h4\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{font_family}}\",\"weight\":\"700\",\"color\":\"{{accent_color}}\",\"size\":\"28px\",\"lineHeight\":\"1.2em\"}}}}}},\"innerContent\":{\"desktop\":{\"value\":\"\\u003ch4\\u003e{{card_3_price}}\\u003c/h4\\u003e\"}}},\"module\":{\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"margin\":{\"top\":\"16px\",\"right\":\"\",\"bottom\":\"\",\"left\":\"\",\"syncVertical\":\"off\",\"syncHorizontal\":\"off\"}}}}}},\"builderVersion\":\"5.0.3\"} /-->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- /wp:divi/section -->",
|
|
6
|
+
"variables": {
|
|
7
|
+
"section_bg": "#ffffff",
|
|
8
|
+
"card_bg": "#faf9f6",
|
|
9
|
+
"card_dark_bg": "#1a1a1a",
|
|
10
|
+
"card_heading_color": "#1a1a1a",
|
|
11
|
+
"card_text_color": "#666666",
|
|
12
|
+
"accent_color": "#f5c518",
|
|
13
|
+
"font_family": "Space Grotesk",
|
|
14
|
+
"card_1_title": "Card One",
|
|
15
|
+
"card_1_text": "Description for card one.",
|
|
16
|
+
"card_1_price": "150\u20ac",
|
|
17
|
+
"card_2_title": "Card Two",
|
|
18
|
+
"card_2_text": "Description for card two.",
|
|
19
|
+
"card_2_price": "75\u20ac",
|
|
20
|
+
"card_3_title": "Card Three",
|
|
21
|
+
"card_3_text": "Description for card three.",
|
|
22
|
+
"card_3_price": "Free"
|
|
23
|
+
},
|
|
24
|
+
"notes": [
|
|
25
|
+
"Uses flex row with equal-columns_3 and flexType 8_24 per column",
|
|
26
|
+
"Third card has dark background with white/transparent text for contrast",
|
|
27
|
+
"Gap between cards controlled by row layout.gap",
|
|
28
|
+
"Cards stack automatically on mobile via Divi's responsive behavior"
|
|
29
|
+
]
|
|
30
|
+
}
|