@local-labs-jpollock/local-cli 0.0.1
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/addon-dist/bin/mcp-stdio.js +2808 -0
- package/addon-dist/lib/common/constants.d.ts +22 -0
- package/addon-dist/lib/common/constants.js +26 -0
- package/addon-dist/lib/common/theme.d.ts +68 -0
- package/addon-dist/lib/common/theme.js +126 -0
- package/addon-dist/lib/common/types.d.ts +298 -0
- package/addon-dist/lib/common/types.js +6 -0
- package/addon-dist/lib/main/config/ConnectionInfo.d.ts +25 -0
- package/addon-dist/lib/main/config/ConnectionInfo.js +82 -0
- package/addon-dist/lib/main/index.d.ts +12 -0
- package/addon-dist/lib/main/index.js +3322 -0
- package/addon-dist/lib/main/mcp/McpAuth.d.ts +37 -0
- package/addon-dist/lib/main/mcp/McpAuth.js +87 -0
- package/addon-dist/lib/main/mcp/McpServer.d.ts +67 -0
- package/addon-dist/lib/main/mcp/McpServer.js +343 -0
- package/addon-dist/lib/main/mcp/tools/changePhpVersion.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/changePhpVersion.js +81 -0
- package/addon-dist/lib/main/mcp/tools/cloneSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/cloneSite.js +66 -0
- package/addon-dist/lib/main/mcp/tools/createSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/createSite.js +137 -0
- package/addon-dist/lib/main/mcp/tools/deleteSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/deleteSite.js +72 -0
- package/addon-dist/lib/main/mcp/tools/exportDatabase.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/exportDatabase.js +72 -0
- package/addon-dist/lib/main/mcp/tools/exportSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/exportSite.js +103 -0
- package/addon-dist/lib/main/mcp/tools/getLocalInfo.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/getLocalInfo.js +72 -0
- package/addon-dist/lib/main/mcp/tools/getSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/getSite.js +68 -0
- package/addon-dist/lib/main/mcp/tools/getSiteLogs.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/getSiteLogs.js +149 -0
- package/addon-dist/lib/main/mcp/tools/helpers.d.ts +59 -0
- package/addon-dist/lib/main/mcp/tools/helpers.js +179 -0
- package/addon-dist/lib/main/mcp/tools/importDatabase.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/importDatabase.js +109 -0
- package/addon-dist/lib/main/mcp/tools/importSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/importSite.js +149 -0
- package/addon-dist/lib/main/mcp/tools/index.d.ts +26 -0
- package/addon-dist/lib/main/mcp/tools/index.js +117 -0
- package/addon-dist/lib/main/mcp/tools/listBlueprints.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/listBlueprints.js +54 -0
- package/addon-dist/lib/main/mcp/tools/listServices.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/listServices.js +112 -0
- package/addon-dist/lib/main/mcp/tools/listSites.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/listSites.js +62 -0
- package/addon-dist/lib/main/mcp/tools/openAdminer.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/openAdminer.js +59 -0
- package/addon-dist/lib/main/mcp/tools/openSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/openSite.js +62 -0
- package/addon-dist/lib/main/mcp/tools/renameSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/renameSite.js +70 -0
- package/addon-dist/lib/main/mcp/tools/restartSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/restartSite.js +56 -0
- package/addon-dist/lib/main/mcp/tools/saveBlueprint.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/saveBlueprint.js +89 -0
- package/addon-dist/lib/main/mcp/tools/startSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/startSite.js +54 -0
- package/addon-dist/lib/main/mcp/tools/stopSite.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/stopSite.js +54 -0
- package/addon-dist/lib/main/mcp/tools/toggleXdebug.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/toggleXdebug.js +69 -0
- package/addon-dist/lib/main/mcp/tools/trustSsl.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/trustSsl.js +59 -0
- package/addon-dist/lib/main/mcp/tools/wpCli.d.ts +7 -0
- package/addon-dist/lib/main/mcp/tools/wpCli.js +110 -0
- package/addon-dist/lib/main.d.ts +1 -0
- package/addon-dist/lib/main.js +10 -0
- package/addon-dist/lib/renderer/index.d.ts +7 -0
- package/addon-dist/lib/renderer/index.js +479 -0
- package/addon-dist/package.json +73 -0
- package/bin/lwp.js +10 -0
- package/lib/bootstrap/index.d.ts +98 -0
- package/lib/bootstrap/index.js +493 -0
- package/lib/bootstrap/paths.d.ts +28 -0
- package/lib/bootstrap/paths.js +96 -0
- package/lib/client/GraphQLClient.d.ts +38 -0
- package/lib/client/GraphQLClient.js +71 -0
- package/lib/client/index.d.ts +4 -0
- package/lib/client/index.js +10 -0
- package/lib/formatters/index.d.ts +75 -0
- package/lib/formatters/index.js +139 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +1173 -0
- package/package.json +72 -0
|
@@ -0,0 +1,2808 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local MCP Server - stdio transport
|
|
4
|
+
*
|
|
5
|
+
* This script acts as an MCP server using stdio transport,
|
|
6
|
+
* bridging to Local's GraphQL API for site management.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const readline = require('readline');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const https = require('https');
|
|
15
|
+
const { URL } = require('url');
|
|
16
|
+
|
|
17
|
+
// Get Local's GraphQL connection info
|
|
18
|
+
function getGraphQLConnectionInfo() {
|
|
19
|
+
const platform = os.platform();
|
|
20
|
+
let configPath;
|
|
21
|
+
|
|
22
|
+
switch (platform) {
|
|
23
|
+
case 'darwin':
|
|
24
|
+
configPath = path.join(os.homedir(), 'Library', 'Application Support', 'Local', 'graphql-connection-info.json');
|
|
25
|
+
break;
|
|
26
|
+
case 'win32':
|
|
27
|
+
configPath = path.join(process.env.APPDATA || '', 'Local', 'graphql-connection-info.json');
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
configPath = path.join(os.homedir(), '.config', 'Local', 'graphql-connection-info.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Make GraphQL request to Local (compatible with Node.js < 18)
|
|
42
|
+
async function graphqlRequest(query, variables = {}) {
|
|
43
|
+
const info = getGraphQLConnectionInfo();
|
|
44
|
+
if (!info) {
|
|
45
|
+
throw new Error('Local is not running or GraphQL connection info not found');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const body = JSON.stringify({ query, variables });
|
|
49
|
+
const url = new URL(info.url);
|
|
50
|
+
const isHttps = url.protocol === 'https:';
|
|
51
|
+
const httpModule = isHttps ? https : http;
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const req = httpModule.request({
|
|
55
|
+
hostname: url.hostname,
|
|
56
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
57
|
+
path: url.pathname,
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'Content-Length': Buffer.byteLength(body),
|
|
62
|
+
'Authorization': `Bearer ${info.authToken}`,
|
|
63
|
+
},
|
|
64
|
+
}, (res) => {
|
|
65
|
+
let data = '';
|
|
66
|
+
res.on('data', chunk => data += chunk);
|
|
67
|
+
res.on('end', () => {
|
|
68
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
69
|
+
reject(new Error(`GraphQL request failed: ${res.statusCode}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const result = JSON.parse(data);
|
|
74
|
+
if (result.errors) {
|
|
75
|
+
reject(new Error(result.errors[0].message));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
resolve(result.data);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
reject(new Error(`Failed to parse response: ${err.message}`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
req.on('error', reject);
|
|
86
|
+
req.write(body);
|
|
87
|
+
req.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Tool definitions
|
|
92
|
+
const tools = [
|
|
93
|
+
{
|
|
94
|
+
name: 'list_sites',
|
|
95
|
+
description: 'List all WordPress sites in Local with their name, ID, status, and domain',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
status: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
enum: ['all', 'running', 'stopped'],
|
|
102
|
+
description: 'Filter sites by status (default: all)',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'get_site',
|
|
109
|
+
description: 'Get detailed information about a WordPress site',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
site: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Site name or ID',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['site'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'start_site',
|
|
123
|
+
description: 'Start a WordPress site',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
site: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Site name or ID',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ['site'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'stop_site',
|
|
137
|
+
description: 'Stop a running WordPress site',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
site: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'Site name or ID',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
required: ['site'],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'restart_site',
|
|
151
|
+
description: 'Restart a WordPress site (stops then starts it)',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
site: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'Site name or ID',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['site'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'wp_cli',
|
|
165
|
+
description: 'Run a WP-CLI command against a WordPress site. Site will be auto-started if not running.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
site: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'Site name or ID',
|
|
172
|
+
},
|
|
173
|
+
command: {
|
|
174
|
+
type: 'array',
|
|
175
|
+
items: { type: 'string' },
|
|
176
|
+
description: 'WP-CLI command as array, e.g. ["plugin", "list", "--format=json"]',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: ['site', 'command'],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'create_site',
|
|
184
|
+
description: 'Create a new WordPress site in Local. The site will be created and started automatically. Optionally create from an existing blueprint.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
name: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Site name (required)',
|
|
191
|
+
},
|
|
192
|
+
php_version: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
description: 'PHP version (e.g., "8.2.10"). Uses Local default if not specified.',
|
|
195
|
+
},
|
|
196
|
+
blueprint: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
description: 'Blueprint name to create the site from. Use list_blueprints to see available blueprints.',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
required: ['name'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'delete_site',
|
|
206
|
+
description: 'Delete a WordPress site. Requires confirm=true as a safety measure.',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
site: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Site name or ID',
|
|
213
|
+
},
|
|
214
|
+
confirm: {
|
|
215
|
+
type: 'boolean',
|
|
216
|
+
description: 'Must be true to confirm deletion',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
required: ['site', 'confirm'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'get_local_info',
|
|
224
|
+
description: 'Get information about Local installation including version, platform, and available tools',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'open_site',
|
|
232
|
+
description: 'Open a WordPress site in the default browser. Site will be auto-started if not running.',
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
site: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'Site name or ID',
|
|
239
|
+
},
|
|
240
|
+
path: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'Path to open (default: /, use /wp-admin for admin panel)',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ['site'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'clone_site',
|
|
250
|
+
description: 'Clone an existing WordPress site to create a copy with a new name',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
site: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: 'Site name or ID to clone',
|
|
257
|
+
},
|
|
258
|
+
new_name: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: 'Name for the cloned site',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ['site', 'new_name'],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'export_site',
|
|
268
|
+
description: 'Export a WordPress site to a zip file. Site will be auto-started if not running.',
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
site: {
|
|
273
|
+
type: 'string',
|
|
274
|
+
description: 'Site name or ID to export',
|
|
275
|
+
},
|
|
276
|
+
output_path: {
|
|
277
|
+
type: 'string',
|
|
278
|
+
description: 'Output directory path (default: ~/Downloads)',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
required: ['site'],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'list_blueprints',
|
|
286
|
+
description: 'List all available site blueprints (templates)',
|
|
287
|
+
inputSchema: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'save_blueprint',
|
|
294
|
+
description: 'Save a site as a blueprint (template) for creating new sites. Site will be auto-started if not running.',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
site: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: 'Site name or ID to save as blueprint',
|
|
301
|
+
},
|
|
302
|
+
name: {
|
|
303
|
+
type: 'string',
|
|
304
|
+
description: 'Name for the blueprint',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
required: ['site', 'name'],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
// Phase 8: WordPress Development Tools
|
|
311
|
+
{
|
|
312
|
+
name: 'export_database',
|
|
313
|
+
description: 'Export a site database to a SQL file. Site will be auto-started if not running.',
|
|
314
|
+
inputSchema: {
|
|
315
|
+
type: 'object',
|
|
316
|
+
properties: {
|
|
317
|
+
site: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Site name or ID',
|
|
320
|
+
},
|
|
321
|
+
output_path: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
description: 'Output file path (default: ~/Downloads/<site-name>.sql)',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ['site'],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'import_database',
|
|
331
|
+
description: 'Import a SQL file into a site database. Site will be auto-started if not running.',
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
site: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
description: 'Site name or ID',
|
|
338
|
+
},
|
|
339
|
+
sql_path: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
description: 'Path to the SQL file to import',
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
required: ['site', 'sql_path'],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'open_adminer',
|
|
349
|
+
description: 'Open Adminer database management UI for a site. Site will be auto-started if not running.',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
site: {
|
|
354
|
+
type: 'string',
|
|
355
|
+
description: 'Site name or ID',
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
required: ['site'],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'trust_ssl',
|
|
363
|
+
description: 'Trust the SSL certificate for a site (may require admin password)',
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
site: {
|
|
368
|
+
type: 'string',
|
|
369
|
+
description: 'Site name or ID',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
required: ['site'],
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: 'rename_site',
|
|
377
|
+
description: 'Rename a WordPress site',
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: 'object',
|
|
380
|
+
properties: {
|
|
381
|
+
site: {
|
|
382
|
+
type: 'string',
|
|
383
|
+
description: 'Current site name or ID',
|
|
384
|
+
},
|
|
385
|
+
new_name: {
|
|
386
|
+
type: 'string',
|
|
387
|
+
description: 'New name for the site',
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
required: ['site', 'new_name'],
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'change_php_version',
|
|
395
|
+
description: 'Change the PHP version for a site',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
site: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
description: 'Site name or ID',
|
|
402
|
+
},
|
|
403
|
+
php_version: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: 'Target PHP version (e.g., "8.2.10", "8.1.27")',
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
required: ['site', 'php_version'],
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'import_site',
|
|
413
|
+
description: 'Import a WordPress site from a zip file',
|
|
414
|
+
inputSchema: {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: {
|
|
417
|
+
zip_path: {
|
|
418
|
+
type: 'string',
|
|
419
|
+
description: 'Path to the zip file to import',
|
|
420
|
+
},
|
|
421
|
+
site_name: {
|
|
422
|
+
type: 'string',
|
|
423
|
+
description: 'Name for the imported site (optional)',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
required: ['zip_path'],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
// Phase 9: Site Configuration & Dev Tools
|
|
430
|
+
{
|
|
431
|
+
name: 'toggle_xdebug',
|
|
432
|
+
description: 'Enable or disable Xdebug for a site',
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
site: {
|
|
437
|
+
type: 'string',
|
|
438
|
+
description: 'Site name or ID',
|
|
439
|
+
},
|
|
440
|
+
enabled: {
|
|
441
|
+
type: 'boolean',
|
|
442
|
+
description: 'True to enable, false to disable',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ['site', 'enabled'],
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: 'get_site_logs',
|
|
450
|
+
description: 'Get log file contents for a site',
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
site: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: 'Site name or ID',
|
|
457
|
+
},
|
|
458
|
+
log_type: {
|
|
459
|
+
type: 'string',
|
|
460
|
+
enum: ['php', 'nginx', 'mysql', 'all'],
|
|
461
|
+
description: 'Type of logs to retrieve (default: php)',
|
|
462
|
+
},
|
|
463
|
+
lines: {
|
|
464
|
+
type: 'number',
|
|
465
|
+
description: 'Number of lines to return (default: 100)',
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
required: ['site'],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'list_services',
|
|
473
|
+
description: 'List available service versions (PHP, MySQL, Nginx)',
|
|
474
|
+
inputSchema: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
type: {
|
|
478
|
+
type: 'string',
|
|
479
|
+
enum: ['php', 'database', 'webserver', 'all'],
|
|
480
|
+
description: 'Filter by service type (default: all)',
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
// Phase 10: Cloud Backups
|
|
486
|
+
{
|
|
487
|
+
name: 'backup_status',
|
|
488
|
+
description: 'Check if cloud backups are available and authenticated. Shows Dropbox and Google Drive status.',
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'list_backups',
|
|
496
|
+
description: 'List all cloud backups for a site from Dropbox or Google Drive',
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: 'object',
|
|
499
|
+
properties: {
|
|
500
|
+
site: {
|
|
501
|
+
type: 'string',
|
|
502
|
+
description: 'Site name or ID',
|
|
503
|
+
},
|
|
504
|
+
provider: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
enum: ['dropbox', 'googleDrive'],
|
|
507
|
+
description: 'Cloud storage provider',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
required: ['site', 'provider'],
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'create_backup',
|
|
515
|
+
description: 'Create a backup of a site to cloud storage (Dropbox or Google Drive). Site will be auto-started if not running.',
|
|
516
|
+
inputSchema: {
|
|
517
|
+
type: 'object',
|
|
518
|
+
properties: {
|
|
519
|
+
site: {
|
|
520
|
+
type: 'string',
|
|
521
|
+
description: 'Site name or ID',
|
|
522
|
+
},
|
|
523
|
+
provider: {
|
|
524
|
+
type: 'string',
|
|
525
|
+
enum: ['dropbox', 'googleDrive'],
|
|
526
|
+
description: 'Cloud storage provider',
|
|
527
|
+
},
|
|
528
|
+
note: {
|
|
529
|
+
type: 'string',
|
|
530
|
+
description: 'Optional note/description for the backup',
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
required: ['site', 'provider'],
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: 'restore_backup',
|
|
538
|
+
description: 'Restore a site from a cloud backup. Site will be auto-started if not running. WARNING: This will overwrite current site files and database.',
|
|
539
|
+
inputSchema: {
|
|
540
|
+
type: 'object',
|
|
541
|
+
properties: {
|
|
542
|
+
site: {
|
|
543
|
+
type: 'string',
|
|
544
|
+
description: 'Site name or ID',
|
|
545
|
+
},
|
|
546
|
+
provider: {
|
|
547
|
+
type: 'string',
|
|
548
|
+
enum: ['dropbox', 'googleDrive'],
|
|
549
|
+
description: 'Cloud storage provider',
|
|
550
|
+
},
|
|
551
|
+
snapshot_id: {
|
|
552
|
+
type: 'string',
|
|
553
|
+
description: 'Snapshot ID to restore (from list_backups)',
|
|
554
|
+
},
|
|
555
|
+
confirm: {
|
|
556
|
+
type: 'boolean',
|
|
557
|
+
description: 'Must be true to confirm restoration',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
required: ['site', 'provider', 'snapshot_id', 'confirm'],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'delete_backup',
|
|
565
|
+
description: 'Delete a backup from cloud storage',
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: 'object',
|
|
568
|
+
properties: {
|
|
569
|
+
site: {
|
|
570
|
+
type: 'string',
|
|
571
|
+
description: 'Site name or ID',
|
|
572
|
+
},
|
|
573
|
+
provider: {
|
|
574
|
+
type: 'string',
|
|
575
|
+
enum: ['dropbox', 'googleDrive'],
|
|
576
|
+
description: 'Cloud storage provider',
|
|
577
|
+
},
|
|
578
|
+
snapshot_id: {
|
|
579
|
+
type: 'string',
|
|
580
|
+
description: 'Snapshot ID to delete (from list_backups)',
|
|
581
|
+
},
|
|
582
|
+
confirm: {
|
|
583
|
+
type: 'boolean',
|
|
584
|
+
description: 'Must be true to confirm deletion',
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
required: ['site', 'provider', 'snapshot_id', 'confirm'],
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: 'download_backup',
|
|
592
|
+
description: 'Download a backup as a ZIP file to the Downloads folder',
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: 'object',
|
|
595
|
+
properties: {
|
|
596
|
+
site: {
|
|
597
|
+
type: 'string',
|
|
598
|
+
description: 'Site name or ID',
|
|
599
|
+
},
|
|
600
|
+
provider: {
|
|
601
|
+
type: 'string',
|
|
602
|
+
enum: ['dropbox', 'googleDrive'],
|
|
603
|
+
description: 'Cloud storage provider',
|
|
604
|
+
},
|
|
605
|
+
snapshot_id: {
|
|
606
|
+
type: 'string',
|
|
607
|
+
description: 'Snapshot ID to download (from list_backups)',
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
required: ['site', 'provider', 'snapshot_id'],
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: 'edit_backup_note',
|
|
615
|
+
description: 'Update the note/description for a backup',
|
|
616
|
+
inputSchema: {
|
|
617
|
+
type: 'object',
|
|
618
|
+
properties: {
|
|
619
|
+
site: {
|
|
620
|
+
type: 'string',
|
|
621
|
+
description: 'Site name or ID',
|
|
622
|
+
},
|
|
623
|
+
provider: {
|
|
624
|
+
type: 'string',
|
|
625
|
+
enum: ['dropbox', 'googleDrive'],
|
|
626
|
+
description: 'Cloud storage provider',
|
|
627
|
+
},
|
|
628
|
+
snapshot_id: {
|
|
629
|
+
type: 'string',
|
|
630
|
+
description: 'Snapshot ID to edit (from list_backups)',
|
|
631
|
+
},
|
|
632
|
+
note: {
|
|
633
|
+
type: 'string',
|
|
634
|
+
description: 'New note/description for the backup',
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
required: ['site', 'provider', 'snapshot_id', 'note'],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
// Phase 11: WP Engine Connect
|
|
641
|
+
{
|
|
642
|
+
name: 'wpe_status',
|
|
643
|
+
description: 'Check WP Engine authentication status',
|
|
644
|
+
inputSchema: {
|
|
645
|
+
type: 'object',
|
|
646
|
+
properties: {},
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: 'wpe_authenticate',
|
|
651
|
+
description: 'Authenticate with WP Engine. Opens browser for OAuth login.',
|
|
652
|
+
inputSchema: {
|
|
653
|
+
type: 'object',
|
|
654
|
+
properties: {},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: 'wpe_logout',
|
|
659
|
+
description: 'Logout from WP Engine and clear stored credentials',
|
|
660
|
+
inputSchema: {
|
|
661
|
+
type: 'object',
|
|
662
|
+
properties: {},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'list_wpe_sites',
|
|
667
|
+
description: 'List all sites from your WP Engine account. Requires authentication.',
|
|
668
|
+
inputSchema: {
|
|
669
|
+
type: 'object',
|
|
670
|
+
properties: {
|
|
671
|
+
account_id: {
|
|
672
|
+
type: 'string',
|
|
673
|
+
description: 'Filter by specific WP Engine account ID (optional)',
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
// Phase 11b: Site Linking
|
|
679
|
+
{
|
|
680
|
+
name: 'get_wpe_link',
|
|
681
|
+
description: 'Get WP Engine connection details for a local site. Shows if the site is linked to a WPE environment and sync capabilities.',
|
|
682
|
+
inputSchema: {
|
|
683
|
+
type: 'object',
|
|
684
|
+
properties: {
|
|
685
|
+
site: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: 'Site name or ID',
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
required: ['site'],
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
// Phase 11c: Sync Operations
|
|
694
|
+
{
|
|
695
|
+
name: 'push_to_wpe',
|
|
696
|
+
description: 'Push local site files and/or database to WP Engine. Site will be auto-started if not running. Requires confirm=true to prevent accidental overwrites.',
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: {
|
|
700
|
+
site: {
|
|
701
|
+
type: 'string',
|
|
702
|
+
description: 'Site name or ID',
|
|
703
|
+
},
|
|
704
|
+
include_database: {
|
|
705
|
+
type: 'boolean',
|
|
706
|
+
description: 'Include database in push (default: false)',
|
|
707
|
+
},
|
|
708
|
+
confirm: {
|
|
709
|
+
type: 'boolean',
|
|
710
|
+
description: 'Must be true to confirm push operation (required for safety)',
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
required: ['site', 'confirm'],
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: 'pull_from_wpe',
|
|
718
|
+
description: 'Pull files and/or database from WP Engine to local site. Site will be auto-started if not running. Requires confirm=true to prevent accidental overwrites.',
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
properties: {
|
|
722
|
+
site: {
|
|
723
|
+
type: 'string',
|
|
724
|
+
description: 'Site name or ID',
|
|
725
|
+
},
|
|
726
|
+
include_database: {
|
|
727
|
+
type: 'boolean',
|
|
728
|
+
description: 'Include database in pull (default: false)',
|
|
729
|
+
},
|
|
730
|
+
confirm: {
|
|
731
|
+
type: 'boolean',
|
|
732
|
+
description: 'Must be true to confirm pull operation (required for safety)',
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
required: ['site', 'confirm'],
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
name: 'get_sync_history',
|
|
740
|
+
description: 'Get sync history (push/pull operations) for a local site.',
|
|
741
|
+
inputSchema: {
|
|
742
|
+
type: 'object',
|
|
743
|
+
properties: {
|
|
744
|
+
site: {
|
|
745
|
+
type: 'string',
|
|
746
|
+
description: 'Site name or ID',
|
|
747
|
+
},
|
|
748
|
+
limit: {
|
|
749
|
+
type: 'number',
|
|
750
|
+
description: 'Maximum number of history events to return (default: 10)',
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
required: ['site'],
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: 'get_site_changes',
|
|
758
|
+
description: 'Preview what files have changed between local site and WP Engine. Site will be auto-started if not running. Uses Magic Sync dry-run comparison.',
|
|
759
|
+
inputSchema: {
|
|
760
|
+
type: 'object',
|
|
761
|
+
properties: {
|
|
762
|
+
site: {
|
|
763
|
+
type: 'string',
|
|
764
|
+
description: 'Site name or ID',
|
|
765
|
+
},
|
|
766
|
+
direction: {
|
|
767
|
+
type: 'string',
|
|
768
|
+
enum: ['push', 'pull'],
|
|
769
|
+
description: 'Direction of comparison: "push" shows local changes to upload, "pull" shows remote changes to download (default: push)',
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
required: ['site'],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
|
|
777
|
+
// Find site by name or ID
|
|
778
|
+
async function findSite(siteIdentifier) {
|
|
779
|
+
const data = await graphqlRequest(`
|
|
780
|
+
query {
|
|
781
|
+
sites {
|
|
782
|
+
id
|
|
783
|
+
name
|
|
784
|
+
status
|
|
785
|
+
domain
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
`);
|
|
789
|
+
|
|
790
|
+
const sites = data.sites || [];
|
|
791
|
+
const searchLower = siteIdentifier.toLowerCase();
|
|
792
|
+
|
|
793
|
+
// Try exact ID match first
|
|
794
|
+
let site = sites.find(s => s.id === siteIdentifier);
|
|
795
|
+
if (site) return site;
|
|
796
|
+
|
|
797
|
+
// Try exact name match
|
|
798
|
+
site = sites.find(s => s.name.toLowerCase() === searchLower);
|
|
799
|
+
if (site) return site;
|
|
800
|
+
|
|
801
|
+
// Try partial name match
|
|
802
|
+
site = sites.find(s => s.name.toLowerCase().includes(searchLower));
|
|
803
|
+
if (site) return site;
|
|
804
|
+
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Security: Validate snapshot ID format (restic uses hex hashes)
|
|
809
|
+
function isValidSnapshotId(snapshotId) {
|
|
810
|
+
if (!snapshotId || typeof snapshotId !== 'string') return false;
|
|
811
|
+
// Restic snapshot IDs are hex strings, 8-64 characters (short prefix or full hash)
|
|
812
|
+
return /^[a-f0-9]{8,64}$/i.test(snapshotId);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Security: Validate SQL file path
|
|
816
|
+
function isValidSqlPath(sqlPath) {
|
|
817
|
+
if (!sqlPath || typeof sqlPath !== 'string') return false;
|
|
818
|
+
const path = require('path');
|
|
819
|
+
const fs = require('fs');
|
|
820
|
+
const resolvedPath = path.resolve(sqlPath);
|
|
821
|
+
// Check path doesn't contain traversal and ends with .sql
|
|
822
|
+
return resolvedPath.endsWith('.sql') && !sqlPath.includes('..') && fs.existsSync(resolvedPath);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Performance: Timeout wrapper for long-running operations
|
|
826
|
+
async function withTimeout(promise, timeoutMs, operationName) {
|
|
827
|
+
let timeoutId;
|
|
828
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
829
|
+
timeoutId = setTimeout(() => {
|
|
830
|
+
reject(new Error(`${operationName} timed out after ${timeoutMs / 1000} seconds`));
|
|
831
|
+
}, timeoutMs);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
const result = await Promise.race([promise, timeoutPromise]);
|
|
836
|
+
clearTimeout(timeoutId);
|
|
837
|
+
return result;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
clearTimeout(timeoutId);
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Timeout constants (in milliseconds)
|
|
845
|
+
const TIMEOUT_SYNC_OPERATION = 300000; // 5 minutes for push/pull
|
|
846
|
+
const TIMEOUT_BACKUP_OPERATION = 600000; // 10 minutes for backup operations
|
|
847
|
+
|
|
848
|
+
// Helper: Sleep for specified milliseconds
|
|
849
|
+
function sleep(ms) {
|
|
850
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Auto-start site if not running (used by tools that require a running site)
|
|
854
|
+
async function ensureSiteRunning(site) {
|
|
855
|
+
// Check current status
|
|
856
|
+
const data = await graphqlRequest(`
|
|
857
|
+
query($id: ID!) {
|
|
858
|
+
site(id: $id) {
|
|
859
|
+
id
|
|
860
|
+
status
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
`, { id: site.id });
|
|
864
|
+
|
|
865
|
+
const currentStatus = data.site?.status;
|
|
866
|
+
|
|
867
|
+
if (currentStatus === 'running') {
|
|
868
|
+
return { wasStarted: false };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Start the site
|
|
872
|
+
await graphqlRequest(`
|
|
873
|
+
mutation($id: ID!) {
|
|
874
|
+
startSite(id: $id) {
|
|
875
|
+
id
|
|
876
|
+
status
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
`, { id: site.id });
|
|
880
|
+
|
|
881
|
+
// Brief wait for services to be ready
|
|
882
|
+
await sleep(2000);
|
|
883
|
+
|
|
884
|
+
return { wasStarted: true };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Tool handlers
|
|
888
|
+
async function handleTool(name, args) {
|
|
889
|
+
switch (name) {
|
|
890
|
+
case 'list_sites': {
|
|
891
|
+
const data = await graphqlRequest(`
|
|
892
|
+
query {
|
|
893
|
+
sites {
|
|
894
|
+
id
|
|
895
|
+
name
|
|
896
|
+
status
|
|
897
|
+
domain
|
|
898
|
+
hostConnections {
|
|
899
|
+
hostId
|
|
900
|
+
remoteSiteId
|
|
901
|
+
remoteSiteEnv
|
|
902
|
+
accountId
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
`);
|
|
907
|
+
|
|
908
|
+
let sites = data.sites || [];
|
|
909
|
+
|
|
910
|
+
if (args.status === 'running') {
|
|
911
|
+
sites = sites.filter(s => s.status === 'running');
|
|
912
|
+
} else if (args.status === 'stopped') {
|
|
913
|
+
sites = sites.filter(s => s.status !== 'running');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Transform hostConnections to wpeConnection for easier reading
|
|
917
|
+
sites = sites.map(site => {
|
|
918
|
+
const wpeConnection = site.hostConnections?.find(c => c.hostId === 'wpe');
|
|
919
|
+
return {
|
|
920
|
+
...site,
|
|
921
|
+
hostConnections: undefined, // Remove raw hostConnections
|
|
922
|
+
wpeConnection: wpeConnection ? {
|
|
923
|
+
remoteSiteId: wpeConnection.remoteSiteId,
|
|
924
|
+
environment: wpeConnection.remoteSiteEnv || null,
|
|
925
|
+
// Note: For full install name and capabilities, use get_wpe_link
|
|
926
|
+
canPushPull: true, // All WPE-connected sites can push/pull
|
|
927
|
+
} : null,
|
|
928
|
+
};
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
content: [{
|
|
933
|
+
type: 'text',
|
|
934
|
+
text: JSON.stringify({ sites, count: sites.length }, null, 2),
|
|
935
|
+
}],
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
case 'get_site': {
|
|
940
|
+
const site = await findSite(args.site);
|
|
941
|
+
if (!site) {
|
|
942
|
+
return {
|
|
943
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
944
|
+
isError: true,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
content: [{ type: 'text', text: JSON.stringify(site, null, 2) }],
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
case 'start_site': {
|
|
953
|
+
const site = await findSite(args.site);
|
|
954
|
+
if (!site) {
|
|
955
|
+
return {
|
|
956
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
957
|
+
isError: true,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
await graphqlRequest(`
|
|
962
|
+
mutation($id: ID!) {
|
|
963
|
+
startSite(id: $id) {
|
|
964
|
+
id
|
|
965
|
+
status
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
`, { id: site.id });
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
content: [{ type: 'text', text: `Started site: ${site.name}` }],
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
case 'stop_site': {
|
|
976
|
+
const site = await findSite(args.site);
|
|
977
|
+
if (!site) {
|
|
978
|
+
return {
|
|
979
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
980
|
+
isError: true,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
await graphqlRequest(`
|
|
985
|
+
mutation($id: ID!) {
|
|
986
|
+
stopSite(id: $id) {
|
|
987
|
+
id
|
|
988
|
+
status
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
`, { id: site.id });
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
content: [{ type: 'text', text: `Stopped site: ${site.name}` }],
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
case 'restart_site': {
|
|
999
|
+
const site = await findSite(args.site);
|
|
1000
|
+
if (!site) {
|
|
1001
|
+
return {
|
|
1002
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1003
|
+
isError: true,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
await graphqlRequest(`
|
|
1008
|
+
mutation($id: ID!) {
|
|
1009
|
+
restartSite(id: $id) {
|
|
1010
|
+
id
|
|
1011
|
+
status
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
`, { id: site.id });
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
content: [{ type: 'text', text: `Restarted site: ${site.name}` }],
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
case 'wp_cli': {
|
|
1022
|
+
// Security: Block dangerous WP-CLI commands that could execute arbitrary code
|
|
1023
|
+
const BLOCKED_WP_COMMANDS = [
|
|
1024
|
+
'eval',
|
|
1025
|
+
'eval-file',
|
|
1026
|
+
'shell',
|
|
1027
|
+
'db query',
|
|
1028
|
+
'db cli',
|
|
1029
|
+
];
|
|
1030
|
+
|
|
1031
|
+
const commandStr = (args.command || []).join(' ').toLowerCase();
|
|
1032
|
+
for (const blocked of BLOCKED_WP_COMMANDS) {
|
|
1033
|
+
if (commandStr.includes(blocked)) {
|
|
1034
|
+
return {
|
|
1035
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1036
|
+
error: `Command '${blocked}' is blocked for security reasons. Use Local's terminal for shell access.`,
|
|
1037
|
+
}) }],
|
|
1038
|
+
isError: true,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const site = await findSite(args.site);
|
|
1044
|
+
if (!site) {
|
|
1045
|
+
return {
|
|
1046
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1047
|
+
isError: true,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Auto-start site if not running
|
|
1052
|
+
const { wasStarted } = await ensureSiteRunning(site);
|
|
1053
|
+
|
|
1054
|
+
const data = await graphqlRequest(`
|
|
1055
|
+
mutation($input: WpCliInput!) {
|
|
1056
|
+
wpCli(input: $input) {
|
|
1057
|
+
success
|
|
1058
|
+
output
|
|
1059
|
+
error
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
`, {
|
|
1063
|
+
input: {
|
|
1064
|
+
siteId: site.id,
|
|
1065
|
+
args: args.command,
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const result = data.wpCli;
|
|
1070
|
+
if (!result.success) {
|
|
1071
|
+
return {
|
|
1072
|
+
content: [{ type: 'text', text: `WP-CLI error: ${result.error}` }],
|
|
1073
|
+
isError: true,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return {
|
|
1078
|
+
content: [{ type: 'text', text: result.output || 'Command completed successfully' }],
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
case 'create_site': {
|
|
1083
|
+
if (!args.name) {
|
|
1084
|
+
return {
|
|
1085
|
+
content: [{ type: 'text', text: 'Error: name is required' }],
|
|
1086
|
+
isError: true,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const input = {
|
|
1091
|
+
name: args.name,
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
if (args.php_version) {
|
|
1095
|
+
input.phpVersion = args.php_version;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (args.blueprint) {
|
|
1099
|
+
input.blueprint = args.blueprint;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Debug: log the input being sent
|
|
1103
|
+
console.error('[MCP stdio] create_site input:', JSON.stringify(input));
|
|
1104
|
+
console.error('[MCP stdio] args.blueprint value:', args.blueprint);
|
|
1105
|
+
|
|
1106
|
+
const data = await graphqlRequest(`
|
|
1107
|
+
mutation($input: CreateSiteInput!) {
|
|
1108
|
+
createSite(input: $input) {
|
|
1109
|
+
success
|
|
1110
|
+
error
|
|
1111
|
+
siteId
|
|
1112
|
+
siteName
|
|
1113
|
+
siteDomain
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
`, { input });
|
|
1117
|
+
|
|
1118
|
+
const result = data.createSite;
|
|
1119
|
+
if (!result.success) {
|
|
1120
|
+
return {
|
|
1121
|
+
content: [{ type: 'text', text: `Failed to create site: ${result.error}` }],
|
|
1122
|
+
isError: true,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const fromBlueprint = args.blueprint ? ` (from blueprint: ${args.blueprint})` : '';
|
|
1127
|
+
return {
|
|
1128
|
+
content: [{
|
|
1129
|
+
type: 'text',
|
|
1130
|
+
text: `Created site: ${result.siteName}${fromBlueprint}\nDomain: ${result.siteDomain}\nID: ${result.siteId}\nAdmin: admin / password\n\nThe site is being provisioned and will start automatically.`,
|
|
1131
|
+
}],
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
case 'delete_site': {
|
|
1136
|
+
if (!args.confirm) {
|
|
1137
|
+
return {
|
|
1138
|
+
content: [{ type: 'text', text: 'Deletion not confirmed. Set confirm=true to delete the site.' }],
|
|
1139
|
+
isError: true,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const site = await findSite(args.site);
|
|
1144
|
+
if (!site) {
|
|
1145
|
+
return {
|
|
1146
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1147
|
+
isError: true,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
await graphqlRequest(`
|
|
1152
|
+
mutation($input: DeleteSiteInput!) {
|
|
1153
|
+
deleteSite(input: $input) {
|
|
1154
|
+
success
|
|
1155
|
+
error
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
`, {
|
|
1159
|
+
input: {
|
|
1160
|
+
id: site.id,
|
|
1161
|
+
trashFiles: true,
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
return {
|
|
1166
|
+
content: [{ type: 'text', text: `Deleted site: ${site.name}` }],
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
case 'get_local_info': {
|
|
1171
|
+
const data = await graphqlRequest(`
|
|
1172
|
+
query {
|
|
1173
|
+
sites {
|
|
1174
|
+
id
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
`);
|
|
1178
|
+
|
|
1179
|
+
const siteCount = data.sites?.length || 0;
|
|
1180
|
+
const platform = os.platform();
|
|
1181
|
+
const platformName = platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : 'Linux';
|
|
1182
|
+
|
|
1183
|
+
const info = {
|
|
1184
|
+
mcpServerVersion: '1.0.0',
|
|
1185
|
+
platform: platformName,
|
|
1186
|
+
arch: os.arch(),
|
|
1187
|
+
siteCount: siteCount,
|
|
1188
|
+
availableTools: tools.map(t => t.name),
|
|
1189
|
+
transport: 'stdio',
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
return {
|
|
1193
|
+
content: [{ type: 'text', text: JSON.stringify(info, null, 2) }],
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
case 'open_site': {
|
|
1198
|
+
const site = await findSite(args.site);
|
|
1199
|
+
if (!site) {
|
|
1200
|
+
return {
|
|
1201
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1202
|
+
isError: true,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Auto-start site if not running
|
|
1207
|
+
await ensureSiteRunning(site);
|
|
1208
|
+
|
|
1209
|
+
const sitePath = args.path || '/';
|
|
1210
|
+
const data = await graphqlRequest(`
|
|
1211
|
+
mutation($input: OpenSiteInput!) {
|
|
1212
|
+
openSite(input: $input) {
|
|
1213
|
+
success
|
|
1214
|
+
error
|
|
1215
|
+
url
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
`, {
|
|
1219
|
+
input: {
|
|
1220
|
+
siteId: site.id,
|
|
1221
|
+
path: sitePath,
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
const result = data.openSite;
|
|
1226
|
+
if (!result.success) {
|
|
1227
|
+
return {
|
|
1228
|
+
content: [{ type: 'text', text: `Failed to open site: ${result.error}` }],
|
|
1229
|
+
isError: true,
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return {
|
|
1234
|
+
content: [{ type: 'text', text: `Opened ${site.name} in browser: ${result.url}` }],
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
case 'clone_site': {
|
|
1239
|
+
const site = await findSite(args.site);
|
|
1240
|
+
if (!site) {
|
|
1241
|
+
return {
|
|
1242
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1243
|
+
isError: true,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (!args.new_name) {
|
|
1248
|
+
return {
|
|
1249
|
+
content: [{ type: 'text', text: 'Error: new_name is required' }],
|
|
1250
|
+
isError: true,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const data = await graphqlRequest(`
|
|
1255
|
+
mutation($input: CloneSiteInput!) {
|
|
1256
|
+
cloneSite(input: $input) {
|
|
1257
|
+
success
|
|
1258
|
+
error
|
|
1259
|
+
newSiteId
|
|
1260
|
+
newSiteName
|
|
1261
|
+
newSiteDomain
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
`, {
|
|
1265
|
+
input: {
|
|
1266
|
+
siteId: site.id,
|
|
1267
|
+
newName: args.new_name,
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
const result = data.cloneSite;
|
|
1272
|
+
if (!result.success) {
|
|
1273
|
+
return {
|
|
1274
|
+
content: [{ type: 'text', text: `Failed to clone site: ${result.error}` }],
|
|
1275
|
+
isError: true,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return {
|
|
1280
|
+
content: [{
|
|
1281
|
+
type: 'text',
|
|
1282
|
+
text: `Cloned ${site.name} to ${result.newSiteName}\nNew site ID: ${result.newSiteId}\nNew domain: ${result.newSiteDomain}`,
|
|
1283
|
+
}],
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
case 'export_site': {
|
|
1288
|
+
const site = await findSite(args.site);
|
|
1289
|
+
if (!site) {
|
|
1290
|
+
return {
|
|
1291
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1292
|
+
isError: true,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Auto-start site if not running
|
|
1297
|
+
await ensureSiteRunning(site);
|
|
1298
|
+
|
|
1299
|
+
const data = await graphqlRequest(`
|
|
1300
|
+
mutation($input: ExportSiteInput!) {
|
|
1301
|
+
exportSite(input: $input) {
|
|
1302
|
+
success
|
|
1303
|
+
error
|
|
1304
|
+
exportPath
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
`, {
|
|
1308
|
+
input: {
|
|
1309
|
+
siteId: site.id,
|
|
1310
|
+
outputPath: args.output_path || null,
|
|
1311
|
+
},
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const result = data.exportSite;
|
|
1315
|
+
if (!result.success) {
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{ type: 'text', text: `Failed to export site: ${result.error}` }],
|
|
1318
|
+
isError: true,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return {
|
|
1323
|
+
content: [{
|
|
1324
|
+
type: 'text',
|
|
1325
|
+
text: `Exported ${site.name} to:\n${result.exportPath}`,
|
|
1326
|
+
}],
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
case 'list_blueprints': {
|
|
1331
|
+
const data = await graphqlRequest(`
|
|
1332
|
+
query {
|
|
1333
|
+
blueprints {
|
|
1334
|
+
success
|
|
1335
|
+
error
|
|
1336
|
+
blueprints {
|
|
1337
|
+
name
|
|
1338
|
+
lastModified
|
|
1339
|
+
phpVersion
|
|
1340
|
+
webServer
|
|
1341
|
+
database
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
`);
|
|
1346
|
+
|
|
1347
|
+
const result = data.blueprints;
|
|
1348
|
+
if (!result.success) {
|
|
1349
|
+
return {
|
|
1350
|
+
content: [{ type: 'text', text: `Failed to list blueprints: ${result.error}` }],
|
|
1351
|
+
isError: true,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (!result.blueprints || result.blueprints.length === 0) {
|
|
1356
|
+
return {
|
|
1357
|
+
content: [{ type: 'text', text: 'No blueprints found. Use save_blueprint to create one from an existing site.' }],
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return {
|
|
1362
|
+
content: [{
|
|
1363
|
+
type: 'text',
|
|
1364
|
+
text: JSON.stringify({ blueprints: result.blueprints, count: result.blueprints.length }, null, 2),
|
|
1365
|
+
}],
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
case 'save_blueprint': {
|
|
1370
|
+
const site = await findSite(args.site);
|
|
1371
|
+
if (!site) {
|
|
1372
|
+
return {
|
|
1373
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1374
|
+
isError: true,
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (!args.name) {
|
|
1379
|
+
return {
|
|
1380
|
+
content: [{ type: 'text', text: 'Error: name is required' }],
|
|
1381
|
+
isError: true,
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Auto-start site if not running
|
|
1386
|
+
await ensureSiteRunning(site);
|
|
1387
|
+
|
|
1388
|
+
const data = await graphqlRequest(`
|
|
1389
|
+
mutation($input: SaveBlueprintInput!) {
|
|
1390
|
+
saveBlueprint(input: $input) {
|
|
1391
|
+
success
|
|
1392
|
+
error
|
|
1393
|
+
blueprintName
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
`, {
|
|
1397
|
+
input: {
|
|
1398
|
+
siteId: site.id,
|
|
1399
|
+
name: args.name,
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
const result = data.saveBlueprint;
|
|
1404
|
+
if (!result.success) {
|
|
1405
|
+
return {
|
|
1406
|
+
content: [{ type: 'text', text: `Failed to save blueprint: ${result.error}` }],
|
|
1407
|
+
isError: true,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return {
|
|
1412
|
+
content: [{
|
|
1413
|
+
type: 'text',
|
|
1414
|
+
text: `Saved ${site.name} as blueprint: ${result.blueprintName}\n\nYou can now create new sites from this blueprint.`,
|
|
1415
|
+
}],
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Phase 8: WordPress Development Tools
|
|
1420
|
+
case 'export_database': {
|
|
1421
|
+
const site = await findSite(args.site);
|
|
1422
|
+
if (!site) {
|
|
1423
|
+
return {
|
|
1424
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1425
|
+
isError: true,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Auto-start site if not running
|
|
1430
|
+
await ensureSiteRunning(site);
|
|
1431
|
+
|
|
1432
|
+
const data = await graphqlRequest(`
|
|
1433
|
+
mutation($input: ExportDatabaseInput!) {
|
|
1434
|
+
exportDatabase(input: $input) {
|
|
1435
|
+
success
|
|
1436
|
+
error
|
|
1437
|
+
outputPath
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
`, {
|
|
1441
|
+
input: {
|
|
1442
|
+
siteId: site.id,
|
|
1443
|
+
outputPath: args.output_path || null,
|
|
1444
|
+
},
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
const result = data.exportDatabase;
|
|
1448
|
+
if (!result.success) {
|
|
1449
|
+
return {
|
|
1450
|
+
content: [{ type: 'text', text: `Failed to export database: ${result.error}` }],
|
|
1451
|
+
isError: true,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return {
|
|
1456
|
+
content: [{
|
|
1457
|
+
type: 'text',
|
|
1458
|
+
text: `Exported database for ${site.name} to:\n${result.outputPath}`,
|
|
1459
|
+
}],
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
case 'import_database': {
|
|
1464
|
+
const site = await findSite(args.site);
|
|
1465
|
+
if (!site) {
|
|
1466
|
+
return {
|
|
1467
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1468
|
+
isError: true,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (!args.sql_path) {
|
|
1473
|
+
return {
|
|
1474
|
+
content: [{ type: 'text', text: 'Error: sql_path is required' }],
|
|
1475
|
+
isError: true,
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// Security: Validate SQL path to prevent path traversal attacks
|
|
1480
|
+
if (!isValidSqlPath(args.sql_path)) {
|
|
1481
|
+
return {
|
|
1482
|
+
content: [{ type: 'text', text: 'Error: Invalid SQL path. Path must end with .sql, exist on disk, and not contain path traversal sequences (..).' }],
|
|
1483
|
+
isError: true,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Auto-start site if not running
|
|
1488
|
+
await ensureSiteRunning(site);
|
|
1489
|
+
|
|
1490
|
+
const data = await graphqlRequest(`
|
|
1491
|
+
mutation($input: ImportDatabaseInput!) {
|
|
1492
|
+
importDatabase(input: $input) {
|
|
1493
|
+
success
|
|
1494
|
+
error
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
`, {
|
|
1498
|
+
input: {
|
|
1499
|
+
siteId: site.id,
|
|
1500
|
+
sqlPath: args.sql_path,
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
const result = data.importDatabase;
|
|
1505
|
+
if (!result.success) {
|
|
1506
|
+
return {
|
|
1507
|
+
content: [{ type: 'text', text: `Failed to import database: ${result.error}` }],
|
|
1508
|
+
isError: true,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return {
|
|
1513
|
+
content: [{
|
|
1514
|
+
type: 'text',
|
|
1515
|
+
text: `Successfully imported database into ${site.name} from:\n${args.sql_path}`,
|
|
1516
|
+
}],
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
case 'open_adminer': {
|
|
1521
|
+
const site = await findSite(args.site);
|
|
1522
|
+
if (!site) {
|
|
1523
|
+
return {
|
|
1524
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1525
|
+
isError: true,
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Auto-start site if not running
|
|
1530
|
+
await ensureSiteRunning(site);
|
|
1531
|
+
|
|
1532
|
+
const data = await graphqlRequest(`
|
|
1533
|
+
mutation($input: OpenAdminerInput!) {
|
|
1534
|
+
openAdminer(input: $input) {
|
|
1535
|
+
success
|
|
1536
|
+
error
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
`, {
|
|
1540
|
+
input: {
|
|
1541
|
+
siteId: site.id,
|
|
1542
|
+
},
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
const result = data.openAdminer;
|
|
1546
|
+
if (!result.success) {
|
|
1547
|
+
return {
|
|
1548
|
+
content: [{ type: 'text', text: `Failed to open Adminer: ${result.error}` }],
|
|
1549
|
+
isError: true,
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return {
|
|
1554
|
+
content: [{
|
|
1555
|
+
type: 'text',
|
|
1556
|
+
text: `Opened Adminer for ${site.name}`,
|
|
1557
|
+
}],
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
case 'trust_ssl': {
|
|
1562
|
+
const site = await findSite(args.site);
|
|
1563
|
+
if (!site) {
|
|
1564
|
+
return {
|
|
1565
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1566
|
+
isError: true,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const data = await graphqlRequest(`
|
|
1571
|
+
mutation($input: TrustSslInput!) {
|
|
1572
|
+
trustSsl(input: $input) {
|
|
1573
|
+
success
|
|
1574
|
+
error
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
`, {
|
|
1578
|
+
input: {
|
|
1579
|
+
siteId: site.id,
|
|
1580
|
+
},
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
const result = data.trustSsl;
|
|
1584
|
+
if (!result.success) {
|
|
1585
|
+
return {
|
|
1586
|
+
content: [{ type: 'text', text: `Failed to trust SSL: ${result.error}` }],
|
|
1587
|
+
isError: true,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
return {
|
|
1592
|
+
content: [{
|
|
1593
|
+
type: 'text',
|
|
1594
|
+
text: `Trusted SSL certificate for ${site.name}`,
|
|
1595
|
+
}],
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
case 'rename_site': {
|
|
1600
|
+
const site = await findSite(args.site);
|
|
1601
|
+
if (!site) {
|
|
1602
|
+
return {
|
|
1603
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1604
|
+
isError: true,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (!args.new_name) {
|
|
1609
|
+
return {
|
|
1610
|
+
content: [{ type: 'text', text: 'Error: new_name is required' }],
|
|
1611
|
+
isError: true,
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const data = await graphqlRequest(`
|
|
1616
|
+
mutation($input: McpRenameSiteInput!) {
|
|
1617
|
+
mcpRenameSite(input: $input) {
|
|
1618
|
+
success
|
|
1619
|
+
error
|
|
1620
|
+
newName
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
`, {
|
|
1624
|
+
input: {
|
|
1625
|
+
siteId: site.id,
|
|
1626
|
+
newName: args.new_name,
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
const result = data.mcpRenameSite;
|
|
1631
|
+
if (!result.success) {
|
|
1632
|
+
return {
|
|
1633
|
+
content: [{ type: 'text', text: `Failed to rename site: ${result.error}` }],
|
|
1634
|
+
isError: true,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
return {
|
|
1639
|
+
content: [{
|
|
1640
|
+
type: 'text',
|
|
1641
|
+
text: `Renamed ${site.name} to ${result.newName}`,
|
|
1642
|
+
}],
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
case 'change_php_version': {
|
|
1647
|
+
const site = await findSite(args.site);
|
|
1648
|
+
if (!site) {
|
|
1649
|
+
return {
|
|
1650
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1651
|
+
isError: true,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (!args.php_version) {
|
|
1656
|
+
return {
|
|
1657
|
+
content: [{ type: 'text', text: 'Error: php_version is required' }],
|
|
1658
|
+
isError: true,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const data = await graphqlRequest(`
|
|
1663
|
+
mutation($input: ChangePhpVersionInput!) {
|
|
1664
|
+
changePhpVersion(input: $input) {
|
|
1665
|
+
success
|
|
1666
|
+
error
|
|
1667
|
+
phpVersion
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
`, {
|
|
1671
|
+
input: {
|
|
1672
|
+
siteId: site.id,
|
|
1673
|
+
phpVersion: args.php_version,
|
|
1674
|
+
},
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
const result = data.changePhpVersion;
|
|
1678
|
+
if (!result.success) {
|
|
1679
|
+
return {
|
|
1680
|
+
content: [{ type: 'text', text: `Failed to change PHP version: ${result.error}` }],
|
|
1681
|
+
isError: true,
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
return {
|
|
1686
|
+
content: [{
|
|
1687
|
+
type: 'text',
|
|
1688
|
+
text: `Changed PHP version for ${site.name} to ${result.phpVersion}`,
|
|
1689
|
+
}],
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
case 'import_site': {
|
|
1694
|
+
if (!args.zip_path) {
|
|
1695
|
+
return {
|
|
1696
|
+
content: [{ type: 'text', text: 'Error: zip_path is required' }],
|
|
1697
|
+
isError: true,
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const data = await graphqlRequest(`
|
|
1702
|
+
mutation($input: ImportSiteInput!) {
|
|
1703
|
+
importSite(input: $input) {
|
|
1704
|
+
success
|
|
1705
|
+
error
|
|
1706
|
+
siteId
|
|
1707
|
+
siteName
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
`, {
|
|
1711
|
+
input: {
|
|
1712
|
+
zipPath: args.zip_path,
|
|
1713
|
+
siteName: args.site_name || null,
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
const result = data.importSite;
|
|
1718
|
+
if (!result.success) {
|
|
1719
|
+
return {
|
|
1720
|
+
content: [{ type: 'text', text: `Failed to import site: ${result.error}` }],
|
|
1721
|
+
isError: true,
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
return {
|
|
1726
|
+
content: [{
|
|
1727
|
+
type: 'text',
|
|
1728
|
+
text: `Imported site: ${result.siteName}\nSite ID: ${result.siteId}`,
|
|
1729
|
+
}],
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Phase 9: Site Configuration & Dev Tools
|
|
1734
|
+
case 'toggle_xdebug': {
|
|
1735
|
+
const site = await findSite(args.site);
|
|
1736
|
+
if (!site) {
|
|
1737
|
+
return {
|
|
1738
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1739
|
+
isError: true,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
if (typeof args.enabled !== 'boolean') {
|
|
1744
|
+
return {
|
|
1745
|
+
content: [{ type: 'text', text: 'Error: enabled must be true or false' }],
|
|
1746
|
+
isError: true,
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
const data = await graphqlRequest(`
|
|
1751
|
+
mutation($input: ToggleXdebugInput!) {
|
|
1752
|
+
toggleXdebug(input: $input) {
|
|
1753
|
+
success
|
|
1754
|
+
error
|
|
1755
|
+
enabled
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
`, {
|
|
1759
|
+
input: {
|
|
1760
|
+
siteId: site.id,
|
|
1761
|
+
enabled: args.enabled,
|
|
1762
|
+
},
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
const result = data.toggleXdebug;
|
|
1766
|
+
if (!result.success) {
|
|
1767
|
+
return {
|
|
1768
|
+
content: [{ type: 'text', text: `Failed to toggle Xdebug: ${result.error}` }],
|
|
1769
|
+
isError: true,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
return {
|
|
1774
|
+
content: [{
|
|
1775
|
+
type: 'text',
|
|
1776
|
+
text: `Xdebug ${result.enabled ? 'enabled' : 'disabled'} for ${site.name}`,
|
|
1777
|
+
}],
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
case 'get_site_logs': {
|
|
1782
|
+
const site = await findSite(args.site);
|
|
1783
|
+
if (!site) {
|
|
1784
|
+
return {
|
|
1785
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1786
|
+
isError: true,
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const data = await graphqlRequest(`
|
|
1791
|
+
mutation($input: GetSiteLogsInput!) {
|
|
1792
|
+
getSiteLogs(input: $input) {
|
|
1793
|
+
success
|
|
1794
|
+
error
|
|
1795
|
+
logs {
|
|
1796
|
+
type
|
|
1797
|
+
content
|
|
1798
|
+
path
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
`, {
|
|
1803
|
+
input: {
|
|
1804
|
+
siteId: site.id,
|
|
1805
|
+
logType: args.log_type || 'php',
|
|
1806
|
+
lines: args.lines || 100,
|
|
1807
|
+
},
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
const result = data.getSiteLogs;
|
|
1811
|
+
if (!result.success) {
|
|
1812
|
+
return {
|
|
1813
|
+
content: [{ type: 'text', text: `Failed to get logs: ${result.error}` }],
|
|
1814
|
+
isError: true,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
let output = `Logs for ${site.name}:\n`;
|
|
1819
|
+
for (const log of result.logs) {
|
|
1820
|
+
output += `\n=== ${log.type.toUpperCase()} (${log.path}) ===\n${log.content}\n`;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
return {
|
|
1824
|
+
content: [{ type: 'text', text: output }],
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
case 'list_services': {
|
|
1829
|
+
const data = await graphqlRequest(`
|
|
1830
|
+
query($type: String) {
|
|
1831
|
+
listServices(type: $type) {
|
|
1832
|
+
success
|
|
1833
|
+
error
|
|
1834
|
+
services {
|
|
1835
|
+
role
|
|
1836
|
+
name
|
|
1837
|
+
version
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
`, {
|
|
1842
|
+
type: args.type || 'all',
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
const result = data.listServices;
|
|
1846
|
+
if (!result.success) {
|
|
1847
|
+
return {
|
|
1848
|
+
content: [{ type: 'text', text: `Failed to list services: ${result.error}` }],
|
|
1849
|
+
isError: true,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
return {
|
|
1854
|
+
content: [{
|
|
1855
|
+
type: 'text',
|
|
1856
|
+
text: JSON.stringify({ services: result.services, count: result.services.length }, null, 2),
|
|
1857
|
+
}],
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Phase 10: Cloud Backups
|
|
1862
|
+
case 'backup_status': {
|
|
1863
|
+
const data = await graphqlRequest(`
|
|
1864
|
+
query {
|
|
1865
|
+
backupStatus {
|
|
1866
|
+
available
|
|
1867
|
+
featureEnabled
|
|
1868
|
+
dropbox {
|
|
1869
|
+
authenticated
|
|
1870
|
+
accountId
|
|
1871
|
+
email
|
|
1872
|
+
}
|
|
1873
|
+
googleDrive {
|
|
1874
|
+
authenticated
|
|
1875
|
+
accountId
|
|
1876
|
+
email
|
|
1877
|
+
}
|
|
1878
|
+
message
|
|
1879
|
+
error
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
`);
|
|
1883
|
+
|
|
1884
|
+
const result = data.backupStatus;
|
|
1885
|
+
if (result.error) {
|
|
1886
|
+
return {
|
|
1887
|
+
content: [{ type: 'text', text: `Backup status check failed: ${result.error}` }],
|
|
1888
|
+
isError: true,
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return {
|
|
1893
|
+
content: [{
|
|
1894
|
+
type: 'text',
|
|
1895
|
+
text: JSON.stringify(result, null, 2),
|
|
1896
|
+
}],
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
case 'list_backups': {
|
|
1901
|
+
const site = await findSite(args.site);
|
|
1902
|
+
if (!site) {
|
|
1903
|
+
return {
|
|
1904
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1905
|
+
isError: true,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
const data = await graphqlRequest(`
|
|
1910
|
+
query ListBackups($siteId: ID!, $provider: String!) {
|
|
1911
|
+
listBackups(siteId: $siteId, provider: $provider) {
|
|
1912
|
+
success
|
|
1913
|
+
siteName
|
|
1914
|
+
provider
|
|
1915
|
+
backups {
|
|
1916
|
+
snapshotId
|
|
1917
|
+
timestamp
|
|
1918
|
+
note
|
|
1919
|
+
siteDomain
|
|
1920
|
+
services
|
|
1921
|
+
}
|
|
1922
|
+
count
|
|
1923
|
+
error
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
`, {
|
|
1927
|
+
siteId: site.id,
|
|
1928
|
+
provider: args.provider,
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
const result = data.listBackups;
|
|
1932
|
+
if (!result.success) {
|
|
1933
|
+
return {
|
|
1934
|
+
content: [{ type: 'text', text: `Failed to list backups: ${result.error}` }],
|
|
1935
|
+
isError: true,
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
return {
|
|
1940
|
+
content: [{
|
|
1941
|
+
type: 'text',
|
|
1942
|
+
text: JSON.stringify({
|
|
1943
|
+
siteName: result.siteName,
|
|
1944
|
+
provider: result.provider,
|
|
1945
|
+
backups: result.backups,
|
|
1946
|
+
count: result.count,
|
|
1947
|
+
}, null, 2),
|
|
1948
|
+
}],
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
case 'create_backup': {
|
|
1953
|
+
const site = await findSite(args.site);
|
|
1954
|
+
if (!site) {
|
|
1955
|
+
return {
|
|
1956
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
1957
|
+
isError: true,
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// Auto-start site if not running
|
|
1962
|
+
await ensureSiteRunning(site);
|
|
1963
|
+
|
|
1964
|
+
const data = await withTimeout(
|
|
1965
|
+
graphqlRequest(`
|
|
1966
|
+
mutation CreateBackup($siteId: ID!, $provider: String!, $note: String) {
|
|
1967
|
+
createBackup(siteId: $siteId, provider: $provider, note: $note) {
|
|
1968
|
+
success
|
|
1969
|
+
snapshotId
|
|
1970
|
+
timestamp
|
|
1971
|
+
message
|
|
1972
|
+
error
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
`, {
|
|
1976
|
+
siteId: site.id,
|
|
1977
|
+
provider: args.provider,
|
|
1978
|
+
note: args.note,
|
|
1979
|
+
}),
|
|
1980
|
+
TIMEOUT_BACKUP_OPERATION,
|
|
1981
|
+
'Create backup'
|
|
1982
|
+
);
|
|
1983
|
+
|
|
1984
|
+
const result = data.createBackup;
|
|
1985
|
+
if (!result.success) {
|
|
1986
|
+
return {
|
|
1987
|
+
content: [{ type: 'text', text: `Failed to create backup: ${result.error}` }],
|
|
1988
|
+
isError: true,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
return {
|
|
1993
|
+
content: [{
|
|
1994
|
+
type: 'text',
|
|
1995
|
+
text: JSON.stringify({
|
|
1996
|
+
success: true,
|
|
1997
|
+
snapshotId: result.snapshotId,
|
|
1998
|
+
timestamp: result.timestamp,
|
|
1999
|
+
message: result.message,
|
|
2000
|
+
}, null, 2),
|
|
2001
|
+
}],
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
case 'restore_backup': {
|
|
2006
|
+
// Security: Validate snapshot ID format
|
|
2007
|
+
if (!isValidSnapshotId(args.snapshot_id)) {
|
|
2008
|
+
return {
|
|
2009
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
2010
|
+
error: 'Invalid snapshot ID format. Use list_backups to get valid snapshot IDs.',
|
|
2011
|
+
}) }],
|
|
2012
|
+
isError: true,
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const site = await findSite(args.site);
|
|
2017
|
+
if (!site) {
|
|
2018
|
+
return {
|
|
2019
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2020
|
+
isError: true,
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Auto-start site if not running
|
|
2025
|
+
await ensureSiteRunning(site);
|
|
2026
|
+
|
|
2027
|
+
const data = await withTimeout(
|
|
2028
|
+
graphqlRequest(`
|
|
2029
|
+
mutation RestoreBackup($siteId: ID!, $provider: String!, $snapshotId: String!, $confirm: Boolean) {
|
|
2030
|
+
restoreBackup(siteId: $siteId, provider: $provider, snapshotId: $snapshotId, confirm: $confirm) {
|
|
2031
|
+
success
|
|
2032
|
+
message
|
|
2033
|
+
error
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
`, {
|
|
2037
|
+
siteId: site.id,
|
|
2038
|
+
provider: args.provider,
|
|
2039
|
+
snapshotId: args.snapshot_id,
|
|
2040
|
+
confirm: args.confirm === true,
|
|
2041
|
+
}),
|
|
2042
|
+
TIMEOUT_BACKUP_OPERATION,
|
|
2043
|
+
'Restore backup'
|
|
2044
|
+
);
|
|
2045
|
+
|
|
2046
|
+
const result = data.restoreBackup;
|
|
2047
|
+
if (!result.success) {
|
|
2048
|
+
return {
|
|
2049
|
+
content: [{ type: 'text', text: `Failed to restore backup: ${result.error}` }],
|
|
2050
|
+
isError: true,
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
return {
|
|
2055
|
+
content: [{
|
|
2056
|
+
type: 'text',
|
|
2057
|
+
text: JSON.stringify({
|
|
2058
|
+
success: true,
|
|
2059
|
+
message: result.message,
|
|
2060
|
+
}, null, 2),
|
|
2061
|
+
}],
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
case 'delete_backup': {
|
|
2066
|
+
// Security: Validate snapshot ID format
|
|
2067
|
+
if (!isValidSnapshotId(args.snapshot_id)) {
|
|
2068
|
+
return {
|
|
2069
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
2070
|
+
error: 'Invalid snapshot ID format. Use list_backups to get valid snapshot IDs.',
|
|
2071
|
+
}) }],
|
|
2072
|
+
isError: true,
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const site = await findSite(args.site);
|
|
2077
|
+
if (!site) {
|
|
2078
|
+
return {
|
|
2079
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2080
|
+
isError: true,
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const data = await graphqlRequest(`
|
|
2085
|
+
mutation DeleteBackup($siteId: ID!, $provider: String!, $snapshotId: String!, $confirm: Boolean) {
|
|
2086
|
+
deleteBackup(siteId: $siteId, provider: $provider, snapshotId: $snapshotId, confirm: $confirm) {
|
|
2087
|
+
success
|
|
2088
|
+
deletedSnapshotId
|
|
2089
|
+
message
|
|
2090
|
+
error
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
`, {
|
|
2094
|
+
siteId: site.id,
|
|
2095
|
+
provider: args.provider,
|
|
2096
|
+
snapshotId: args.snapshot_id,
|
|
2097
|
+
confirm: args.confirm === true,
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
const result = data.deleteBackup;
|
|
2101
|
+
if (!result.success) {
|
|
2102
|
+
return {
|
|
2103
|
+
content: [{ type: 'text', text: `Failed to delete backup: ${result.error}` }],
|
|
2104
|
+
isError: true,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
return {
|
|
2109
|
+
content: [{
|
|
2110
|
+
type: 'text',
|
|
2111
|
+
text: JSON.stringify({
|
|
2112
|
+
success: true,
|
|
2113
|
+
deletedSnapshotId: result.deletedSnapshotId,
|
|
2114
|
+
message: result.message,
|
|
2115
|
+
}, null, 2),
|
|
2116
|
+
}],
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
case 'download_backup': {
|
|
2121
|
+
const site = await findSite(args.site);
|
|
2122
|
+
if (!site) {
|
|
2123
|
+
return {
|
|
2124
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2125
|
+
isError: true,
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Security: Validate snapshot ID format
|
|
2130
|
+
if (!isValidSnapshotId(args.snapshot_id)) {
|
|
2131
|
+
return {
|
|
2132
|
+
content: [{ type: 'text', text: 'Error: Invalid snapshot ID format. Expected hex string (8-64 characters).' }],
|
|
2133
|
+
isError: true,
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
const data = await graphqlRequest(`
|
|
2138
|
+
mutation DownloadBackup($siteId: ID!, $provider: String!, $snapshotId: String!) {
|
|
2139
|
+
downloadBackup(siteId: $siteId, provider: $provider, snapshotId: $snapshotId) {
|
|
2140
|
+
success
|
|
2141
|
+
filePath
|
|
2142
|
+
message
|
|
2143
|
+
error
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
`, {
|
|
2147
|
+
siteId: site.id,
|
|
2148
|
+
provider: args.provider,
|
|
2149
|
+
snapshotId: args.snapshot_id,
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
const result = data.downloadBackup;
|
|
2153
|
+
if (!result.success) {
|
|
2154
|
+
return {
|
|
2155
|
+
content: [{ type: 'text', text: `Failed to download backup: ${result.error}` }],
|
|
2156
|
+
isError: true,
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
return {
|
|
2161
|
+
content: [{
|
|
2162
|
+
type: 'text',
|
|
2163
|
+
text: JSON.stringify({
|
|
2164
|
+
success: true,
|
|
2165
|
+
filePath: result.filePath,
|
|
2166
|
+
message: result.message,
|
|
2167
|
+
}, null, 2),
|
|
2168
|
+
}],
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
case 'edit_backup_note': {
|
|
2173
|
+
const site = await findSite(args.site);
|
|
2174
|
+
if (!site) {
|
|
2175
|
+
return {
|
|
2176
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2177
|
+
isError: true,
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
const data = await graphqlRequest(`
|
|
2182
|
+
mutation EditBackupNote($siteId: ID!, $provider: String!, $snapshotId: String!, $note: String!) {
|
|
2183
|
+
editBackupNote(siteId: $siteId, provider: $provider, snapshotId: $snapshotId, note: $note) {
|
|
2184
|
+
success
|
|
2185
|
+
snapshotId
|
|
2186
|
+
note
|
|
2187
|
+
error
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
`, {
|
|
2191
|
+
siteId: site.id,
|
|
2192
|
+
provider: args.provider,
|
|
2193
|
+
snapshotId: args.snapshot_id,
|
|
2194
|
+
note: args.note,
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
const result = data.editBackupNote;
|
|
2198
|
+
if (!result.success) {
|
|
2199
|
+
return {
|
|
2200
|
+
content: [{ type: 'text', text: `Failed to edit backup note: ${result.error}` }],
|
|
2201
|
+
isError: true,
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return {
|
|
2206
|
+
content: [{
|
|
2207
|
+
type: 'text',
|
|
2208
|
+
text: JSON.stringify({
|
|
2209
|
+
success: true,
|
|
2210
|
+
snapshotId: result.snapshotId,
|
|
2211
|
+
note: result.note,
|
|
2212
|
+
}, null, 2),
|
|
2213
|
+
}],
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// Phase 11: WP Engine Connect
|
|
2218
|
+
case 'wpe_status': {
|
|
2219
|
+
const data = await graphqlRequest(`
|
|
2220
|
+
query {
|
|
2221
|
+
wpeStatus {
|
|
2222
|
+
authenticated
|
|
2223
|
+
email
|
|
2224
|
+
accountId
|
|
2225
|
+
accountName
|
|
2226
|
+
tokenExpiry
|
|
2227
|
+
error
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
`);
|
|
2231
|
+
|
|
2232
|
+
const result = data.wpeStatus;
|
|
2233
|
+
if (result.error && !result.authenticated) {
|
|
2234
|
+
return {
|
|
2235
|
+
content: [{ type: 'text', text: `WP Engine status check failed: ${result.error}` }],
|
|
2236
|
+
isError: true,
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (!result.authenticated) {
|
|
2241
|
+
return {
|
|
2242
|
+
content: [{
|
|
2243
|
+
type: 'text',
|
|
2244
|
+
text: JSON.stringify({
|
|
2245
|
+
authenticated: false,
|
|
2246
|
+
message: 'Not authenticated with WP Engine. Use wpe_authenticate to login.',
|
|
2247
|
+
}, null, 2),
|
|
2248
|
+
}],
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
return {
|
|
2253
|
+
content: [{
|
|
2254
|
+
type: 'text',
|
|
2255
|
+
text: JSON.stringify({
|
|
2256
|
+
authenticated: true,
|
|
2257
|
+
email: result.email,
|
|
2258
|
+
accountId: result.accountId,
|
|
2259
|
+
accountName: result.accountName,
|
|
2260
|
+
tokenExpiry: result.tokenExpiry,
|
|
2261
|
+
}, null, 2),
|
|
2262
|
+
}],
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
case 'wpe_authenticate': {
|
|
2267
|
+
const data = await graphqlRequest(`
|
|
2268
|
+
mutation {
|
|
2269
|
+
wpeAuthenticate {
|
|
2270
|
+
success
|
|
2271
|
+
email
|
|
2272
|
+
message
|
|
2273
|
+
error
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
`);
|
|
2277
|
+
|
|
2278
|
+
const result = data.wpeAuthenticate;
|
|
2279
|
+
if (!result.success) {
|
|
2280
|
+
return {
|
|
2281
|
+
content: [{ type: 'text', text: `WP Engine authentication failed: ${result.error}` }],
|
|
2282
|
+
isError: true,
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
return {
|
|
2287
|
+
content: [{
|
|
2288
|
+
type: 'text',
|
|
2289
|
+
text: JSON.stringify({
|
|
2290
|
+
success: true,
|
|
2291
|
+
email: result.email,
|
|
2292
|
+
message: result.message || 'Authentication initiated. Please complete the login in your browser.',
|
|
2293
|
+
}, null, 2),
|
|
2294
|
+
}],
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
case 'wpe_logout': {
|
|
2299
|
+
const data = await graphqlRequest(`
|
|
2300
|
+
mutation {
|
|
2301
|
+
wpeLogout {
|
|
2302
|
+
success
|
|
2303
|
+
message
|
|
2304
|
+
error
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
`);
|
|
2308
|
+
|
|
2309
|
+
const result = data.wpeLogout;
|
|
2310
|
+
if (!result.success) {
|
|
2311
|
+
return {
|
|
2312
|
+
content: [{ type: 'text', text: `WP Engine logout failed: ${result.error}` }],
|
|
2313
|
+
isError: true,
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
return {
|
|
2318
|
+
content: [{
|
|
2319
|
+
type: 'text',
|
|
2320
|
+
text: JSON.stringify({
|
|
2321
|
+
success: true,
|
|
2322
|
+
message: result.message || 'Logged out from WP Engine',
|
|
2323
|
+
}, null, 2),
|
|
2324
|
+
}],
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
case 'list_wpe_sites': {
|
|
2329
|
+
const data = await graphqlRequest(`
|
|
2330
|
+
query($accountId: String) {
|
|
2331
|
+
listWpeSites(accountId: $accountId) {
|
|
2332
|
+
success
|
|
2333
|
+
error
|
|
2334
|
+
sites {
|
|
2335
|
+
id
|
|
2336
|
+
name
|
|
2337
|
+
environment
|
|
2338
|
+
phpVersion
|
|
2339
|
+
primaryDomain
|
|
2340
|
+
accountId
|
|
2341
|
+
accountName
|
|
2342
|
+
sftpHost
|
|
2343
|
+
sftpUser
|
|
2344
|
+
}
|
|
2345
|
+
count
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
`, {
|
|
2349
|
+
accountId: args.account_id || null,
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
const result = data.listWpeSites;
|
|
2353
|
+
if (!result.success) {
|
|
2354
|
+
return {
|
|
2355
|
+
content: [{ type: 'text', text: `Failed to list WP Engine sites: ${result.error}` }],
|
|
2356
|
+
isError: true,
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
return {
|
|
2361
|
+
content: [{
|
|
2362
|
+
type: 'text',
|
|
2363
|
+
text: JSON.stringify({ sites: result.sites, count: result.count }, null, 2),
|
|
2364
|
+
}],
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// Phase 11b: Site Linking
|
|
2369
|
+
case 'get_wpe_link': {
|
|
2370
|
+
const site = await findSite(args.site);
|
|
2371
|
+
if (!site) {
|
|
2372
|
+
return {
|
|
2373
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2374
|
+
isError: true,
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// Use the getWpeLink query which enriches with CAPI data
|
|
2379
|
+
const data = await graphqlRequest(`
|
|
2380
|
+
query($siteId: ID!) {
|
|
2381
|
+
getWpeLink(siteId: $siteId) {
|
|
2382
|
+
linked
|
|
2383
|
+
siteName
|
|
2384
|
+
connections {
|
|
2385
|
+
remoteInstallId
|
|
2386
|
+
installName
|
|
2387
|
+
environment
|
|
2388
|
+
accountId
|
|
2389
|
+
portalUrl
|
|
2390
|
+
primaryDomain
|
|
2391
|
+
}
|
|
2392
|
+
connectionCount
|
|
2393
|
+
capabilities {
|
|
2394
|
+
canPush
|
|
2395
|
+
canPull
|
|
2396
|
+
syncModes
|
|
2397
|
+
magicSyncAvailable
|
|
2398
|
+
databaseSyncAvailable
|
|
2399
|
+
}
|
|
2400
|
+
message
|
|
2401
|
+
error
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
`, { siteId: site.id });
|
|
2405
|
+
|
|
2406
|
+
const result = data.getWpeLink;
|
|
2407
|
+
if (result.error) {
|
|
2408
|
+
return {
|
|
2409
|
+
content: [{ type: 'text', text: `Failed to get WPE link: ${result.error}` }],
|
|
2410
|
+
isError: true,
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
if (!result.linked) {
|
|
2415
|
+
return {
|
|
2416
|
+
content: [{
|
|
2417
|
+
type: 'text',
|
|
2418
|
+
text: JSON.stringify({
|
|
2419
|
+
linked: false,
|
|
2420
|
+
siteName: result.siteName,
|
|
2421
|
+
message: result.message || 'Site is not linked to any WP Engine environment.',
|
|
2422
|
+
}, null, 2),
|
|
2423
|
+
}],
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
return {
|
|
2428
|
+
content: [{
|
|
2429
|
+
type: 'text',
|
|
2430
|
+
text: JSON.stringify({
|
|
2431
|
+
linked: true,
|
|
2432
|
+
siteName: result.siteName,
|
|
2433
|
+
connections: result.connections,
|
|
2434
|
+
connectionCount: result.connectionCount,
|
|
2435
|
+
capabilities: result.capabilities,
|
|
2436
|
+
}, null, 2),
|
|
2437
|
+
}],
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// Phase 11c: Sync Operations
|
|
2442
|
+
case 'push_to_wpe': {
|
|
2443
|
+
const site = await findSite(args.site);
|
|
2444
|
+
if (!site) {
|
|
2445
|
+
return {
|
|
2446
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2447
|
+
isError: true,
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// Auto-start site if not running
|
|
2452
|
+
await ensureSiteRunning(site);
|
|
2453
|
+
|
|
2454
|
+
const data = await withTimeout(
|
|
2455
|
+
graphqlRequest(`
|
|
2456
|
+
mutation($localSiteId: ID!, $remoteInstallId: ID!, $includeSql: Boolean, $confirm: Boolean) {
|
|
2457
|
+
pushToWpe(localSiteId: $localSiteId, remoteInstallId: $remoteInstallId, includeSql: $includeSql, confirm: $confirm) {
|
|
2458
|
+
success
|
|
2459
|
+
message
|
|
2460
|
+
error
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
`, {
|
|
2464
|
+
localSiteId: site.id,
|
|
2465
|
+
remoteInstallId: site.id, // Will be resolved by the mutation using hostConnections
|
|
2466
|
+
includeSql: args.include_database || false,
|
|
2467
|
+
confirm: args.confirm || false,
|
|
2468
|
+
}),
|
|
2469
|
+
TIMEOUT_SYNC_OPERATION,
|
|
2470
|
+
'Push to WP Engine'
|
|
2471
|
+
);
|
|
2472
|
+
|
|
2473
|
+
const result = data.pushToWpe;
|
|
2474
|
+
if (!result.success) {
|
|
2475
|
+
return {
|
|
2476
|
+
content: [{ type: 'text', text: result.error || 'Push failed' }],
|
|
2477
|
+
isError: true,
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
return {
|
|
2482
|
+
content: [{
|
|
2483
|
+
type: 'text',
|
|
2484
|
+
text: JSON.stringify({
|
|
2485
|
+
success: true,
|
|
2486
|
+
message: result.message,
|
|
2487
|
+
}, null, 2),
|
|
2488
|
+
}],
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
case 'pull_from_wpe': {
|
|
2493
|
+
// Require confirmation for destructive operation
|
|
2494
|
+
if (!args.confirm) {
|
|
2495
|
+
return {
|
|
2496
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
2497
|
+
error: 'Pull requires confirm=true to prevent accidental overwrites. This operation will overwrite local files' + (args.include_database ? ' and database' : '') + '.',
|
|
2498
|
+
}) }],
|
|
2499
|
+
isError: true,
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
const site = await findSite(args.site);
|
|
2504
|
+
if (!site) {
|
|
2505
|
+
return {
|
|
2506
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2507
|
+
isError: true,
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// Auto-start site if not running
|
|
2512
|
+
await ensureSiteRunning(site);
|
|
2513
|
+
|
|
2514
|
+
const data = await withTimeout(
|
|
2515
|
+
graphqlRequest(`
|
|
2516
|
+
mutation($localSiteId: ID!, $remoteInstallId: ID!, $includeSql: Boolean, $confirm: Boolean) {
|
|
2517
|
+
pullFromWpe(localSiteId: $localSiteId, remoteInstallId: $remoteInstallId, includeSql: $includeSql, confirm: $confirm) {
|
|
2518
|
+
success
|
|
2519
|
+
message
|
|
2520
|
+
error
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
`, {
|
|
2524
|
+
localSiteId: site.id,
|
|
2525
|
+
remoteInstallId: site.id, // Will be resolved by the mutation using hostConnections
|
|
2526
|
+
includeSql: args.include_database || false,
|
|
2527
|
+
confirm: args.confirm || false,
|
|
2528
|
+
}),
|
|
2529
|
+
TIMEOUT_SYNC_OPERATION,
|
|
2530
|
+
'Pull from WP Engine'
|
|
2531
|
+
);
|
|
2532
|
+
|
|
2533
|
+
const result = data.pullFromWpe;
|
|
2534
|
+
if (!result.success) {
|
|
2535
|
+
return {
|
|
2536
|
+
content: [{ type: 'text', text: result.error || 'Pull failed' }],
|
|
2537
|
+
isError: true,
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
return {
|
|
2542
|
+
content: [{
|
|
2543
|
+
type: 'text',
|
|
2544
|
+
text: JSON.stringify({
|
|
2545
|
+
success: true,
|
|
2546
|
+
message: result.message,
|
|
2547
|
+
}, null, 2),
|
|
2548
|
+
}],
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
case 'get_sync_history': {
|
|
2553
|
+
const site = await findSite(args.site);
|
|
2554
|
+
if (!site) {
|
|
2555
|
+
return {
|
|
2556
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2557
|
+
isError: true,
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
const data = await graphqlRequest(`
|
|
2562
|
+
query($siteId: ID!, $limit: Int) {
|
|
2563
|
+
getSyncHistory(siteId: $siteId, limit: $limit) {
|
|
2564
|
+
success
|
|
2565
|
+
siteName
|
|
2566
|
+
events {
|
|
2567
|
+
remoteInstallName
|
|
2568
|
+
timestamp
|
|
2569
|
+
environment
|
|
2570
|
+
direction
|
|
2571
|
+
status
|
|
2572
|
+
}
|
|
2573
|
+
count
|
|
2574
|
+
error
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
`, {
|
|
2578
|
+
siteId: site.id,
|
|
2579
|
+
limit: args.limit || 10,
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
const result = data.getSyncHistory;
|
|
2583
|
+
if (!result.success) {
|
|
2584
|
+
return {
|
|
2585
|
+
content: [{ type: 'text', text: result.error || 'Failed to get sync history' }],
|
|
2586
|
+
isError: true,
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// Format events for readability
|
|
2591
|
+
const formattedEvents = result.events.map(e => ({
|
|
2592
|
+
...e,
|
|
2593
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
2594
|
+
}));
|
|
2595
|
+
|
|
2596
|
+
return {
|
|
2597
|
+
content: [{
|
|
2598
|
+
type: 'text',
|
|
2599
|
+
text: JSON.stringify({
|
|
2600
|
+
siteName: result.siteName,
|
|
2601
|
+
events: formattedEvents,
|
|
2602
|
+
count: result.count,
|
|
2603
|
+
}, null, 2),
|
|
2604
|
+
}],
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
case 'get_site_changes': {
|
|
2609
|
+
const site = await findSite(args.site);
|
|
2610
|
+
if (!site) {
|
|
2611
|
+
return {
|
|
2612
|
+
content: [{ type: 'text', text: `Site not found: ${args.site}` }],
|
|
2613
|
+
isError: true,
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// Auto-start site if not running
|
|
2618
|
+
await ensureSiteRunning(site);
|
|
2619
|
+
|
|
2620
|
+
const data = await graphqlRequest(`
|
|
2621
|
+
query($siteId: ID!, $direction: String) {
|
|
2622
|
+
getSiteChanges(siteId: $siteId, direction: $direction) {
|
|
2623
|
+
success
|
|
2624
|
+
siteName
|
|
2625
|
+
direction
|
|
2626
|
+
added {
|
|
2627
|
+
path
|
|
2628
|
+
instruction
|
|
2629
|
+
size
|
|
2630
|
+
type
|
|
2631
|
+
}
|
|
2632
|
+
modified {
|
|
2633
|
+
path
|
|
2634
|
+
instruction
|
|
2635
|
+
size
|
|
2636
|
+
type
|
|
2637
|
+
}
|
|
2638
|
+
deleted {
|
|
2639
|
+
path
|
|
2640
|
+
instruction
|
|
2641
|
+
size
|
|
2642
|
+
type
|
|
2643
|
+
}
|
|
2644
|
+
totalChanges
|
|
2645
|
+
message
|
|
2646
|
+
error
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
`, {
|
|
2650
|
+
siteId: site.id,
|
|
2651
|
+
direction: args.direction || 'push',
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
const result = data.getSiteChanges;
|
|
2655
|
+
if (!result.success) {
|
|
2656
|
+
return {
|
|
2657
|
+
content: [{ type: 'text', text: result.error || 'Failed to get site changes' }],
|
|
2658
|
+
isError: true,
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// Format output for readability
|
|
2663
|
+
const output = {
|
|
2664
|
+
siteName: result.siteName,
|
|
2665
|
+
direction: result.direction,
|
|
2666
|
+
summary: result.message,
|
|
2667
|
+
totalChanges: result.totalChanges,
|
|
2668
|
+
};
|
|
2669
|
+
|
|
2670
|
+
// Only include non-empty arrays
|
|
2671
|
+
if (result.added.length > 0) {
|
|
2672
|
+
output.added = result.added.map(f => f.path);
|
|
2673
|
+
}
|
|
2674
|
+
if (result.modified.length > 0) {
|
|
2675
|
+
output.modified = result.modified.map(f => f.path);
|
|
2676
|
+
}
|
|
2677
|
+
if (result.deleted.length > 0) {
|
|
2678
|
+
output.deleted = result.deleted.map(f => f.path);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
return {
|
|
2682
|
+
content: [{
|
|
2683
|
+
type: 'text',
|
|
2684
|
+
text: JSON.stringify(output, null, 2),
|
|
2685
|
+
}],
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
default:
|
|
2690
|
+
return {
|
|
2691
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
2692
|
+
isError: true,
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
// Process MCP messages
|
|
2698
|
+
async function processMessage(message) {
|
|
2699
|
+
const { id, method, params } = message;
|
|
2700
|
+
|
|
2701
|
+
switch (method) {
|
|
2702
|
+
case 'initialize':
|
|
2703
|
+
return {
|
|
2704
|
+
jsonrpc: '2.0',
|
|
2705
|
+
id,
|
|
2706
|
+
result: {
|
|
2707
|
+
protocolVersion: '2024-11-05',
|
|
2708
|
+
capabilities: { tools: {} },
|
|
2709
|
+
serverInfo: {
|
|
2710
|
+
name: 'local-mcp',
|
|
2711
|
+
version: '1.0.0',
|
|
2712
|
+
},
|
|
2713
|
+
},
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
case 'notifications/initialized':
|
|
2717
|
+
return null; // No response for notifications
|
|
2718
|
+
|
|
2719
|
+
case 'tools/list':
|
|
2720
|
+
return {
|
|
2721
|
+
jsonrpc: '2.0',
|
|
2722
|
+
id,
|
|
2723
|
+
result: { tools },
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
case 'tools/call':
|
|
2727
|
+
try {
|
|
2728
|
+
const result = await handleTool(params.name, params.arguments || {});
|
|
2729
|
+
return {
|
|
2730
|
+
jsonrpc: '2.0',
|
|
2731
|
+
id,
|
|
2732
|
+
result,
|
|
2733
|
+
};
|
|
2734
|
+
} catch (err) {
|
|
2735
|
+
return {
|
|
2736
|
+
jsonrpc: '2.0',
|
|
2737
|
+
id,
|
|
2738
|
+
result: {
|
|
2739
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
2740
|
+
isError: true,
|
|
2741
|
+
},
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
case 'ping':
|
|
2746
|
+
return {
|
|
2747
|
+
jsonrpc: '2.0',
|
|
2748
|
+
id,
|
|
2749
|
+
result: {},
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
default:
|
|
2753
|
+
if (method?.startsWith('notifications/')) {
|
|
2754
|
+
return null;
|
|
2755
|
+
}
|
|
2756
|
+
return {
|
|
2757
|
+
jsonrpc: '2.0',
|
|
2758
|
+
id,
|
|
2759
|
+
error: {
|
|
2760
|
+
code: -32601,
|
|
2761
|
+
message: `Unknown method: ${method}`,
|
|
2762
|
+
},
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// Main loop
|
|
2768
|
+
const rl = readline.createInterface({
|
|
2769
|
+
input: process.stdin,
|
|
2770
|
+
terminal: false,
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
let pendingRequests = 0;
|
|
2774
|
+
let closing = false;
|
|
2775
|
+
|
|
2776
|
+
function checkExit() {
|
|
2777
|
+
if (closing && pendingRequests === 0) {
|
|
2778
|
+
process.exit(0);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
rl.on('line', async (line) => {
|
|
2783
|
+
pendingRequests++;
|
|
2784
|
+
try {
|
|
2785
|
+
const message = JSON.parse(line);
|
|
2786
|
+
const response = await processMessage(message);
|
|
2787
|
+
|
|
2788
|
+
if (response) {
|
|
2789
|
+
console.log(JSON.stringify(response));
|
|
2790
|
+
}
|
|
2791
|
+
} catch (err) {
|
|
2792
|
+
console.log(JSON.stringify({
|
|
2793
|
+
jsonrpc: '2.0',
|
|
2794
|
+
error: {
|
|
2795
|
+
code: -32700,
|
|
2796
|
+
message: `Parse error: ${err.message}`,
|
|
2797
|
+
},
|
|
2798
|
+
}));
|
|
2799
|
+
} finally {
|
|
2800
|
+
pendingRequests--;
|
|
2801
|
+
checkExit();
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
rl.on('close', () => {
|
|
2806
|
+
closing = true;
|
|
2807
|
+
checkExit();
|
|
2808
|
+
});
|