@studiometa/forge-mcp 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +152 -0
  2. package/dist/auth.d.ts +15 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +18 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/errors.d.ts +36 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/formatters.d.ts +187 -0
  9. package/dist/formatters.d.ts.map +1 -0
  10. package/dist/handlers/backups.d.ts +2 -0
  11. package/dist/handlers/backups.d.ts.map +1 -0
  12. package/dist/handlers/certificates.d.ts +2 -0
  13. package/dist/handlers/certificates.d.ts.map +1 -0
  14. package/dist/handlers/commands.d.ts +2 -0
  15. package/dist/handlers/commands.d.ts.map +1 -0
  16. package/dist/handlers/daemons.d.ts +2 -0
  17. package/dist/handlers/daemons.d.ts.map +1 -0
  18. package/dist/handlers/database-users.d.ts +2 -0
  19. package/dist/handlers/database-users.d.ts.map +1 -0
  20. package/dist/handlers/databases.d.ts +2 -0
  21. package/dist/handlers/databases.d.ts.map +1 -0
  22. package/dist/handlers/deployments.d.ts +9 -0
  23. package/dist/handlers/deployments.d.ts.map +1 -0
  24. package/dist/handlers/env.d.ts +2 -0
  25. package/dist/handlers/env.d.ts.map +1 -0
  26. package/dist/handlers/factory.d.ts +71 -0
  27. package/dist/handlers/factory.d.ts.map +1 -0
  28. package/dist/handlers/firewall-rules.d.ts +2 -0
  29. package/dist/handlers/firewall-rules.d.ts.map +1 -0
  30. package/dist/handlers/help.d.ts +16 -0
  31. package/dist/handlers/help.d.ts.map +1 -0
  32. package/dist/handlers/index.d.ts +20 -0
  33. package/dist/handlers/index.d.ts.map +1 -0
  34. package/dist/handlers/monitors.d.ts +2 -0
  35. package/dist/handlers/monitors.d.ts.map +1 -0
  36. package/dist/handlers/nginx-config.d.ts +2 -0
  37. package/dist/handlers/nginx-config.d.ts.map +1 -0
  38. package/dist/handlers/nginx-templates.d.ts +2 -0
  39. package/dist/handlers/nginx-templates.d.ts.map +1 -0
  40. package/dist/handlers/recipes.d.ts +2 -0
  41. package/dist/handlers/recipes.d.ts.map +1 -0
  42. package/dist/handlers/redirect-rules.d.ts +2 -0
  43. package/dist/handlers/redirect-rules.d.ts.map +1 -0
  44. package/dist/handlers/scheduled-jobs.d.ts +2 -0
  45. package/dist/handlers/scheduled-jobs.d.ts.map +1 -0
  46. package/dist/handlers/schema.d.ts +16 -0
  47. package/dist/handlers/schema.d.ts.map +1 -0
  48. package/dist/handlers/security-rules.d.ts +2 -0
  49. package/dist/handlers/security-rules.d.ts.map +1 -0
  50. package/dist/handlers/servers.d.ts +2 -0
  51. package/dist/handlers/servers.d.ts.map +1 -0
  52. package/dist/handlers/sites.d.ts +2 -0
  53. package/dist/handlers/sites.d.ts.map +1 -0
  54. package/dist/handlers/ssh-keys.d.ts +2 -0
  55. package/dist/handlers/ssh-keys.d.ts.map +1 -0
  56. package/dist/handlers/types.d.ts +38 -0
  57. package/dist/handlers/types.d.ts.map +1 -0
  58. package/dist/handlers/user.d.ts +2 -0
  59. package/dist/handlers/user.d.ts.map +1 -0
  60. package/dist/handlers/utils.d.ts +29 -0
  61. package/dist/handlers/utils.d.ts.map +1 -0
  62. package/dist/hints.d.ts +60 -0
  63. package/dist/hints.d.ts.map +1 -0
  64. package/dist/http-CfjqK_e4.js +277 -0
  65. package/dist/http-CfjqK_e4.js.map +1 -0
  66. package/dist/http.d.ts +55 -0
  67. package/dist/http.d.ts.map +1 -0
  68. package/dist/http.js +3 -0
  69. package/dist/index.d.ts +44 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +4 -0
  72. package/dist/instructions.d.ts +11 -0
  73. package/dist/instructions.d.ts.map +1 -0
  74. package/dist/server.d.ts +35 -0
  75. package/dist/server.d.ts.map +1 -0
  76. package/dist/server.js +76 -0
  77. package/dist/server.js.map +1 -0
  78. package/dist/sessions.d.ts +64 -0
  79. package/dist/sessions.d.ts.map +1 -0
  80. package/dist/src-BdwavqrN.js +189 -0
  81. package/dist/src-BdwavqrN.js.map +1 -0
  82. package/dist/stdio.d.ts +36 -0
  83. package/dist/stdio.d.ts.map +1 -0
  84. package/dist/tools.d.ts +47 -0
  85. package/dist/tools.d.ts.map +1 -0
  86. package/dist/version-DaD5zvGh.js +3470 -0
  87. package/dist/version-DaD5zvGh.js.map +1 -0
  88. package/dist/version.d.ts +2 -0
  89. package/dist/version.d.ts.map +1 -0
  90. package/package.json +53 -1
  91. package/skills/SKILL.md +219 -0
@@ -0,0 +1,3470 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { HttpClient, isForgeApiError } from "@studiometa/forge-api";
5
+ import { RESOURCES, activateCertificate, createAuditLogger, createBackupConfig, createCertificate, createCommand, createDaemon, createDatabase, createDatabaseUser, createFirewallRule, createMonitor, createNginxTemplate, createRecipe, createRedirectRule, createScheduledJob, createSecurityRule, createServer, createSite, createSshKey, deleteBackupConfig, deleteCertificate, deleteDaemon, deleteDatabase, deleteDatabaseUser, deleteFirewallRule, deleteMonitor, deleteNginxTemplate, deleteRecipe, deleteRedirectRule, deleteScheduledJob, deleteSecurityRule, deleteServer, deleteSite, deleteSshKey, deploySiteAndWait, getBackupConfig, getCertificate, getCommand, getDaemon, getDatabase, getDatabaseUser, getDeploymentOutput, getDeploymentScript, getEnv, getFirewallRule, getMonitor, getNginxConfig, getNginxTemplate, getRecipe, getRedirectRule, getScheduledJob, getSecurityRule, getServer, getSite, getSshKey, getUser, listBackupConfigs, listCertificates, listCommands, listDaemons, listDatabaseUsers, listDatabases, listDeployments, listFirewallRules, listMonitors, listNginxTemplates, listRecipes, listRedirectRules, listScheduledJobs, listSecurityRules, listServers, listSites, listSshKeys, rebootServer, restartDaemon, runRecipe, updateDeploymentScript, updateEnv, updateNginxConfig, updateNginxTemplate } from "@studiometa/forge-core";
6
+ /**
7
+ * MCP Server Instructions
8
+ *
9
+ * These instructions are sent to MCP clients during initialization
10
+ * and used as context/hints for the LLM. Ensures the AI agent
11
+ * knows how to properly use the Forge MCP server.
12
+ *
13
+ * The content is derived from skills/SKILL.md (without YAML frontmatter).
14
+ */
15
+ var __dirname = dirname(fileURLToPath(import.meta.url));
16
+ /**
17
+ * Load instructions from SKILL.md file.
18
+ * Removes YAML frontmatter (content between --- markers).
19
+ */
20
+ function loadInstructions() {
21
+ try {
22
+ return readFileSync(join(__dirname, "..", "skills", "SKILL.md"), "utf-8").replace(/^---\n[\s\S]*?\n---\n+/, "").trim();
23
+ } catch {
24
+ return "Laravel Forge MCP Server - Use the forge tool with resource and action parameters. Call action=\"help\" for documentation.";
25
+ }
26
+ }
27
+ const INSTRUCTIONS = loadInstructions();
28
+ /**
29
+ * Format a list of servers.
30
+ */
31
+ function formatServerList(servers) {
32
+ if (servers.length === 0) return "No servers found.";
33
+ const lines = servers.map((s) => `• ${s.name} (ID: ${s.id}) — ${s.provider} ${s.region} — ${s.ip_address} — ${s.is_ready ? "ready" : "provisioning"}`);
34
+ return `${servers.length} server(s):\n${lines.join("\n")}`;
35
+ }
36
+ /**
37
+ * Format a single server.
38
+ */
39
+ function formatServer(server) {
40
+ return [
41
+ `Server: ${server.name} (ID: ${server.id})`,
42
+ `Provider: ${server.provider} (${server.region})`,
43
+ `IP: ${server.ip_address}`,
44
+ `PHP: ${server.php_version}`,
45
+ `Ubuntu: ${server.ubuntu_version}`,
46
+ `Status: ${server.is_ready ? "ready" : "provisioning"}`,
47
+ `Created: ${server.created_at}`
48
+ ].join("\n");
49
+ }
50
+ /**
51
+ * Format a list of sites.
52
+ */
53
+ function formatSiteList(sites, serverId) {
54
+ if (sites.length === 0) return serverId ? `No sites on server ${serverId}.` : "No sites found.";
55
+ const lines = sites.map((s) => `• ${s.name} (ID: ${s.id}) — ${s.project_type} — ${s.status}`);
56
+ return `${serverId ? `${sites.length} site(s) on server ${serverId}:` : `${sites.length} site(s):`}\n${lines.join("\n")}`;
57
+ }
58
+ /**
59
+ * Format a single site.
60
+ */
61
+ function formatSite(site) {
62
+ return [
63
+ `Site: ${site.name} (ID: ${site.id})`,
64
+ `Type: ${site.project_type}`,
65
+ `Directory: ${site.directory}`,
66
+ `Repository: ${site.repository ?? "none"}`,
67
+ `Branch: ${site.repository_branch ?? "none"}`,
68
+ `Status: ${site.status}`,
69
+ `Deploy status: ${site.deployment_status ?? "none"}`,
70
+ `Quick deploy: ${site.quick_deploy ? "enabled" : "disabled"}`,
71
+ `PHP: ${site.php_version}`,
72
+ `Created: ${site.created_at}`
73
+ ].join("\n");
74
+ }
75
+ /**
76
+ * Format a list of databases.
77
+ */
78
+ function formatDatabaseList(databases) {
79
+ if (databases.length === 0) return "No databases found.";
80
+ const lines = databases.map((d) => `• ${d.name} (ID: ${d.id}) — ${d.status}`);
81
+ return `${databases.length} database(s):\n${lines.join("\n")}`;
82
+ }
83
+ /**
84
+ * Format a single database.
85
+ */
86
+ function formatDatabase(db) {
87
+ return `Database: ${db.name} (ID: ${db.id})\nStatus: ${db.status}\nCreated: ${db.created_at}`;
88
+ }
89
+ /**
90
+ * Format a list of database users.
91
+ */
92
+ function formatDatabaseUserList(users) {
93
+ if (users.length === 0) return "No database users found.";
94
+ const lines = users.map((u) => `• ${u.name} (ID: ${u.id}) — ${u.status}`);
95
+ return `${users.length} database user(s):\n${lines.join("\n")}`;
96
+ }
97
+ /**
98
+ * Format a single database user.
99
+ */
100
+ function formatDatabaseUser(user) {
101
+ return [
102
+ `Database User: ${user.name} (ID: ${user.id})`,
103
+ `Status: ${user.status}`,
104
+ `Databases: ${user.databases.length > 0 ? user.databases.join(", ") : "none"}`,
105
+ `Created: ${user.created_at}`
106
+ ].join("\n");
107
+ }
108
+ /**
109
+ * Format a list of deployments.
110
+ */
111
+ function formatDeploymentList(deployments) {
112
+ if (deployments.length === 0) return "No deployments found.";
113
+ const lines = deployments.map((d) => `• #${d.id} — ${d.status} — ${d.commit_hash?.slice(0, 7) ?? "no commit"} — ${d.started_at}`);
114
+ return `${deployments.length} deployment(s):\n${lines.join("\n")}`;
115
+ }
116
+ /**
117
+ * Format a deployment action result.
118
+ *
119
+ * When a `DeployResult` is provided the output includes status, elapsed time,
120
+ * and the deployment log. When called with just IDs (legacy) it falls back to
121
+ * the simple confirmation message so existing tests keep passing.
122
+ */
123
+ function formatDeployAction(siteId, serverId, result) {
124
+ if (!result) return `Deployment triggered for site ${siteId} on server ${serverId}.`;
125
+ const elapsedSec = (result.elapsed_ms / 1e3).toFixed(1);
126
+ const lines = [`Deployment ${result.status === "success" ? "✓ succeeded" : "✗ failed"} for site ${siteId} on server ${serverId} (${elapsedSec}s).`];
127
+ if (result.log) lines.push("", "Deployment log:", result.log);
128
+ return lines.join("\n");
129
+ }
130
+ /**
131
+ * Format deployment script content.
132
+ */
133
+ function formatDeploymentScript(script) {
134
+ return `Deployment script:\n${script}`;
135
+ }
136
+ /**
137
+ * Format deployment output.
138
+ */
139
+ function formatDeploymentOutput(deploymentId, output) {
140
+ return `Deployment ${deploymentId} output:\n${output}`;
141
+ }
142
+ /**
143
+ * Format a deployment script update confirmation.
144
+ */
145
+ function formatDeploymentScriptUpdated(siteId, serverId) {
146
+ return `Deployment script updated for site ${siteId} on server ${serverId}.`;
147
+ }
148
+ /**
149
+ * Format a list of certificates.
150
+ */
151
+ function formatCertificateList(certificates) {
152
+ if (certificates.length === 0) return "No certificates found.";
153
+ const lines = certificates.map((c) => `• ${c.domain} (ID: ${c.id}) — ${c.type} — ${c.active ? "active" : "inactive"} — ${c.status}`);
154
+ return `${certificates.length} certificate(s):\n${lines.join("\n")}`;
155
+ }
156
+ /**
157
+ * Format a single certificate.
158
+ */
159
+ function formatCertificate(cert) {
160
+ return `Certificate: ${cert.domain} (ID: ${cert.id})\nType: ${cert.type}\nStatus: ${cert.status}\nActive: ${cert.active}`;
161
+ }
162
+ /**
163
+ * Format a list of daemons.
164
+ */
165
+ function formatDaemonList(daemons) {
166
+ if (daemons.length === 0) return "No daemons found.";
167
+ const lines = daemons.map((d) => `• ${d.command} (ID: ${d.id}) — user: ${d.user} — ${d.status}`);
168
+ return `${daemons.length} daemon(s):\n${lines.join("\n")}`;
169
+ }
170
+ /**
171
+ * Format a single daemon.
172
+ */
173
+ function formatDaemon(daemon) {
174
+ return `Daemon: ${daemon.command} (ID: ${daemon.id})\nUser: ${daemon.user}\nProcesses: ${daemon.processes}\nStatus: ${daemon.status}`;
175
+ }
176
+ /**
177
+ * Format a list of firewall rules.
178
+ */
179
+ function formatFirewallRuleList(rules) {
180
+ if (rules.length === 0) return "No firewall rules found.";
181
+ const lines = rules.map((r) => `• ${r.name} (ID: ${r.id}) — port: ${r.port} — ${r.ip_address} — ${r.status}`);
182
+ return `${rules.length} firewall rule(s):\n${lines.join("\n")}`;
183
+ }
184
+ /**
185
+ * Format a single firewall rule.
186
+ */
187
+ function formatFirewallRule(rule) {
188
+ return `Firewall Rule: ${rule.name} (ID: ${rule.id})\nPort: ${rule.port}\nType: ${rule.type}\nIP: ${rule.ip_address}\nStatus: ${rule.status}`;
189
+ }
190
+ /**
191
+ * Format a list of monitors.
192
+ */
193
+ function formatMonitorList(monitors) {
194
+ if (monitors.length === 0) return "No monitors found.";
195
+ const lines = monitors.map((m) => `• ${m.type} ${m.operator} ${m.threshold} (ID: ${m.id}) — ${m.state}`);
196
+ return `${monitors.length} monitor(s):\n${lines.join("\n")}`;
197
+ }
198
+ /**
199
+ * Format a single monitor.
200
+ */
201
+ function formatMonitor(monitor) {
202
+ return `Monitor: ${monitor.type} ${monitor.operator} ${monitor.threshold} (ID: ${monitor.id})\nState: ${monitor.state}\nMinutes: ${monitor.minutes}`;
203
+ }
204
+ /**
205
+ * Format a list of SSH keys.
206
+ */
207
+ function formatSshKeyList(keys) {
208
+ if (keys.length === 0) return "No SSH keys found.";
209
+ const lines = keys.map((k) => `• ${k.name} (ID: ${k.id}) — ${k.status}`);
210
+ return `${keys.length} SSH key(s):\n${lines.join("\n")}`;
211
+ }
212
+ /**
213
+ * Format a single SSH key.
214
+ */
215
+ function formatSshKey(key) {
216
+ return `SSH Key: ${key.name} (ID: ${key.id})\nStatus: ${key.status}\nCreated: ${key.created_at}`;
217
+ }
218
+ /**
219
+ * Format a list of scheduled jobs.
220
+ */
221
+ function formatScheduledJobList(jobs) {
222
+ if (jobs.length === 0) return "No scheduled jobs found.";
223
+ const lines = jobs.map((j) => `• ${j.command} (ID: ${j.id}) — ${j.frequency} — ${j.status} — user: ${j.user}`);
224
+ return `${jobs.length} scheduled job(s):\n${lines.join("\n")}`;
225
+ }
226
+ /**
227
+ * Format a single scheduled job.
228
+ */
229
+ function formatScheduledJob(job) {
230
+ return [
231
+ `Job: ${job.command} (ID: ${job.id})`,
232
+ `User: ${job.user}`,
233
+ `Frequency: ${job.frequency}`,
234
+ `Cron: ${job.cron}`,
235
+ `Status: ${job.status}`,
236
+ `Created: ${job.created_at}`
237
+ ].join("\n");
238
+ }
239
+ /**
240
+ * Format a list of security rules.
241
+ */
242
+ function formatSecurityRuleList(rules) {
243
+ if (rules.length === 0) return "No security rules found.";
244
+ const lines = rules.map((r) => `• ${r.name} (ID: ${r.id}) — path: ${r.path ?? "/"}`);
245
+ return `${rules.length} security rule(s):\n${lines.join("\n")}`;
246
+ }
247
+ /**
248
+ * Format a single security rule.
249
+ */
250
+ function formatSecurityRule(rule) {
251
+ return `Security Rule: ${rule.name} (ID: ${rule.id})\nPath: ${rule.path ?? "/"}`;
252
+ }
253
+ /**
254
+ * Format a list of redirect rules.
255
+ */
256
+ function formatRedirectRuleList(rules) {
257
+ if (rules.length === 0) return "No redirect rules found.";
258
+ const lines = rules.map((r) => `• ${r.from} → ${r.to} (ID: ${r.id}) — ${r.type}`);
259
+ return `${rules.length} redirect rule(s):\n${lines.join("\n")}`;
260
+ }
261
+ /**
262
+ * Format a single redirect rule.
263
+ */
264
+ function formatRedirectRule(rule) {
265
+ return `Redirect Rule: ${rule.from} → ${rule.to} (ID: ${rule.id})\nType: ${rule.type}`;
266
+ }
267
+ /**
268
+ * Format nginx configuration content.
269
+ */
270
+ function formatNginxConfig(content) {
271
+ return `Nginx configuration:\n${content}`;
272
+ }
273
+ /**
274
+ * Format a list of nginx templates.
275
+ */
276
+ function formatNginxTemplateList(templates) {
277
+ if (templates.length === 0) return "No nginx templates found.";
278
+ const lines = templates.map((t) => `• ${t.name} (ID: ${t.id})`);
279
+ return `${templates.length} nginx template(s):\n${lines.join("\n")}`;
280
+ }
281
+ /**
282
+ * Format a single nginx template.
283
+ */
284
+ function formatNginxTemplate(template) {
285
+ return `Nginx Template: ${template.name} (ID: ${template.id})\n\n${template.content}`;
286
+ }
287
+ /**
288
+ * Format a list of backup configurations.
289
+ */
290
+ function formatBackupConfigList(backups) {
291
+ if (backups.length === 0) return "No backup configurations found.";
292
+ const lines = backups.map((b) => `• ${b.provider_name} (ID: ${b.id}) — ${b.frequency} — ${b.status} — last: ${b.last_backup_time ?? "never"}`);
293
+ return `${backups.length} backup config(s):\n${lines.join("\n")}`;
294
+ }
295
+ /**
296
+ * Format a single backup configuration.
297
+ */
298
+ function formatBackupConfig(backup) {
299
+ return [
300
+ `Backup Config: ${backup.provider_name} (ID: ${backup.id})`,
301
+ `Frequency: ${backup.frequency}`,
302
+ `Status: ${backup.status}`,
303
+ `Retention: ${backup.retention} backups`,
304
+ `Databases: ${backup.databases.map((d) => d.name).join(", ") || "none"}`,
305
+ `Last backup: ${backup.last_backup_time ?? "never"}`
306
+ ].join("\n");
307
+ }
308
+ /**
309
+ * Format a list of recipes.
310
+ */
311
+ function formatRecipeList(recipes) {
312
+ if (recipes.length === 0) return "No recipes found.";
313
+ const lines = recipes.map((r) => `• ${r.name} (ID: ${r.id}) — user: ${r.user}`);
314
+ return `${recipes.length} recipe(s):\n${lines.join("\n")}`;
315
+ }
316
+ /**
317
+ * Format a single recipe.
318
+ */
319
+ function formatRecipe(recipe) {
320
+ return `Recipe: ${recipe.name} (ID: ${recipe.id})\nUser: ${recipe.user}\nScript:\n${recipe.script}`;
321
+ }
322
+ /**
323
+ * Format a list of commands.
324
+ */
325
+ function formatCommandList(commands) {
326
+ if (commands.length === 0) return "No commands found.";
327
+ const lines = commands.map((c) => `• #${c.id} — ${c.status} — ${c.user_name} — ${c.command.slice(0, 60)}`);
328
+ return `${commands.length} command(s):\n${lines.join("\n")}`;
329
+ }
330
+ /**
331
+ * Format a single command.
332
+ */
333
+ function formatCommand(command) {
334
+ return [
335
+ `Command #${command.id}`,
336
+ `Command: ${command.command}`,
337
+ `Status: ${command.status}`,
338
+ `User: ${command.user_name}`,
339
+ `Created: ${command.created_at}`
340
+ ].join("\n");
341
+ }
342
+ /**
343
+ * Format environment variables content.
344
+ */
345
+ function formatEnv(content) {
346
+ return `Environment variables:\n${content}`;
347
+ }
348
+ /**
349
+ * Format the authenticated user.
350
+ */
351
+ function formatUser(user) {
352
+ return [
353
+ `User: ${user.name} (ID: ${user.id})`,
354
+ `Email: ${user.email}`,
355
+ `GitHub: ${user.connected_to_github ? "connected" : "not connected"}`,
356
+ `GitLab: ${user.connected_to_gitlab ? "connected" : "not connected"}`,
357
+ `2FA: ${user.two_factor_enabled ? "enabled" : "disabled"}`
358
+ ].join("\n");
359
+ }
360
+ /**
361
+ * Custom error classes for MCP server
362
+ *
363
+ * These provide structured error handling with LLM-friendly messages
364
+ * that include guidance on how to resolve issues.
365
+ */
366
+ /**
367
+ * Error thrown when user input validation fails.
368
+ * These errors should be returned to the user directly.
369
+ *
370
+ * Includes optional hints for how to resolve the issue.
371
+ */
372
+ var UserInputError = class extends Error {
373
+ hints;
374
+ constructor(message, hints) {
375
+ super(message);
376
+ this.name = "UserInputError";
377
+ this.hints = hints;
378
+ }
379
+ /**
380
+ * Format error message with hints for LLM consumption
381
+ */
382
+ toFormattedMessage() {
383
+ let msg = `**Input Error:** ${this.message}`;
384
+ if (this.hints && this.hints.length > 0) msg += "\n\n**Hints:**\n" + this.hints.map((h) => `- ${h}`).join("\n");
385
+ return msg;
386
+ }
387
+ };
388
+ /**
389
+ * Check if an error is a UserInputError
390
+ */
391
+ function isUserInputError(error) {
392
+ return error instanceof UserInputError;
393
+ }
394
+ /**
395
+ * Create a successful result with both human-readable text and structured content.
396
+ *
397
+ * - When `data` is a string, `structuredContent` wraps it as `{ result: data }`.
398
+ * - When `data` is an object/array, `structuredContent` is `{ result: data }` and
399
+ * the text representation is the JSON-serialized form.
400
+ */
401
+ function jsonResult(data) {
402
+ return {
403
+ content: [{
404
+ type: "text",
405
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
406
+ }],
407
+ structuredContent: {
408
+ success: true,
409
+ result: data
410
+ }
411
+ };
412
+ }
413
+ /**
414
+ * Validate an ID-like value (must be alphanumeric/dashes only).
415
+ * Prevents path traversal via `../` in URL segments.
416
+ *
417
+ * @returns true if the value is safe, false otherwise.
418
+ */
419
+ function sanitizeId(value) {
420
+ return /^[\w-]+$/.test(value);
421
+ }
422
+ /**
423
+ * Create an error result with structured error content.
424
+ */
425
+ function errorResult(message) {
426
+ return {
427
+ content: [{
428
+ type: "text",
429
+ text: `Error: ${message}`
430
+ }],
431
+ structuredContent: {
432
+ success: false,
433
+ error: message
434
+ },
435
+ isError: true
436
+ };
437
+ }
438
+ /**
439
+ * Create a resource handler from configuration.
440
+ *
441
+ * Returns a function that routes actions to the correct executor,
442
+ * validates required fields, and formats results.
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * export const handleDatabases = createResourceHandler({
447
+ * resource: 'databases',
448
+ * actions: ['list', 'get', 'create', 'delete'],
449
+ * requiredFields: {
450
+ * list: ['server_id'],
451
+ * get: ['server_id', 'id'],
452
+ * create: ['server_id', 'name'],
453
+ * delete: ['server_id', 'id'],
454
+ * },
455
+ * executors: {
456
+ * list: listDatabases,
457
+ * get: getDatabase,
458
+ * create: createDatabase,
459
+ * delete: deleteDatabase,
460
+ * },
461
+ * formatResult: (action, data) => {
462
+ * if (action === 'list') return formatDatabaseList(data);
463
+ * if (action === 'get') return formatDatabase(data);
464
+ * return 'Done.';
465
+ * },
466
+ * });
467
+ * ```
468
+ */
469
+ function createResourceHandler(config) {
470
+ const { resource, actions, requiredFields = {}, executors, hints, mapOptions, formatResult } = config;
471
+ return async (action, args, ctx) => {
472
+ if (!actions.includes(action)) return errorResult(`Unknown action "${action}" for ${resource}. Valid actions: ${actions.join(", ")}.`);
473
+ const required = requiredFields[action] ?? [];
474
+ for (const field of required) if (!args[field]) return errorResult(`Missing required field: ${field}`);
475
+ for (const field of [
476
+ "id",
477
+ "server_id",
478
+ "site_id"
479
+ ]) {
480
+ const value = args[field];
481
+ if (value !== void 0 && !sanitizeId(String(value))) return errorResult(`Invalid ${field}: "${value}". IDs must be alphanumeric.`);
482
+ }
483
+ const executor = executors[action];
484
+ if (!executor) return errorResult(`Action "${action}" is not yet implemented for ${resource}.`);
485
+ let options;
486
+ if (mapOptions) options = mapOptions(action, args);
487
+ else {
488
+ const { resource: _r, action: _a, compact: _c, ...rest } = args;
489
+ options = rest;
490
+ }
491
+ const result = await executor(options, ctx.executorContext);
492
+ if (result.data === void 0) return jsonResult(formatResult ? formatResult(action, void 0, args) : "Done.");
493
+ if (ctx.compact && formatResult) return jsonResult(formatResult(action, result.data, args));
494
+ if (action === "get" && ctx.includeHints && hints) {
495
+ /* v8 ignore start */
496
+ const id = args.id ?? args.server_id ?? "";
497
+ return jsonResult({
498
+ ...result.data,
499
+ _hints: hints(result.data, String(id))
500
+ });
501
+ }
502
+ return jsonResult(result.data);
503
+ };
504
+ }
505
+ const handleBackups = createResourceHandler({
506
+ resource: "backups",
507
+ actions: [
508
+ "list",
509
+ "get",
510
+ "create",
511
+ "delete"
512
+ ],
513
+ requiredFields: {
514
+ list: ["server_id"],
515
+ get: ["server_id", "id"],
516
+ create: [
517
+ "server_id",
518
+ "provider",
519
+ "credentials",
520
+ "frequency",
521
+ "databases"
522
+ ],
523
+ delete: ["server_id", "id"]
524
+ },
525
+ executors: {
526
+ list: listBackupConfigs,
527
+ get: getBackupConfig,
528
+ create: createBackupConfig,
529
+ delete: deleteBackupConfig
530
+ },
531
+ formatResult: (action, data, args) => {
532
+ switch (action) {
533
+ case "list": return formatBackupConfigList(data);
534
+ case "get": return formatBackupConfig(data);
535
+ case "create": return formatBackupConfig(data);
536
+ case "delete": return `Backup config ${args.id} deleted.`;
537
+ default: return "Done.";
538
+ }
539
+ }
540
+ });
541
+ /**
542
+ * Hints after getting a server.
543
+ */
544
+ function getServerHints(serverId) {
545
+ return {
546
+ related_resources: [
547
+ {
548
+ resource: "sites",
549
+ description: "List sites on this server",
550
+ example: {
551
+ resource: "sites",
552
+ action: "list",
553
+ server_id: serverId
554
+ }
555
+ },
556
+ {
557
+ resource: "databases",
558
+ description: "List databases",
559
+ example: {
560
+ resource: "databases",
561
+ action: "list",
562
+ server_id: serverId
563
+ }
564
+ },
565
+ {
566
+ resource: "daemons",
567
+ description: "List background processes",
568
+ example: {
569
+ resource: "daemons",
570
+ action: "list",
571
+ server_id: serverId
572
+ }
573
+ },
574
+ {
575
+ resource: "firewall-rules",
576
+ description: "List firewall rules",
577
+ example: {
578
+ resource: "firewall-rules",
579
+ action: "list",
580
+ server_id: serverId
581
+ }
582
+ },
583
+ {
584
+ resource: "ssh-keys",
585
+ description: "List SSH keys on this server",
586
+ example: {
587
+ resource: "ssh-keys",
588
+ action: "list",
589
+ server_id: serverId
590
+ }
591
+ }
592
+ ],
593
+ common_actions: [{
594
+ action: "Reboot server",
595
+ example: {
596
+ resource: "servers",
597
+ action: "reboot",
598
+ id: serverId
599
+ }
600
+ }]
601
+ };
602
+ }
603
+ /**
604
+ * Hints after getting a site.
605
+ */
606
+ function getSiteHints(serverId, siteId) {
607
+ return {
608
+ related_resources: [
609
+ {
610
+ resource: "deployments",
611
+ description: "List deployments for this site",
612
+ example: {
613
+ resource: "deployments",
614
+ action: "list",
615
+ server_id: serverId,
616
+ site_id: siteId
617
+ }
618
+ },
619
+ {
620
+ resource: "env",
621
+ description: "Get environment variables",
622
+ example: {
623
+ resource: "env",
624
+ action: "get",
625
+ server_id: serverId,
626
+ site_id: siteId
627
+ }
628
+ },
629
+ {
630
+ resource: "certificates",
631
+ description: "List SSL certificates",
632
+ example: {
633
+ resource: "certificates",
634
+ action: "list",
635
+ server_id: serverId,
636
+ site_id: siteId
637
+ }
638
+ },
639
+ {
640
+ resource: "nginx",
641
+ description: "Get nginx configuration",
642
+ example: {
643
+ resource: "nginx",
644
+ action: "get",
645
+ server_id: serverId,
646
+ site_id: siteId
647
+ }
648
+ }
649
+ ],
650
+ common_actions: [{
651
+ action: "Deploy this site",
652
+ example: {
653
+ resource: "deployments",
654
+ action: "deploy",
655
+ server_id: serverId,
656
+ site_id: siteId
657
+ }
658
+ }]
659
+ };
660
+ }
661
+ /**
662
+ * Hints after getting a database.
663
+ */
664
+ function getDatabaseHints(serverId, databaseId) {
665
+ return {
666
+ related_resources: [{
667
+ resource: "databases",
668
+ description: "List all databases on this server",
669
+ example: {
670
+ resource: "databases",
671
+ action: "list",
672
+ server_id: serverId
673
+ }
674
+ }],
675
+ common_actions: [{
676
+ action: "Delete this database",
677
+ example: {
678
+ resource: "databases",
679
+ action: "delete",
680
+ server_id: serverId,
681
+ id: databaseId
682
+ }
683
+ }]
684
+ };
685
+ }
686
+ /**
687
+ * Hints after getting a database user.
688
+ */
689
+ function getDatabaseUserHints(serverId, userId) {
690
+ return {
691
+ related_resources: [{
692
+ resource: "database-users",
693
+ description: "List all database users on this server",
694
+ example: {
695
+ resource: "database-users",
696
+ action: "list",
697
+ server_id: serverId
698
+ }
699
+ }, {
700
+ resource: "databases",
701
+ description: "List databases on this server",
702
+ example: {
703
+ resource: "databases",
704
+ action: "list",
705
+ server_id: serverId
706
+ }
707
+ }],
708
+ common_actions: [{
709
+ action: "Delete this database user",
710
+ example: {
711
+ resource: "database-users",
712
+ action: "delete",
713
+ server_id: serverId,
714
+ id: userId
715
+ }
716
+ }]
717
+ };
718
+ }
719
+ /**
720
+ * Hints after getting a daemon.
721
+ */
722
+ function getDaemonHints(serverId, daemonId) {
723
+ return {
724
+ related_resources: [{
725
+ resource: "daemons",
726
+ description: "List all daemons on this server",
727
+ example: {
728
+ resource: "daemons",
729
+ action: "list",
730
+ server_id: serverId
731
+ }
732
+ }],
733
+ common_actions: [{
734
+ action: "Restart this daemon",
735
+ example: {
736
+ resource: "daemons",
737
+ action: "restart",
738
+ server_id: serverId,
739
+ id: daemonId
740
+ }
741
+ }, {
742
+ action: "Delete this daemon",
743
+ example: {
744
+ resource: "daemons",
745
+ action: "delete",
746
+ server_id: serverId,
747
+ id: daemonId
748
+ }
749
+ }]
750
+ };
751
+ }
752
+ /**
753
+ * Hints after getting a certificate.
754
+ */
755
+ function getCertificateHints(serverId, siteId, certificateId) {
756
+ return {
757
+ related_resources: [{
758
+ resource: "certificates",
759
+ description: "List all certificates for this site",
760
+ example: {
761
+ resource: "certificates",
762
+ action: "list",
763
+ server_id: serverId,
764
+ site_id: siteId
765
+ }
766
+ }],
767
+ common_actions: [{
768
+ action: "Activate this certificate",
769
+ example: {
770
+ resource: "certificates",
771
+ action: "activate",
772
+ server_id: serverId,
773
+ site_id: siteId,
774
+ id: certificateId
775
+ }
776
+ }, {
777
+ action: "Delete this certificate",
778
+ example: {
779
+ resource: "certificates",
780
+ action: "delete",
781
+ server_id: serverId,
782
+ site_id: siteId,
783
+ id: certificateId
784
+ }
785
+ }]
786
+ };
787
+ }
788
+ /**
789
+ * Hints after getting a firewall rule.
790
+ */
791
+ function getFirewallRuleHints(serverId, ruleId) {
792
+ return {
793
+ related_resources: [{
794
+ resource: "firewall-rules",
795
+ description: "List all firewall rules on this server",
796
+ example: {
797
+ resource: "firewall-rules",
798
+ action: "list",
799
+ server_id: serverId
800
+ }
801
+ }],
802
+ common_actions: [{
803
+ action: "Delete this firewall rule",
804
+ example: {
805
+ resource: "firewall-rules",
806
+ action: "delete",
807
+ server_id: serverId,
808
+ id: ruleId
809
+ }
810
+ }]
811
+ };
812
+ }
813
+ /**
814
+ * Hints after getting an SSH key.
815
+ */
816
+ function getSshKeyHints(serverId, keyId) {
817
+ return {
818
+ related_resources: [{
819
+ resource: "ssh-keys",
820
+ description: "List all SSH keys on this server",
821
+ example: {
822
+ resource: "ssh-keys",
823
+ action: "list",
824
+ server_id: serverId
825
+ }
826
+ }],
827
+ common_actions: [{
828
+ action: "Delete this SSH key",
829
+ example: {
830
+ resource: "ssh-keys",
831
+ action: "delete",
832
+ server_id: serverId,
833
+ id: keyId
834
+ }
835
+ }]
836
+ };
837
+ }
838
+ /**
839
+ * Hints after getting a recipe.
840
+ */
841
+ function getRecipeHints(recipeId) {
842
+ return {
843
+ related_resources: [{
844
+ resource: "recipes",
845
+ description: "List all recipes",
846
+ example: {
847
+ resource: "recipes",
848
+ action: "list"
849
+ }
850
+ }],
851
+ common_actions: [{
852
+ action: "Run this recipe on servers",
853
+ example: {
854
+ resource: "recipes",
855
+ action: "run",
856
+ id: recipeId,
857
+ servers: []
858
+ }
859
+ }, {
860
+ action: "Delete this recipe",
861
+ example: {
862
+ resource: "recipes",
863
+ action: "delete",
864
+ id: recipeId
865
+ }
866
+ }]
867
+ };
868
+ }
869
+ /**
870
+ * Hints after getting an nginx template.
871
+ */
872
+ function getNginxTemplateHints(serverId, templateId) {
873
+ return {
874
+ related_resources: [{
875
+ resource: "nginx-templates",
876
+ description: "List all nginx templates on this server",
877
+ example: {
878
+ resource: "nginx-templates",
879
+ action: "list",
880
+ server_id: serverId
881
+ }
882
+ }],
883
+ common_actions: [{
884
+ action: "Update this nginx template",
885
+ example: {
886
+ resource: "nginx-templates",
887
+ action: "update",
888
+ server_id: serverId,
889
+ id: templateId,
890
+ content: "<nginx config content>"
891
+ }
892
+ }, {
893
+ action: "Delete this nginx template",
894
+ example: {
895
+ resource: "nginx-templates",
896
+ action: "delete",
897
+ server_id: serverId,
898
+ id: templateId
899
+ }
900
+ }]
901
+ };
902
+ }
903
+ const handleCertificates = createResourceHandler({
904
+ resource: "certificates",
905
+ actions: [
906
+ "list",
907
+ "get",
908
+ "create",
909
+ "delete",
910
+ "activate"
911
+ ],
912
+ requiredFields: {
913
+ list: ["server_id", "site_id"],
914
+ get: [
915
+ "server_id",
916
+ "site_id",
917
+ "id"
918
+ ],
919
+ create: [
920
+ "server_id",
921
+ "site_id",
922
+ "domain"
923
+ ],
924
+ delete: [
925
+ "server_id",
926
+ "site_id",
927
+ "id"
928
+ ],
929
+ activate: [
930
+ "server_id",
931
+ "site_id",
932
+ "id"
933
+ ]
934
+ },
935
+ executors: {
936
+ list: listCertificates,
937
+ get: getCertificate,
938
+ create: createCertificate,
939
+ delete: deleteCertificate,
940
+ activate: activateCertificate
941
+ },
942
+ hints: (data, id) => {
943
+ const cert = data;
944
+ return getCertificateHints(String(cert.server_id), String(cert.site_id), id);
945
+ },
946
+ formatResult: (action, data, args) => {
947
+ switch (action) {
948
+ case "list": return formatCertificateList(data);
949
+ case "get": return formatCertificate(data);
950
+ case "create": return formatCertificate(data);
951
+ case "delete": return `Certificate ${args.id} deleted.`;
952
+ case "activate": return `Certificate ${args.id} activated.`;
953
+ default: return "Done.";
954
+ }
955
+ }
956
+ });
957
+ const handleCommands = createResourceHandler({
958
+ resource: "commands",
959
+ actions: [
960
+ "list",
961
+ "get",
962
+ "create"
963
+ ],
964
+ requiredFields: {
965
+ list: ["server_id", "site_id"],
966
+ get: [
967
+ "server_id",
968
+ "site_id",
969
+ "id"
970
+ ],
971
+ create: [
972
+ "server_id",
973
+ "site_id",
974
+ "command"
975
+ ]
976
+ },
977
+ executors: {
978
+ list: listCommands,
979
+ get: getCommand,
980
+ create: createCommand
981
+ },
982
+ formatResult: (action, data) => {
983
+ switch (action) {
984
+ case "list": return formatCommandList(data);
985
+ case "get": return formatCommand(data);
986
+ case "create": return formatCommand(data);
987
+ default: return "Done.";
988
+ }
989
+ }
990
+ });
991
+ const handleDaemons = createResourceHandler({
992
+ resource: "daemons",
993
+ actions: [
994
+ "list",
995
+ "get",
996
+ "create",
997
+ "delete",
998
+ "restart"
999
+ ],
1000
+ requiredFields: {
1001
+ list: ["server_id"],
1002
+ get: ["server_id", "id"],
1003
+ create: ["server_id", "command"],
1004
+ delete: ["server_id", "id"],
1005
+ restart: ["server_id", "id"]
1006
+ },
1007
+ executors: {
1008
+ list: listDaemons,
1009
+ get: getDaemon,
1010
+ create: createDaemon,
1011
+ delete: deleteDaemon,
1012
+ restart: restartDaemon
1013
+ },
1014
+ hints: (data, id) => {
1015
+ const daemon = data;
1016
+ return getDaemonHints(String(daemon.server_id), id);
1017
+ },
1018
+ formatResult: (action, data, args) => {
1019
+ switch (action) {
1020
+ case "list": return formatDaemonList(data);
1021
+ case "get": return formatDaemon(data);
1022
+ case "create": return formatDaemon(data);
1023
+ case "delete": return `Daemon ${args.id} deleted.`;
1024
+ case "restart": return `Daemon ${args.id} restarted.`;
1025
+ default: return "Done.";
1026
+ }
1027
+ }
1028
+ });
1029
+ const handleDatabases = createResourceHandler({
1030
+ resource: "databases",
1031
+ actions: [
1032
+ "list",
1033
+ "get",
1034
+ "create",
1035
+ "delete"
1036
+ ],
1037
+ requiredFields: {
1038
+ list: ["server_id"],
1039
+ get: ["server_id", "id"],
1040
+ create: ["server_id", "name"],
1041
+ delete: ["server_id", "id"]
1042
+ },
1043
+ executors: {
1044
+ list: listDatabases,
1045
+ get: getDatabase,
1046
+ create: createDatabase,
1047
+ delete: deleteDatabase
1048
+ },
1049
+ hints: (data, id) => {
1050
+ const db = data;
1051
+ return getDatabaseHints(String(db.server_id), id);
1052
+ },
1053
+ formatResult: (action, data, args) => {
1054
+ switch (action) {
1055
+ case "list": return formatDatabaseList(data);
1056
+ case "get": return formatDatabase(data);
1057
+ case "create": return formatDatabase(data);
1058
+ case "delete": return `Database ${args.id} deleted.`;
1059
+ default: return "Done.";
1060
+ }
1061
+ }
1062
+ });
1063
+ const handleDatabaseUsers = createResourceHandler({
1064
+ resource: "database-users",
1065
+ actions: [
1066
+ "list",
1067
+ "get",
1068
+ "create",
1069
+ "delete"
1070
+ ],
1071
+ requiredFields: {
1072
+ list: ["server_id"],
1073
+ get: ["server_id", "id"],
1074
+ create: [
1075
+ "server_id",
1076
+ "name",
1077
+ "password"
1078
+ ],
1079
+ delete: ["server_id", "id"]
1080
+ },
1081
+ executors: {
1082
+ list: listDatabaseUsers,
1083
+ get: getDatabaseUser,
1084
+ create: createDatabaseUser,
1085
+ delete: deleteDatabaseUser
1086
+ },
1087
+ hints: (data, id) => {
1088
+ const user = data;
1089
+ return getDatabaseUserHints(String(user.server_id), id);
1090
+ },
1091
+ formatResult: (action, data, args) => {
1092
+ switch (action) {
1093
+ case "list": return formatDatabaseUserList(data);
1094
+ case "get": return formatDatabaseUser(data);
1095
+ case "create": return formatDatabaseUser(data);
1096
+ case "delete": return `Database user ${args.id} deleted.`;
1097
+ default: return "Done.";
1098
+ }
1099
+ }
1100
+ });
1101
+ /**
1102
+ * Handle deployment resource actions.
1103
+ *
1104
+ * Not using the factory because the `get` action has special logic
1105
+ * (with id → output, without id → script).
1106
+ */
1107
+ async function handleDeployments(action, args, ctx) {
1108
+ if (!args.server_id) return errorResult("Missing required field: server_id");
1109
+ if (!args.site_id) return errorResult("Missing required field: site_id");
1110
+ if (!sanitizeId(args.server_id)) return errorResult(`Invalid server_id: "${args.server_id}". IDs must be alphanumeric.`);
1111
+ if (!sanitizeId(args.site_id)) return errorResult(`Invalid site_id: "${args.site_id}". IDs must be alphanumeric.`);
1112
+ if (args.id && !sanitizeId(args.id)) return errorResult(`Invalid id: "${args.id}". IDs must be alphanumeric.`);
1113
+ const opts = {
1114
+ server_id: args.server_id,
1115
+ site_id: args.site_id
1116
+ };
1117
+ switch (action) {
1118
+ case "list": {
1119
+ const result = await listDeployments(opts, ctx.executorContext);
1120
+ if (ctx.compact) return jsonResult(formatDeploymentList(result.data));
1121
+ return jsonResult(result.data);
1122
+ }
1123
+ case "deploy": {
1124
+ const deployResult = await deploySiteAndWait(opts, ctx.executorContext);
1125
+ return jsonResult(formatDeployAction(args.site_id, args.server_id, deployResult.data));
1126
+ }
1127
+ case "get":
1128
+ if (args.id) {
1129
+ const result = await getDeploymentOutput({
1130
+ ...opts,
1131
+ deployment_id: args.id
1132
+ }, ctx.executorContext);
1133
+ return jsonResult(formatDeploymentOutput(args.id, result.data));
1134
+ }
1135
+ return jsonResult(formatDeploymentScript((await getDeploymentScript(opts, ctx.executorContext)).data));
1136
+ case "update":
1137
+ if (!args.content) return errorResult("Missing required field: content");
1138
+ await updateDeploymentScript({
1139
+ ...opts,
1140
+ content: args.content
1141
+ }, ctx.executorContext);
1142
+ return jsonResult(formatDeploymentScriptUpdated(args.site_id, args.server_id));
1143
+ default: return errorResult(`Unknown action "${action}" for deployments. Valid actions: list, deploy, get, update.`);
1144
+ }
1145
+ }
1146
+ const handleEnv = createResourceHandler({
1147
+ resource: "env",
1148
+ actions: ["get", "update"],
1149
+ requiredFields: {
1150
+ get: ["server_id", "site_id"],
1151
+ update: [
1152
+ "server_id",
1153
+ "site_id",
1154
+ "content"
1155
+ ]
1156
+ },
1157
+ executors: {
1158
+ get: getEnv,
1159
+ update: updateEnv
1160
+ },
1161
+ mapOptions: (_action, args) => ({
1162
+ server_id: args.server_id,
1163
+ site_id: args.site_id,
1164
+ content: args.content
1165
+ }),
1166
+ formatResult: (action, data) => {
1167
+ switch (action) {
1168
+ case "get": return formatEnv(data);
1169
+ case "update": return "Environment variables updated.";
1170
+ default: return "Done.";
1171
+ }
1172
+ }
1173
+ });
1174
+ const handleFirewallRules = createResourceHandler({
1175
+ resource: "firewall-rules",
1176
+ actions: [
1177
+ "list",
1178
+ "get",
1179
+ "create",
1180
+ "delete"
1181
+ ],
1182
+ requiredFields: {
1183
+ list: ["server_id"],
1184
+ get: ["server_id", "id"],
1185
+ create: [
1186
+ "server_id",
1187
+ "name",
1188
+ "port"
1189
+ ],
1190
+ delete: ["server_id", "id"]
1191
+ },
1192
+ executors: {
1193
+ list: listFirewallRules,
1194
+ get: getFirewallRule,
1195
+ create: createFirewallRule,
1196
+ delete: deleteFirewallRule
1197
+ },
1198
+ hints: (data, id) => {
1199
+ const rule = data;
1200
+ return getFirewallRuleHints(String(rule.server_id), id);
1201
+ },
1202
+ formatResult: (action, data, args) => {
1203
+ switch (action) {
1204
+ case "list": return formatFirewallRuleList(data);
1205
+ case "get": return formatFirewallRule(data);
1206
+ case "create": return formatFirewallRule(data);
1207
+ case "delete": return `Firewall rule ${args.id} deleted.`;
1208
+ default: return "Done.";
1209
+ }
1210
+ }
1211
+ });
1212
+ var RESOURCE_HELP = {
1213
+ servers: {
1214
+ description: "Manage Laravel Forge servers — provisioned cloud instances (DigitalOcean, AWS, Hetzner, etc.)",
1215
+ scope: "global (no parent ID needed)",
1216
+ actions: {
1217
+ list: "List all servers in your Forge account",
1218
+ get: "Get a single server by ID with full details",
1219
+ create: "Provision a new server (requires provider, type, region, name)",
1220
+ delete: "Delete a server by ID (irreversible)",
1221
+ reboot: "Reboot a server by ID"
1222
+ },
1223
+ fields: {
1224
+ id: "Server ID",
1225
+ name: "Server name (hostname)",
1226
+ provider: "Cloud provider (hetzner, ocean2, aws, linode, vultr, custom)",
1227
+ region: "Provider region code",
1228
+ ip_address: "Public IP address",
1229
+ is_ready: "Whether the server has finished provisioning",
1230
+ php_version: "Default PHP version (php84, php83, etc.)"
1231
+ },
1232
+ examples: [
1233
+ {
1234
+ description: "List all servers",
1235
+ params: {
1236
+ resource: "servers",
1237
+ action: "list"
1238
+ }
1239
+ },
1240
+ {
1241
+ description: "Get server details",
1242
+ params: {
1243
+ resource: "servers",
1244
+ action: "get",
1245
+ id: "123"
1246
+ }
1247
+ },
1248
+ {
1249
+ description: "Reboot a server",
1250
+ params: {
1251
+ resource: "servers",
1252
+ action: "reboot",
1253
+ id: "123"
1254
+ }
1255
+ }
1256
+ ]
1257
+ },
1258
+ sites: {
1259
+ description: "Manage sites on a Forge server — web applications, PHP projects, static sites",
1260
+ scope: "server (requires server_id)",
1261
+ actions: {
1262
+ list: "List all sites on a server",
1263
+ get: "Get a single site by ID",
1264
+ create: "Create a new site (requires domain, project_type)",
1265
+ delete: "Delete a site by ID"
1266
+ },
1267
+ fields: {
1268
+ id: "Site ID",
1269
+ server_id: "Parent server ID",
1270
+ name: "Domain name (e.g. example.com)",
1271
+ project_type: "Project type (php, html, symfony, symfony_dev, symfony_four, laravel)",
1272
+ directory: "Web root directory (e.g. /public)",
1273
+ repository: "Git repository URL (if connected)",
1274
+ deployment_status: "Last deployment status (null, deploying, deployed, failed)"
1275
+ },
1276
+ examples: [
1277
+ {
1278
+ description: "List sites on a server",
1279
+ params: {
1280
+ resource: "sites",
1281
+ action: "list",
1282
+ server_id: "123"
1283
+ }
1284
+ },
1285
+ {
1286
+ description: "Get site details",
1287
+ params: {
1288
+ resource: "sites",
1289
+ action: "get",
1290
+ server_id: "123",
1291
+ id: "456"
1292
+ }
1293
+ },
1294
+ {
1295
+ description: "Create a Laravel site",
1296
+ params: {
1297
+ resource: "sites",
1298
+ action: "create",
1299
+ server_id: "123",
1300
+ domain: "app.example.com",
1301
+ project_type: "php",
1302
+ directory: "/public"
1303
+ }
1304
+ }
1305
+ ]
1306
+ },
1307
+ deployments: {
1308
+ description: "Manage site deployments — trigger, monitor, and configure deploy scripts",
1309
+ scope: "site (requires server_id + site_id)",
1310
+ actions: {
1311
+ list: "List recent deployments for a site",
1312
+ deploy: "Trigger a new deployment",
1313
+ get: "Get deployment output (requires deployment_id in 'id' field)",
1314
+ update: "Update the deployment script (provide 'content' field)"
1315
+ },
1316
+ fields: {
1317
+ id: "Deployment ID",
1318
+ server_id: "Parent server ID",
1319
+ site_id: "Parent site ID",
1320
+ status: "Deployment status (null, deploying, deployed, failed)",
1321
+ displayable_type: "Type of deployment trigger",
1322
+ ended_at: "Completion timestamp"
1323
+ },
1324
+ examples: [
1325
+ {
1326
+ description: "Deploy a site",
1327
+ params: {
1328
+ resource: "deployments",
1329
+ action: "deploy",
1330
+ server_id: "123",
1331
+ site_id: "456"
1332
+ }
1333
+ },
1334
+ {
1335
+ description: "List deployments",
1336
+ params: {
1337
+ resource: "deployments",
1338
+ action: "list",
1339
+ server_id: "123",
1340
+ site_id: "456"
1341
+ }
1342
+ },
1343
+ {
1344
+ description: "Get deployment output",
1345
+ params: {
1346
+ resource: "deployments",
1347
+ action: "get",
1348
+ server_id: "123",
1349
+ site_id: "456",
1350
+ id: "789"
1351
+ }
1352
+ },
1353
+ {
1354
+ description: "Update deploy script",
1355
+ params: {
1356
+ resource: "deployments",
1357
+ action: "update",
1358
+ server_id: "123",
1359
+ site_id: "456",
1360
+ content: "cd /home/forge/app.example.com\ngit pull\ncomposer install\nphp artisan migrate --force"
1361
+ }
1362
+ }
1363
+ ]
1364
+ },
1365
+ env: {
1366
+ description: "Manage environment variables (.env file) for a site",
1367
+ scope: "site (requires server_id + site_id)",
1368
+ actions: {
1369
+ get: "Get the current .env file content",
1370
+ update: "Replace the entire .env file (provide 'content' field)"
1371
+ },
1372
+ examples: [{
1373
+ description: "Get env variables",
1374
+ params: {
1375
+ resource: "env",
1376
+ action: "get",
1377
+ server_id: "123",
1378
+ site_id: "456"
1379
+ }
1380
+ }, {
1381
+ description: "Update env",
1382
+ params: {
1383
+ resource: "env",
1384
+ action: "update",
1385
+ server_id: "123",
1386
+ site_id: "456",
1387
+ content: "APP_ENV=production\nAPP_DEBUG=false\nDB_HOST=127.0.0.1"
1388
+ }
1389
+ }]
1390
+ },
1391
+ nginx: {
1392
+ description: "Manage Nginx configuration for a site",
1393
+ scope: "site (requires server_id + site_id)",
1394
+ actions: {
1395
+ get: "Get the current Nginx config",
1396
+ update: "Replace the Nginx config (provide 'content' field)"
1397
+ },
1398
+ examples: [{
1399
+ description: "Get nginx config",
1400
+ params: {
1401
+ resource: "nginx",
1402
+ action: "get",
1403
+ server_id: "123",
1404
+ site_id: "456"
1405
+ }
1406
+ }]
1407
+ },
1408
+ certificates: {
1409
+ description: "Manage SSL/TLS certificates for a site — Let's Encrypt, custom, or cloned",
1410
+ scope: "site (requires server_id + site_id)",
1411
+ actions: {
1412
+ list: "List certificates for a site",
1413
+ get: "Get certificate details",
1414
+ create: "Create/request a new certificate",
1415
+ delete: "Delete a certificate",
1416
+ activate: "Activate a certificate (make it the active cert for the site)"
1417
+ },
1418
+ examples: [
1419
+ {
1420
+ description: "List certificates",
1421
+ params: {
1422
+ resource: "certificates",
1423
+ action: "list",
1424
+ server_id: "123",
1425
+ site_id: "456"
1426
+ }
1427
+ },
1428
+ {
1429
+ description: "Create Let's Encrypt cert",
1430
+ params: {
1431
+ resource: "certificates",
1432
+ action: "create",
1433
+ server_id: "123",
1434
+ site_id: "456",
1435
+ domain: "example.com",
1436
+ type: "new"
1437
+ }
1438
+ },
1439
+ {
1440
+ description: "Activate a certificate",
1441
+ params: {
1442
+ resource: "certificates",
1443
+ action: "activate",
1444
+ server_id: "123",
1445
+ site_id: "456",
1446
+ id: "789"
1447
+ }
1448
+ }
1449
+ ]
1450
+ },
1451
+ databases: {
1452
+ description: "Manage MySQL/PostgreSQL databases on a server",
1453
+ scope: "server (requires server_id)",
1454
+ actions: {
1455
+ list: "List databases on a server",
1456
+ get: "Get database details",
1457
+ create: "Create a new database (optionally with user)",
1458
+ delete: "Delete a database"
1459
+ },
1460
+ fields: {
1461
+ name: "Database name",
1462
+ user: "(create only) Create a database user",
1463
+ password: "(create only) User password"
1464
+ },
1465
+ examples: [{
1466
+ description: "List databases",
1467
+ params: {
1468
+ resource: "databases",
1469
+ action: "list",
1470
+ server_id: "123"
1471
+ }
1472
+ }, {
1473
+ description: "Create database with user",
1474
+ params: {
1475
+ resource: "databases",
1476
+ action: "create",
1477
+ server_id: "123",
1478
+ name: "myapp",
1479
+ user: "admin",
1480
+ password: "secret123"
1481
+ }
1482
+ }]
1483
+ },
1484
+ "database-users": {
1485
+ description: "Manage database users on a server",
1486
+ scope: "server (requires server_id)",
1487
+ actions: {
1488
+ list: "List database users on a server",
1489
+ get: "Get database user details",
1490
+ create: "Create a new database user",
1491
+ delete: "Delete a database user"
1492
+ },
1493
+ fields: {
1494
+ name: "Database user name",
1495
+ password: "User password",
1496
+ databases: "(create only) Array of database IDs to grant access to"
1497
+ },
1498
+ examples: [{
1499
+ description: "List database users",
1500
+ params: {
1501
+ resource: "database-users",
1502
+ action: "list",
1503
+ server_id: "123"
1504
+ }
1505
+ }, {
1506
+ description: "Create a database user",
1507
+ params: {
1508
+ resource: "database-users",
1509
+ action: "create",
1510
+ server_id: "123",
1511
+ name: "forge",
1512
+ password: "secret123",
1513
+ databases: [1, 2]
1514
+ }
1515
+ }]
1516
+ },
1517
+ daemons: {
1518
+ description: "Manage background processes (daemons) — queue workers, websocket servers, etc.",
1519
+ scope: "server (requires server_id)",
1520
+ actions: {
1521
+ list: "List daemons on a server",
1522
+ get: "Get daemon details",
1523
+ create: "Create a new daemon",
1524
+ delete: "Delete a daemon",
1525
+ restart: "Restart a daemon"
1526
+ },
1527
+ fields: {
1528
+ command: "Shell command to run",
1529
+ user: "Execution user (default: forge)",
1530
+ directory: "Working directory",
1531
+ processes: "Number of processes (default: 1)"
1532
+ },
1533
+ examples: [
1534
+ {
1535
+ description: "List daemons",
1536
+ params: {
1537
+ resource: "daemons",
1538
+ action: "list",
1539
+ server_id: "123"
1540
+ }
1541
+ },
1542
+ {
1543
+ description: "Create queue worker",
1544
+ params: {
1545
+ resource: "daemons",
1546
+ action: "create",
1547
+ server_id: "123",
1548
+ command: "php artisan queue:work --tries=3",
1549
+ user: "forge"
1550
+ }
1551
+ },
1552
+ {
1553
+ description: "Restart a daemon",
1554
+ params: {
1555
+ resource: "daemons",
1556
+ action: "restart",
1557
+ server_id: "123",
1558
+ id: "456"
1559
+ }
1560
+ }
1561
+ ]
1562
+ },
1563
+ "firewall-rules": {
1564
+ description: "Manage UFW firewall rules on a server",
1565
+ scope: "server (requires server_id)",
1566
+ actions: {
1567
+ list: "List firewall rules",
1568
+ get: "Get rule details",
1569
+ create: "Create a new firewall rule",
1570
+ delete: "Delete a firewall rule"
1571
+ },
1572
+ fields: {
1573
+ name: "Rule name/description",
1574
+ port: "Port number or range (e.g. 80, 8000-9000)",
1575
+ type: "allow or deny (default: allow)",
1576
+ ip_address: "Restrict to specific IP (optional)"
1577
+ },
1578
+ examples: [{
1579
+ description: "List firewall rules",
1580
+ params: {
1581
+ resource: "firewall-rules",
1582
+ action: "list",
1583
+ server_id: "123"
1584
+ }
1585
+ }, {
1586
+ description: "Open port 3000",
1587
+ params: {
1588
+ resource: "firewall-rules",
1589
+ action: "create",
1590
+ server_id: "123",
1591
+ name: "Allow Node.js",
1592
+ port: 3e3
1593
+ }
1594
+ }]
1595
+ },
1596
+ "ssh-keys": {
1597
+ description: "Manage SSH keys on a server",
1598
+ scope: "server (requires server_id)",
1599
+ actions: {
1600
+ list: "List SSH keys",
1601
+ get: "Get key details",
1602
+ create: "Add an SSH key to the server",
1603
+ delete: "Remove an SSH key"
1604
+ },
1605
+ fields: {
1606
+ name: "Key label/description",
1607
+ key: "Public key content (ssh-rsa ...)",
1608
+ username: "User to add the key to (optional)"
1609
+ },
1610
+ examples: [{
1611
+ description: "List SSH keys",
1612
+ params: {
1613
+ resource: "ssh-keys",
1614
+ action: "list",
1615
+ server_id: "123"
1616
+ }
1617
+ }, {
1618
+ description: "Add SSH key",
1619
+ params: {
1620
+ resource: "ssh-keys",
1621
+ action: "create",
1622
+ server_id: "123",
1623
+ name: "Deploy Key",
1624
+ key: "ssh-rsa AAAA..."
1625
+ }
1626
+ }]
1627
+ },
1628
+ "security-rules": {
1629
+ description: "Manage HTTP Basic Auth security rules for a site",
1630
+ scope: "site (requires server_id + site_id)",
1631
+ actions: {
1632
+ list: "List security rules",
1633
+ get: "Get rule details",
1634
+ create: "Create a security rule with credentials",
1635
+ delete: "Delete a security rule"
1636
+ },
1637
+ examples: [{
1638
+ description: "List security rules",
1639
+ params: {
1640
+ resource: "security-rules",
1641
+ action: "list",
1642
+ server_id: "123",
1643
+ site_id: "456"
1644
+ }
1645
+ }, {
1646
+ description: "Create basic auth",
1647
+ params: {
1648
+ resource: "security-rules",
1649
+ action: "create",
1650
+ server_id: "123",
1651
+ site_id: "456",
1652
+ name: "Staging Auth",
1653
+ credentials: [{
1654
+ username: "admin",
1655
+ password: "secret"
1656
+ }]
1657
+ }
1658
+ }]
1659
+ },
1660
+ "redirect-rules": {
1661
+ description: "Manage URL redirect rules for a site",
1662
+ scope: "site (requires server_id + site_id)",
1663
+ actions: {
1664
+ list: "List redirect rules",
1665
+ get: "Get rule details",
1666
+ create: "Create a redirect rule",
1667
+ delete: "Delete a redirect rule"
1668
+ },
1669
+ fields: {
1670
+ from: "Source path (regex supported)",
1671
+ to: "Destination URL",
1672
+ type: "redirect (302, default) or permanent (301)"
1673
+ },
1674
+ examples: [{
1675
+ description: "List redirects",
1676
+ params: {
1677
+ resource: "redirect-rules",
1678
+ action: "list",
1679
+ server_id: "123",
1680
+ site_id: "456"
1681
+ }
1682
+ }, {
1683
+ description: "Create redirect",
1684
+ params: {
1685
+ resource: "redirect-rules",
1686
+ action: "create",
1687
+ server_id: "123",
1688
+ site_id: "456",
1689
+ from: "/old-page",
1690
+ to: "/new-page",
1691
+ type: "permanent"
1692
+ }
1693
+ }]
1694
+ },
1695
+ monitors: {
1696
+ description: "Manage server monitoring alerts — CPU, disk, memory thresholds",
1697
+ scope: "server (requires server_id)",
1698
+ actions: {
1699
+ list: "List monitors",
1700
+ get: "Get monitor details",
1701
+ create: "Create a monitor",
1702
+ delete: "Delete a monitor"
1703
+ },
1704
+ fields: {
1705
+ type: "Monitor type (disk, cpu, memory)",
1706
+ operator: "Comparison operator (gte, lte)",
1707
+ threshold: "Threshold value (e.g. 80 for 80%)",
1708
+ minutes: "Check interval in minutes"
1709
+ },
1710
+ examples: [{
1711
+ description: "List monitors",
1712
+ params: {
1713
+ resource: "monitors",
1714
+ action: "list",
1715
+ server_id: "123"
1716
+ }
1717
+ }, {
1718
+ description: "Alert on high disk usage",
1719
+ params: {
1720
+ resource: "monitors",
1721
+ action: "create",
1722
+ server_id: "123",
1723
+ type: "disk",
1724
+ operator: "gte",
1725
+ threshold: 80,
1726
+ minutes: 5
1727
+ }
1728
+ }]
1729
+ },
1730
+ "nginx-templates": {
1731
+ description: "Manage reusable Nginx configuration templates on a server",
1732
+ scope: "server (requires server_id)",
1733
+ actions: {
1734
+ list: "List nginx templates",
1735
+ get: "Get template with content",
1736
+ create: "Create a new template",
1737
+ update: "Update template name or content",
1738
+ delete: "Delete a template"
1739
+ },
1740
+ examples: [{
1741
+ description: "List templates",
1742
+ params: {
1743
+ resource: "nginx-templates",
1744
+ action: "list",
1745
+ server_id: "123"
1746
+ }
1747
+ }, {
1748
+ description: "Get template content",
1749
+ params: {
1750
+ resource: "nginx-templates",
1751
+ action: "get",
1752
+ server_id: "123",
1753
+ id: "456"
1754
+ }
1755
+ }]
1756
+ },
1757
+ backups: {
1758
+ description: "Manage backup configurations — automated database backups to S3, Spaces, or other providers",
1759
+ scope: "server (requires server_id)",
1760
+ actions: {
1761
+ list: "List backup configurations on a server",
1762
+ get: "Get backup config details (databases, schedule, retention)",
1763
+ create: "Create a new backup configuration",
1764
+ delete: "Delete a backup configuration"
1765
+ },
1766
+ fields: {
1767
+ provider: "Backup provider (s3, spaces, custom)",
1768
+ credentials: "Provider credentials (keys, bucket, region)",
1769
+ frequency: "Backup frequency (daily, weekly, custom)",
1770
+ databases: "Array of database IDs to back up",
1771
+ retention: "Number of backups to retain"
1772
+ },
1773
+ examples: [{
1774
+ description: "List backup configs",
1775
+ params: {
1776
+ resource: "backups",
1777
+ action: "list",
1778
+ server_id: "123"
1779
+ }
1780
+ }, {
1781
+ description: "Get backup config details",
1782
+ params: {
1783
+ resource: "backups",
1784
+ action: "get",
1785
+ server_id: "123",
1786
+ id: "456"
1787
+ }
1788
+ }]
1789
+ },
1790
+ commands: {
1791
+ description: "Execute and list site commands — run artisan commands or shell scripts on a site",
1792
+ scope: "site (requires server_id + site_id)",
1793
+ actions: {
1794
+ list: "List commands executed on a site",
1795
+ get: "Get command details and status",
1796
+ create: "Execute a new command on the site"
1797
+ },
1798
+ fields: { command: "Shell command to execute" },
1799
+ examples: [{
1800
+ description: "List commands",
1801
+ params: {
1802
+ resource: "commands",
1803
+ action: "list",
1804
+ server_id: "123",
1805
+ site_id: "456"
1806
+ }
1807
+ }, {
1808
+ description: "Run a command",
1809
+ params: {
1810
+ resource: "commands",
1811
+ action: "create",
1812
+ server_id: "123",
1813
+ site_id: "456",
1814
+ command: "php artisan migrate --force"
1815
+ }
1816
+ }]
1817
+ },
1818
+ "scheduled-jobs": {
1819
+ description: "Manage cron jobs (scheduled tasks) on a server",
1820
+ scope: "server (requires server_id)",
1821
+ actions: {
1822
+ list: "List scheduled jobs on a server",
1823
+ get: "Get job details (command, frequency, cron expression)",
1824
+ create: "Create a new scheduled job",
1825
+ delete: "Delete a scheduled job"
1826
+ },
1827
+ fields: {
1828
+ command: "Shell command to schedule",
1829
+ user: "Execution user (default: forge)",
1830
+ frequency: "Frequency preset (minutely, hourly, nightly, weekly, monthly, custom)",
1831
+ minute: "(custom frequency) Minute field",
1832
+ hour: "(custom frequency) Hour field",
1833
+ day: "(custom frequency) Day of month field",
1834
+ month: "(custom frequency) Month field",
1835
+ weekday: "(custom frequency) Day of week field"
1836
+ },
1837
+ examples: [{
1838
+ description: "List scheduled jobs",
1839
+ params: {
1840
+ resource: "scheduled-jobs",
1841
+ action: "list",
1842
+ server_id: "123"
1843
+ }
1844
+ }, {
1845
+ description: "Create minutely scheduler",
1846
+ params: {
1847
+ resource: "scheduled-jobs",
1848
+ action: "create",
1849
+ server_id: "123",
1850
+ command: "php /home/forge/app.com/artisan schedule:run",
1851
+ frequency: "minutely",
1852
+ user: "forge"
1853
+ }
1854
+ }]
1855
+ },
1856
+ user: {
1857
+ description: "Get the currently authenticated Forge user profile",
1858
+ scope: "global (no parent ID needed)",
1859
+ actions: { get: "Get the authenticated user's profile (name, email, connected services)" },
1860
+ examples: [{
1861
+ description: "Get user profile",
1862
+ params: {
1863
+ resource: "user",
1864
+ action: "get"
1865
+ }
1866
+ }]
1867
+ },
1868
+ recipes: {
1869
+ description: "Manage and run server recipes — reusable bash scripts executed on one or more servers",
1870
+ scope: "global (no parent ID needed)",
1871
+ actions: {
1872
+ list: "List all recipes",
1873
+ get: "Get recipe details and script content",
1874
+ create: "Create a new recipe",
1875
+ delete: "Delete a recipe",
1876
+ run: "Run a recipe on specified servers (provide 'servers' as array of server IDs)"
1877
+ },
1878
+ fields: {
1879
+ name: "Recipe name",
1880
+ script: "Bash script content",
1881
+ user: "Execution user (default: root)",
1882
+ servers: "(run only) Array of server IDs to run on"
1883
+ },
1884
+ examples: [
1885
+ {
1886
+ description: "List recipes",
1887
+ params: {
1888
+ resource: "recipes",
1889
+ action: "list"
1890
+ }
1891
+ },
1892
+ {
1893
+ description: "Create a recipe",
1894
+ params: {
1895
+ resource: "recipes",
1896
+ action: "create",
1897
+ name: "Clear caches",
1898
+ script: "php artisan cache:clear\nphp artisan view:clear"
1899
+ }
1900
+ },
1901
+ {
1902
+ description: "Run recipe on servers",
1903
+ params: {
1904
+ resource: "recipes",
1905
+ action: "run",
1906
+ id: "123",
1907
+ servers: [
1908
+ 1,
1909
+ 2,
1910
+ 3
1911
+ ]
1912
+ }
1913
+ }
1914
+ ]
1915
+ }
1916
+ };
1917
+ /**
1918
+ * Handle help action — returns documentation for a specific resource.
1919
+ */
1920
+ function handleHelp(resource) {
1921
+ const help = RESOURCE_HELP[resource];
1922
+ if (!help) return handleHelpOverview();
1923
+ return jsonResult({
1924
+ resource,
1925
+ ...help
1926
+ });
1927
+ }
1928
+ /**
1929
+ * Get help for all resources (overview).
1930
+ */
1931
+ function handleHelpOverview() {
1932
+ return jsonResult({
1933
+ message: "Use action=\"help\" with a specific resource for detailed documentation",
1934
+ resources: Object.entries(RESOURCE_HELP).map(([resource, help]) => ({
1935
+ resource,
1936
+ description: help.description,
1937
+ scope: help.scope,
1938
+ actions: Object.keys(help.actions)
1939
+ })),
1940
+ _tip: "Always call { action: 'help', resource: '<name>' } before your first interaction with any resource to learn required fields and examples."
1941
+ });
1942
+ }
1943
+ const handleMonitors = createResourceHandler({
1944
+ resource: "monitors",
1945
+ actions: [
1946
+ "list",
1947
+ "get",
1948
+ "create",
1949
+ "delete"
1950
+ ],
1951
+ requiredFields: {
1952
+ list: ["server_id"],
1953
+ get: ["server_id", "id"],
1954
+ create: [
1955
+ "server_id",
1956
+ "type",
1957
+ "operator",
1958
+ "threshold",
1959
+ "minutes"
1960
+ ],
1961
+ delete: ["server_id", "id"]
1962
+ },
1963
+ executors: {
1964
+ list: listMonitors,
1965
+ get: getMonitor,
1966
+ create: createMonitor,
1967
+ delete: deleteMonitor
1968
+ },
1969
+ formatResult: (action, data, args) => {
1970
+ switch (action) {
1971
+ case "list": return formatMonitorList(data);
1972
+ case "get": return formatMonitor(data);
1973
+ case "create": return formatMonitor(data);
1974
+ case "delete": return `Monitor ${args.id} deleted.`;
1975
+ default: return "Done.";
1976
+ }
1977
+ }
1978
+ });
1979
+ const handleNginxConfig = createResourceHandler({
1980
+ resource: "nginx",
1981
+ actions: ["get", "update"],
1982
+ requiredFields: {
1983
+ get: ["server_id", "site_id"],
1984
+ update: [
1985
+ "server_id",
1986
+ "site_id",
1987
+ "content"
1988
+ ]
1989
+ },
1990
+ executors: {
1991
+ get: getNginxConfig,
1992
+ update: updateNginxConfig
1993
+ },
1994
+ mapOptions: (_action, args) => ({
1995
+ server_id: args.server_id,
1996
+ site_id: args.site_id,
1997
+ content: args.content
1998
+ }),
1999
+ formatResult: (action, data) => {
2000
+ switch (action) {
2001
+ case "get": return formatNginxConfig(data);
2002
+ case "update": return "Nginx configuration updated.";
2003
+ default: return "Done.";
2004
+ }
2005
+ }
2006
+ });
2007
+ const handleNginxTemplates = createResourceHandler({
2008
+ resource: "nginx-templates",
2009
+ actions: [
2010
+ "list",
2011
+ "get",
2012
+ "create",
2013
+ "update",
2014
+ "delete"
2015
+ ],
2016
+ requiredFields: {
2017
+ list: ["server_id"],
2018
+ get: ["server_id", "id"],
2019
+ create: [
2020
+ "server_id",
2021
+ "name",
2022
+ "content"
2023
+ ],
2024
+ update: ["server_id", "id"],
2025
+ delete: ["server_id", "id"]
2026
+ },
2027
+ executors: {
2028
+ list: listNginxTemplates,
2029
+ get: getNginxTemplate,
2030
+ create: createNginxTemplate,
2031
+ update: updateNginxTemplate,
2032
+ delete: deleteNginxTemplate
2033
+ },
2034
+ hints: (data, id) => {
2035
+ const template = data;
2036
+ return getNginxTemplateHints(String(template.server_id), id);
2037
+ },
2038
+ formatResult: (action, data, args) => {
2039
+ switch (action) {
2040
+ case "list": return formatNginxTemplateList(data);
2041
+ case "get": return formatNginxTemplate(data);
2042
+ case "create": return formatNginxTemplate(data);
2043
+ case "update": return formatNginxTemplate(data);
2044
+ case "delete": return `Nginx template ${args.id} deleted.`;
2045
+ default: return "Done.";
2046
+ }
2047
+ }
2048
+ });
2049
+ const handleRecipes = createResourceHandler({
2050
+ resource: "recipes",
2051
+ actions: [
2052
+ "list",
2053
+ "get",
2054
+ "create",
2055
+ "delete",
2056
+ "run"
2057
+ ],
2058
+ requiredFields: {
2059
+ get: ["id"],
2060
+ create: ["name", "script"],
2061
+ delete: ["id"],
2062
+ run: ["id", "servers"]
2063
+ },
2064
+ executors: {
2065
+ list: listRecipes,
2066
+ get: getRecipe,
2067
+ create: createRecipe,
2068
+ delete: deleteRecipe,
2069
+ run: runRecipe
2070
+ },
2071
+ hints: (_data, id) => getRecipeHints(id),
2072
+ formatResult: (action, data, args) => {
2073
+ switch (action) {
2074
+ case "list": return formatRecipeList(data);
2075
+ case "get": return formatRecipe(data);
2076
+ case "create": return formatRecipe(data);
2077
+ case "delete": return `Recipe ${args.id} deleted.`;
2078
+ case "run": {
2079
+ const servers = args.servers;
2080
+ const count = Array.isArray(servers) ? servers.length : 1;
2081
+ return `Recipe ${args.id} run on ${count} server(s).`;
2082
+ }
2083
+ default: return "Done.";
2084
+ }
2085
+ }
2086
+ });
2087
+ const handleRedirectRules = createResourceHandler({
2088
+ resource: "redirect-rules",
2089
+ actions: [
2090
+ "list",
2091
+ "get",
2092
+ "create",
2093
+ "delete"
2094
+ ],
2095
+ requiredFields: {
2096
+ list: ["server_id", "site_id"],
2097
+ get: [
2098
+ "server_id",
2099
+ "site_id",
2100
+ "id"
2101
+ ],
2102
+ create: [
2103
+ "server_id",
2104
+ "site_id",
2105
+ "from",
2106
+ "to"
2107
+ ],
2108
+ delete: [
2109
+ "server_id",
2110
+ "site_id",
2111
+ "id"
2112
+ ]
2113
+ },
2114
+ executors: {
2115
+ list: listRedirectRules,
2116
+ get: getRedirectRule,
2117
+ create: createRedirectRule,
2118
+ delete: deleteRedirectRule
2119
+ },
2120
+ formatResult: (action, data, args) => {
2121
+ switch (action) {
2122
+ case "list": return formatRedirectRuleList(data);
2123
+ case "get": return formatRedirectRule(data);
2124
+ case "create": return formatRedirectRule(data);
2125
+ case "delete": return `Redirect rule ${args.id} deleted.`;
2126
+ default: return "Done.";
2127
+ }
2128
+ }
2129
+ });
2130
+ const handleScheduledJobs = createResourceHandler({
2131
+ resource: "scheduled-jobs",
2132
+ actions: [
2133
+ "list",
2134
+ "get",
2135
+ "create",
2136
+ "delete"
2137
+ ],
2138
+ requiredFields: {
2139
+ list: ["server_id"],
2140
+ get: ["server_id", "id"],
2141
+ create: ["server_id", "command"],
2142
+ delete: ["server_id", "id"]
2143
+ },
2144
+ executors: {
2145
+ list: listScheduledJobs,
2146
+ get: getScheduledJob,
2147
+ create: createScheduledJob,
2148
+ delete: deleteScheduledJob
2149
+ },
2150
+ formatResult: (action, data, args) => {
2151
+ switch (action) {
2152
+ case "list": return formatScheduledJobList(data);
2153
+ case "get": return formatScheduledJob(data);
2154
+ case "create": return formatScheduledJob(data);
2155
+ case "delete": return `Scheduled job ${args.id} deleted.`;
2156
+ default: return "Done.";
2157
+ }
2158
+ }
2159
+ });
2160
+ var RESOURCE_SCHEMAS = {
2161
+ servers: {
2162
+ actions: [
2163
+ "list",
2164
+ "get",
2165
+ "create",
2166
+ "delete",
2167
+ "reboot"
2168
+ ],
2169
+ scope: "global",
2170
+ required: {
2171
+ get: ["id"],
2172
+ create: [
2173
+ "provider",
2174
+ "type",
2175
+ "region",
2176
+ "name"
2177
+ ],
2178
+ delete: ["id"],
2179
+ reboot: ["id"]
2180
+ },
2181
+ create: {
2182
+ provider: {
2183
+ required: true,
2184
+ type: "string — hetzner, ocean2, aws, etc."
2185
+ },
2186
+ type: {
2187
+ required: true,
2188
+ type: "string — app, web, worker, etc."
2189
+ },
2190
+ region: {
2191
+ required: true,
2192
+ type: "string — provider-specific region code"
2193
+ },
2194
+ name: {
2195
+ required: true,
2196
+ type: "string"
2197
+ },
2198
+ credential_id: {
2199
+ required: false,
2200
+ type: "string — provider credential ID"
2201
+ },
2202
+ php_version: {
2203
+ required: false,
2204
+ type: "string — php84, php83, etc."
2205
+ },
2206
+ database: {
2207
+ required: false,
2208
+ type: "string — database name to create"
2209
+ }
2210
+ }
2211
+ },
2212
+ sites: {
2213
+ actions: [
2214
+ "list",
2215
+ "get",
2216
+ "create",
2217
+ "delete"
2218
+ ],
2219
+ scope: "server",
2220
+ required: {
2221
+ list: ["server_id"],
2222
+ get: ["server_id", "id"],
2223
+ create: [
2224
+ "server_id",
2225
+ "domain",
2226
+ "project_type"
2227
+ ],
2228
+ delete: ["server_id", "id"]
2229
+ },
2230
+ create: {
2231
+ domain: {
2232
+ required: true,
2233
+ type: "string — e.g. example.com"
2234
+ },
2235
+ project_type: {
2236
+ required: true,
2237
+ type: "string — php, html, symfony, etc."
2238
+ },
2239
+ directory: {
2240
+ required: false,
2241
+ type: "string — web root (/public)"
2242
+ }
2243
+ }
2244
+ },
2245
+ deployments: {
2246
+ actions: [
2247
+ "list",
2248
+ "deploy",
2249
+ "get",
2250
+ "update"
2251
+ ],
2252
+ scope: "site",
2253
+ required: {
2254
+ list: ["server_id", "site_id"],
2255
+ deploy: ["server_id", "site_id"],
2256
+ get: [
2257
+ "server_id",
2258
+ "site_id",
2259
+ "id"
2260
+ ],
2261
+ update: [
2262
+ "server_id",
2263
+ "site_id",
2264
+ "content"
2265
+ ]
2266
+ }
2267
+ },
2268
+ env: {
2269
+ actions: ["get", "update"],
2270
+ scope: "site",
2271
+ required: {
2272
+ get: ["server_id", "site_id"],
2273
+ update: [
2274
+ "server_id",
2275
+ "site_id",
2276
+ "content"
2277
+ ]
2278
+ }
2279
+ },
2280
+ nginx: {
2281
+ actions: ["get", "update"],
2282
+ scope: "site",
2283
+ required: {
2284
+ get: ["server_id", "site_id"],
2285
+ update: [
2286
+ "server_id",
2287
+ "site_id",
2288
+ "content"
2289
+ ]
2290
+ }
2291
+ },
2292
+ certificates: {
2293
+ actions: [
2294
+ "list",
2295
+ "get",
2296
+ "create",
2297
+ "delete",
2298
+ "activate"
2299
+ ],
2300
+ scope: "site",
2301
+ required: {
2302
+ list: ["server_id", "site_id"],
2303
+ get: [
2304
+ "server_id",
2305
+ "site_id",
2306
+ "id"
2307
+ ],
2308
+ create: [
2309
+ "server_id",
2310
+ "site_id",
2311
+ "domain"
2312
+ ],
2313
+ delete: [
2314
+ "server_id",
2315
+ "site_id",
2316
+ "id"
2317
+ ],
2318
+ activate: [
2319
+ "server_id",
2320
+ "site_id",
2321
+ "id"
2322
+ ]
2323
+ },
2324
+ create: {
2325
+ type: {
2326
+ required: false,
2327
+ type: "string — new, existing, clone (default: new)"
2328
+ },
2329
+ domain: {
2330
+ required: true,
2331
+ type: "string"
2332
+ }
2333
+ }
2334
+ },
2335
+ databases: {
2336
+ actions: [
2337
+ "list",
2338
+ "get",
2339
+ "create",
2340
+ "delete"
2341
+ ],
2342
+ scope: "server",
2343
+ required: {
2344
+ list: ["server_id"],
2345
+ get: ["server_id", "id"],
2346
+ create: ["server_id", "name"],
2347
+ delete: ["server_id", "id"]
2348
+ },
2349
+ create: {
2350
+ name: {
2351
+ required: true,
2352
+ type: "string — database name"
2353
+ },
2354
+ user: {
2355
+ required: false,
2356
+ type: "string — database user to create"
2357
+ },
2358
+ password: {
2359
+ required: false,
2360
+ type: "string — user password"
2361
+ }
2362
+ }
2363
+ },
2364
+ "database-users": {
2365
+ actions: [
2366
+ "list",
2367
+ "get",
2368
+ "create",
2369
+ "delete"
2370
+ ],
2371
+ scope: "server",
2372
+ required: {
2373
+ list: ["server_id"],
2374
+ get: ["server_id", "id"],
2375
+ create: [
2376
+ "server_id",
2377
+ "name",
2378
+ "password"
2379
+ ],
2380
+ delete: ["server_id", "id"]
2381
+ },
2382
+ create: {
2383
+ name: {
2384
+ required: true,
2385
+ type: "string — database user name"
2386
+ },
2387
+ password: {
2388
+ required: true,
2389
+ type: "string — user password"
2390
+ },
2391
+ databases: {
2392
+ required: false,
2393
+ type: "array — database IDs to grant access to"
2394
+ }
2395
+ }
2396
+ },
2397
+ daemons: {
2398
+ actions: [
2399
+ "list",
2400
+ "get",
2401
+ "create",
2402
+ "delete",
2403
+ "restart"
2404
+ ],
2405
+ scope: "server",
2406
+ required: {
2407
+ list: ["server_id"],
2408
+ get: ["server_id", "id"],
2409
+ create: ["server_id", "command"],
2410
+ delete: ["server_id", "id"],
2411
+ restart: ["server_id", "id"]
2412
+ },
2413
+ create: {
2414
+ command: {
2415
+ required: true,
2416
+ type: "string — e.g. php artisan queue:work"
2417
+ },
2418
+ user: {
2419
+ required: false,
2420
+ type: "string — default: forge"
2421
+ },
2422
+ directory: {
2423
+ required: false,
2424
+ type: "string — working directory"
2425
+ },
2426
+ processes: {
2427
+ required: false,
2428
+ type: "number — default: 1"
2429
+ }
2430
+ }
2431
+ },
2432
+ "firewall-rules": {
2433
+ actions: [
2434
+ "list",
2435
+ "get",
2436
+ "create",
2437
+ "delete"
2438
+ ],
2439
+ scope: "server",
2440
+ required: {
2441
+ list: ["server_id"],
2442
+ get: ["server_id", "id"],
2443
+ create: [
2444
+ "server_id",
2445
+ "name",
2446
+ "port"
2447
+ ],
2448
+ delete: ["server_id", "id"]
2449
+ },
2450
+ create: {
2451
+ name: {
2452
+ required: true,
2453
+ type: "string"
2454
+ },
2455
+ port: {
2456
+ required: true,
2457
+ type: "number|string — e.g. 80, 443, 8000-9000"
2458
+ },
2459
+ type: {
2460
+ required: false,
2461
+ type: "string — allow (default) or deny"
2462
+ },
2463
+ ip_address: {
2464
+ required: false,
2465
+ type: "string — restrict to IP"
2466
+ }
2467
+ }
2468
+ },
2469
+ "ssh-keys": {
2470
+ actions: [
2471
+ "list",
2472
+ "get",
2473
+ "create",
2474
+ "delete"
2475
+ ],
2476
+ scope: "server",
2477
+ required: {
2478
+ list: ["server_id"],
2479
+ get: ["server_id", "id"],
2480
+ create: [
2481
+ "server_id",
2482
+ "name",
2483
+ "key"
2484
+ ],
2485
+ delete: ["server_id", "id"]
2486
+ },
2487
+ create: {
2488
+ name: {
2489
+ required: true,
2490
+ type: "string — key label"
2491
+ },
2492
+ key: {
2493
+ required: true,
2494
+ type: "string — public key content"
2495
+ },
2496
+ username: {
2497
+ required: false,
2498
+ type: "string — user to add key to"
2499
+ }
2500
+ }
2501
+ },
2502
+ "security-rules": {
2503
+ actions: [
2504
+ "list",
2505
+ "get",
2506
+ "create",
2507
+ "delete"
2508
+ ],
2509
+ scope: "site",
2510
+ required: {
2511
+ list: ["server_id", "site_id"],
2512
+ get: [
2513
+ "server_id",
2514
+ "site_id",
2515
+ "id"
2516
+ ],
2517
+ create: [
2518
+ "server_id",
2519
+ "site_id",
2520
+ "name",
2521
+ "credentials"
2522
+ ],
2523
+ delete: [
2524
+ "server_id",
2525
+ "site_id",
2526
+ "id"
2527
+ ]
2528
+ },
2529
+ create: {
2530
+ name: {
2531
+ required: true,
2532
+ type: "string — rule name"
2533
+ },
2534
+ path: {
2535
+ required: false,
2536
+ type: "string — protected path (default: /)"
2537
+ },
2538
+ credentials: {
2539
+ required: true,
2540
+ type: "array — [{username, password}]"
2541
+ }
2542
+ }
2543
+ },
2544
+ "redirect-rules": {
2545
+ actions: [
2546
+ "list",
2547
+ "get",
2548
+ "create",
2549
+ "delete"
2550
+ ],
2551
+ scope: "site",
2552
+ required: {
2553
+ list: ["server_id", "site_id"],
2554
+ get: [
2555
+ "server_id",
2556
+ "site_id",
2557
+ "id"
2558
+ ],
2559
+ create: [
2560
+ "server_id",
2561
+ "site_id",
2562
+ "from",
2563
+ "to"
2564
+ ],
2565
+ delete: [
2566
+ "server_id",
2567
+ "site_id",
2568
+ "id"
2569
+ ]
2570
+ },
2571
+ create: {
2572
+ from: {
2573
+ required: true,
2574
+ type: "string — source path"
2575
+ },
2576
+ to: {
2577
+ required: true,
2578
+ type: "string — destination URL"
2579
+ },
2580
+ type: {
2581
+ required: false,
2582
+ type: "string — redirect (302) or permanent (301)"
2583
+ }
2584
+ }
2585
+ },
2586
+ monitors: {
2587
+ actions: [
2588
+ "list",
2589
+ "get",
2590
+ "create",
2591
+ "delete"
2592
+ ],
2593
+ scope: "server",
2594
+ required: {
2595
+ list: ["server_id"],
2596
+ get: ["server_id", "id"],
2597
+ create: [
2598
+ "server_id",
2599
+ "type",
2600
+ "operator",
2601
+ "threshold",
2602
+ "minutes"
2603
+ ],
2604
+ delete: ["server_id", "id"]
2605
+ },
2606
+ create: {
2607
+ type: {
2608
+ required: true,
2609
+ type: "string — disk, cpu, memory, etc."
2610
+ },
2611
+ operator: {
2612
+ required: true,
2613
+ type: "string — gte, lte"
2614
+ },
2615
+ threshold: {
2616
+ required: true,
2617
+ type: "number — e.g. 80"
2618
+ },
2619
+ minutes: {
2620
+ required: true,
2621
+ type: "number — check interval in minutes"
2622
+ }
2623
+ }
2624
+ },
2625
+ "nginx-templates": {
2626
+ actions: [
2627
+ "list",
2628
+ "get",
2629
+ "create",
2630
+ "update",
2631
+ "delete"
2632
+ ],
2633
+ scope: "server",
2634
+ required: {
2635
+ list: ["server_id"],
2636
+ get: ["server_id", "id"],
2637
+ create: [
2638
+ "server_id",
2639
+ "name",
2640
+ "content"
2641
+ ],
2642
+ update: ["server_id", "id"],
2643
+ delete: ["server_id", "id"]
2644
+ },
2645
+ create: {
2646
+ name: {
2647
+ required: true,
2648
+ type: "string — template name"
2649
+ },
2650
+ content: {
2651
+ required: true,
2652
+ type: "string — nginx configuration template"
2653
+ }
2654
+ }
2655
+ },
2656
+ backups: {
2657
+ actions: [
2658
+ "list",
2659
+ "get",
2660
+ "create",
2661
+ "delete"
2662
+ ],
2663
+ scope: "server",
2664
+ required: {
2665
+ list: ["server_id"],
2666
+ get: ["server_id", "id"],
2667
+ create: [
2668
+ "server_id",
2669
+ "provider",
2670
+ "credentials",
2671
+ "frequency",
2672
+ "databases"
2673
+ ],
2674
+ delete: ["server_id", "id"]
2675
+ },
2676
+ create: {
2677
+ provider: {
2678
+ required: true,
2679
+ type: "string — s3, spaces, custom"
2680
+ },
2681
+ credentials: {
2682
+ required: true,
2683
+ type: "object — provider credentials (key, secret, etc.)"
2684
+ },
2685
+ frequency: {
2686
+ required: true,
2687
+ type: "string — daily, weekly, custom"
2688
+ },
2689
+ databases: {
2690
+ required: true,
2691
+ type: "array — database IDs to back up"
2692
+ },
2693
+ directory: {
2694
+ required: false,
2695
+ type: "string — backup directory"
2696
+ },
2697
+ email: {
2698
+ required: false,
2699
+ type: "string — notification email"
2700
+ },
2701
+ retention: {
2702
+ required: false,
2703
+ type: "number — backups to retain (default: 7)"
2704
+ }
2705
+ }
2706
+ },
2707
+ commands: {
2708
+ actions: [
2709
+ "list",
2710
+ "get",
2711
+ "create"
2712
+ ],
2713
+ scope: "site",
2714
+ required: {
2715
+ list: ["server_id", "site_id"],
2716
+ get: [
2717
+ "server_id",
2718
+ "site_id",
2719
+ "id"
2720
+ ],
2721
+ create: [
2722
+ "server_id",
2723
+ "site_id",
2724
+ "command"
2725
+ ]
2726
+ },
2727
+ create: { command: {
2728
+ required: true,
2729
+ type: "string — shell command to execute"
2730
+ } }
2731
+ },
2732
+ "scheduled-jobs": {
2733
+ actions: [
2734
+ "list",
2735
+ "get",
2736
+ "create",
2737
+ "delete"
2738
+ ],
2739
+ scope: "server",
2740
+ required: {
2741
+ list: ["server_id"],
2742
+ get: ["server_id", "id"],
2743
+ create: ["server_id", "command"],
2744
+ delete: ["server_id", "id"]
2745
+ },
2746
+ create: {
2747
+ command: {
2748
+ required: true,
2749
+ type: "string — command to schedule"
2750
+ },
2751
+ user: {
2752
+ required: false,
2753
+ type: "string — execution user (default: forge)"
2754
+ },
2755
+ frequency: {
2756
+ required: false,
2757
+ type: "string — minutely, hourly, nightly, weekly, monthly, custom"
2758
+ },
2759
+ minute: {
2760
+ required: false,
2761
+ type: "string — cron minute field (custom frequency)"
2762
+ },
2763
+ hour: {
2764
+ required: false,
2765
+ type: "string — cron hour field"
2766
+ },
2767
+ day: {
2768
+ required: false,
2769
+ type: "string — cron day field"
2770
+ },
2771
+ month: {
2772
+ required: false,
2773
+ type: "string — cron month field"
2774
+ },
2775
+ weekday: {
2776
+ required: false,
2777
+ type: "string — cron weekday field"
2778
+ }
2779
+ }
2780
+ },
2781
+ user: {
2782
+ actions: ["get"],
2783
+ scope: "global",
2784
+ required: {}
2785
+ },
2786
+ recipes: {
2787
+ actions: [
2788
+ "list",
2789
+ "get",
2790
+ "create",
2791
+ "delete",
2792
+ "run"
2793
+ ],
2794
+ scope: "global",
2795
+ required: {
2796
+ get: ["id"],
2797
+ create: ["name", "script"],
2798
+ delete: ["id"],
2799
+ run: ["id", "servers"]
2800
+ },
2801
+ create: {
2802
+ name: {
2803
+ required: true,
2804
+ type: "string — recipe name"
2805
+ },
2806
+ script: {
2807
+ required: true,
2808
+ type: "string — bash script content"
2809
+ },
2810
+ user: {
2811
+ required: false,
2812
+ type: "string — execution user (default: root)"
2813
+ }
2814
+ }
2815
+ }
2816
+ };
2817
+ /**
2818
+ * Handle schema action — returns compact spec for a specific resource.
2819
+ */
2820
+ function handleSchema(resource) {
2821
+ const schema = RESOURCE_SCHEMAS[resource];
2822
+ if (!schema) return jsonResult({
2823
+ error: `Unknown resource: ${resource}`,
2824
+ available_resources: Object.keys(RESOURCE_SCHEMAS)
2825
+ });
2826
+ return jsonResult({
2827
+ resource,
2828
+ ...schema
2829
+ });
2830
+ }
2831
+ /**
2832
+ * Get schema overview for all resources.
2833
+ */
2834
+ function handleSchemaOverview() {
2835
+ return jsonResult({
2836
+ _tip: "Use action=\"schema\" with a specific resource for full required/create spec",
2837
+ resources: Object.entries(RESOURCE_SCHEMAS).map(([resource, schema]) => ({
2838
+ resource,
2839
+ actions: schema.actions,
2840
+ scope: schema.scope
2841
+ }))
2842
+ });
2843
+ }
2844
+ const handleSecurityRules = createResourceHandler({
2845
+ resource: "security-rules",
2846
+ actions: [
2847
+ "list",
2848
+ "get",
2849
+ "create",
2850
+ "delete"
2851
+ ],
2852
+ requiredFields: {
2853
+ list: ["server_id", "site_id"],
2854
+ get: [
2855
+ "server_id",
2856
+ "site_id",
2857
+ "id"
2858
+ ],
2859
+ create: [
2860
+ "server_id",
2861
+ "site_id",
2862
+ "name",
2863
+ "credentials"
2864
+ ],
2865
+ delete: [
2866
+ "server_id",
2867
+ "site_id",
2868
+ "id"
2869
+ ]
2870
+ },
2871
+ executors: {
2872
+ list: listSecurityRules,
2873
+ get: getSecurityRule,
2874
+ create: createSecurityRule,
2875
+ delete: deleteSecurityRule
2876
+ },
2877
+ formatResult: (action, data, args) => {
2878
+ switch (action) {
2879
+ case "list": return formatSecurityRuleList(data);
2880
+ case "get": return formatSecurityRule(data);
2881
+ case "create": return formatSecurityRule(data);
2882
+ case "delete": return `Security rule ${args.id} deleted.`;
2883
+ default: return "Done.";
2884
+ }
2885
+ }
2886
+ });
2887
+ const handleServers = createResourceHandler({
2888
+ resource: "servers",
2889
+ actions: [
2890
+ "list",
2891
+ "get",
2892
+ "create",
2893
+ "delete",
2894
+ "reboot"
2895
+ ],
2896
+ requiredFields: {
2897
+ get: ["id"],
2898
+ create: [
2899
+ "provider",
2900
+ "name",
2901
+ "type",
2902
+ "region"
2903
+ ],
2904
+ delete: ["id"],
2905
+ reboot: ["id"]
2906
+ },
2907
+ executors: {
2908
+ list: listServers,
2909
+ get: getServer,
2910
+ create: createServer,
2911
+ delete: deleteServer,
2912
+ reboot: rebootServer
2913
+ },
2914
+ hints: (_data, id) => getServerHints(id),
2915
+ mapOptions: (action, args) => {
2916
+ switch (action) {
2917
+ case "get":
2918
+ case "delete":
2919
+ case "reboot": return { server_id: args.id };
2920
+ case "create": return {
2921
+ provider: args.provider,
2922
+ credential_id: Number(args.credential_id) || 0,
2923
+ name: args.name,
2924
+ type: args.type ?? "app",
2925
+ size: args.size ?? "",
2926
+ region: args.region
2927
+ };
2928
+ default: return {};
2929
+ }
2930
+ },
2931
+ formatResult: (action, data, args) => {
2932
+ switch (action) {
2933
+ case "list": return formatServerList(data);
2934
+ case "get": return formatServer(data);
2935
+ case "create": return formatServer(data);
2936
+ case "delete": return `Server ${args.id} deleted.`;
2937
+ case "reboot": return `Server ${args.id} reboot initiated.`;
2938
+ default: return "Done.";
2939
+ }
2940
+ }
2941
+ });
2942
+ const handleSites = createResourceHandler({
2943
+ resource: "sites",
2944
+ actions: [
2945
+ "list",
2946
+ "get",
2947
+ "create",
2948
+ "delete"
2949
+ ],
2950
+ requiredFields: {
2951
+ list: ["server_id"],
2952
+ get: ["server_id", "id"],
2953
+ create: ["server_id", "domain"],
2954
+ delete: ["server_id", "id"]
2955
+ },
2956
+ executors: {
2957
+ list: listSites,
2958
+ get: getSite,
2959
+ create: createSite,
2960
+ delete: deleteSite
2961
+ },
2962
+ hints: (data, id) => {
2963
+ const site = data;
2964
+ return getSiteHints(String(site.server_id), id);
2965
+ },
2966
+ mapOptions: (action, args) => {
2967
+ switch (action) {
2968
+ case "list": return { server_id: args.server_id };
2969
+ case "get": return {
2970
+ server_id: args.server_id,
2971
+ site_id: args.id
2972
+ };
2973
+ case "create": return {
2974
+ server_id: args.server_id,
2975
+ domain: args.domain,
2976
+ project_type: args.project_type ?? "php",
2977
+ directory: args.directory
2978
+ };
2979
+ case "delete": return {
2980
+ server_id: args.server_id,
2981
+ site_id: args.id
2982
+ };
2983
+ default: return {};
2984
+ }
2985
+ },
2986
+ formatResult: (action, data, args) => {
2987
+ switch (action) {
2988
+ case "list": return formatSiteList(data, args.server_id);
2989
+ case "get": return formatSite(data);
2990
+ case "create": return formatSite(data);
2991
+ case "delete": return `Site ${args.id} deleted from server ${args.server_id}.`;
2992
+ default: return "Done.";
2993
+ }
2994
+ }
2995
+ });
2996
+ const handleSshKeys = createResourceHandler({
2997
+ resource: "ssh-keys",
2998
+ actions: [
2999
+ "list",
3000
+ "get",
3001
+ "create",
3002
+ "delete"
3003
+ ],
3004
+ requiredFields: {
3005
+ list: ["server_id"],
3006
+ get: ["server_id", "id"],
3007
+ create: [
3008
+ "server_id",
3009
+ "name",
3010
+ "key"
3011
+ ],
3012
+ delete: ["server_id", "id"]
3013
+ },
3014
+ executors: {
3015
+ list: listSshKeys,
3016
+ get: getSshKey,
3017
+ create: createSshKey,
3018
+ delete: deleteSshKey
3019
+ },
3020
+ hints: (data, id) => {
3021
+ const key = data;
3022
+ return getSshKeyHints(String(key.server_id), id);
3023
+ },
3024
+ formatResult: (action, data, args) => {
3025
+ switch (action) {
3026
+ case "list": return formatSshKeyList(data);
3027
+ case "get": return formatSshKey(data);
3028
+ case "create": return formatSshKey(data);
3029
+ case "delete": return `SSH key ${args.id} deleted.`;
3030
+ default: return "Done.";
3031
+ }
3032
+ }
3033
+ });
3034
+ const handleUser = createResourceHandler({
3035
+ resource: "user",
3036
+ actions: ["get"],
3037
+ executors: { get: getUser },
3038
+ formatResult: (_action, data) => {
3039
+ return formatUser(data);
3040
+ }
3041
+ });
3042
+ /**
3043
+ * Read-only actions — safe operations that don't modify server state.
3044
+ */
3045
+ const READ_ACTIONS = [
3046
+ "list",
3047
+ "get",
3048
+ "help",
3049
+ "schema"
3050
+ ];
3051
+ /**
3052
+ * Write actions — operations that modify server state.
3053
+ * These are separated into the `forge_write` tool for safety.
3054
+ */
3055
+ const WRITE_ACTIONS = [
3056
+ "create",
3057
+ "update",
3058
+ "delete",
3059
+ "deploy",
3060
+ "reboot",
3061
+ "restart",
3062
+ "activate",
3063
+ "run"
3064
+ ];
3065
+ /**
3066
+ * Check if an action is a write action.
3067
+ */
3068
+ function isWriteAction(action) {
3069
+ return WRITE_ACTIONS.includes(action);
3070
+ }
3071
+ /**
3072
+ * Check if an action is a read action.
3073
+ */
3074
+ function isReadAction(action) {
3075
+ return READ_ACTIONS.includes(action);
3076
+ }
3077
+ /**
3078
+ * Output schema shared by all forge tools.
3079
+ *
3080
+ * All tools return a consistent envelope:
3081
+ * - success: true/false
3082
+ * - result: the resource data (shape varies by resource and action)
3083
+ * - error: error message (only when success is false)
3084
+ *
3085
+ * The `result` field contains resource-specific data:
3086
+ * - list actions → array of resource objects
3087
+ * - get actions → single resource object (or string for env/nginx/scripts)
3088
+ * - help/schema → documentation text or schema object
3089
+ * - write actions → confirmation message or updated resource
3090
+ */
3091
+ var OUTPUT_SCHEMA = {
3092
+ type: "object",
3093
+ properties: {
3094
+ success: {
3095
+ type: "boolean",
3096
+ description: "Whether the operation succeeded"
3097
+ },
3098
+ result: { description: "Operation result — shape varies by resource and action (array for list, object for get, string for text content)" },
3099
+ error: {
3100
+ type: "string",
3101
+ description: "Error message (only present on failure)"
3102
+ }
3103
+ },
3104
+ required: ["success"]
3105
+ };
3106
+ /**
3107
+ * Shared input schema properties used by both forge and forge_write tools.
3108
+ */
3109
+ var SHARED_INPUT_PROPERTIES = {
3110
+ resource: {
3111
+ type: "string",
3112
+ enum: [...RESOURCES],
3113
+ description: "Forge resource to operate on"
3114
+ },
3115
+ id: {
3116
+ type: "string",
3117
+ description: "Resource ID (for get, delete, update actions)"
3118
+ },
3119
+ server_id: {
3120
+ type: "string",
3121
+ description: "Server ID (required for most resources)"
3122
+ },
3123
+ site_id: {
3124
+ type: "string",
3125
+ description: "Site ID (required for site-level resources: deployments, env, certificates, etc.)"
3126
+ },
3127
+ compact: {
3128
+ type: "boolean",
3129
+ description: "Compact output (default: true for list, false for get)"
3130
+ }
3131
+ };
3132
+ /**
3133
+ * Core tools available in both stdio and HTTP transports.
3134
+ *
3135
+ * Split into two tools for safety:
3136
+ * - `forge` — read-only operations (auto-approvable by MCP clients)
3137
+ * - `forge_write` — write operations (always requires confirmation)
3138
+ */
3139
+ const TOOLS = [{
3140
+ name: "forge",
3141
+ title: "Laravel Forge",
3142
+ description: [
3143
+ "Laravel Forge API — read operations.",
3144
+ `Resources: ${RESOURCES.join(", ")}.`,
3145
+ `Actions: ${[...READ_ACTIONS].join(", ")}.`,
3146
+ "Discovery: action=help with any resource for filters and examples.",
3147
+ "Server operations require id. Site operations require server_id.",
3148
+ "Deployment operations require server_id and site_id."
3149
+ ].join("\n"),
3150
+ annotations: {
3151
+ title: "Laravel Forge",
3152
+ readOnlyHint: true,
3153
+ destructiveHint: false,
3154
+ idempotentHint: true,
3155
+ openWorldHint: true
3156
+ },
3157
+ inputSchema: {
3158
+ type: "object",
3159
+ properties: {
3160
+ ...SHARED_INPUT_PROPERTIES,
3161
+ action: {
3162
+ type: "string",
3163
+ enum: [...READ_ACTIONS],
3164
+ description: "Read action to perform. Use \"help\" for resource documentation."
3165
+ }
3166
+ },
3167
+ required: ["resource", "action"]
3168
+ },
3169
+ outputSchema: OUTPUT_SCHEMA
3170
+ }, {
3171
+ name: "forge_write",
3172
+ title: "Laravel Forge (Write)",
3173
+ description: [
3174
+ "Laravel Forge API — write operations (create, update, delete, deploy, reboot, etc.).",
3175
+ `Resources: ${RESOURCES.join(", ")}.`,
3176
+ `Actions: ${[...WRITE_ACTIONS].join(", ")}.`,
3177
+ "Server operations require id. Site operations require server_id.",
3178
+ "Deployment operations require server_id and site_id.",
3179
+ "Use forge tool with action=help for resource documentation."
3180
+ ].join("\n"),
3181
+ annotations: {
3182
+ title: "Laravel Forge (Write)",
3183
+ readOnlyHint: false,
3184
+ destructiveHint: true,
3185
+ idempotentHint: false,
3186
+ openWorldHint: true
3187
+ },
3188
+ inputSchema: {
3189
+ type: "object",
3190
+ properties: {
3191
+ ...SHARED_INPUT_PROPERTIES,
3192
+ action: {
3193
+ type: "string",
3194
+ enum: [...WRITE_ACTIONS],
3195
+ description: "Write action to perform"
3196
+ },
3197
+ name: {
3198
+ type: "string",
3199
+ description: "Resource name (servers, databases, daemons, etc.)"
3200
+ },
3201
+ provider: {
3202
+ type: "string",
3203
+ description: "Server provider (e.g. ocean2, linode, aws)"
3204
+ },
3205
+ region: {
3206
+ type: "string",
3207
+ description: "Server region (e.g. nyc3, us-east-1)"
3208
+ },
3209
+ size: {
3210
+ type: "string",
3211
+ description: "Server size (e.g. s-1vcpu-1gb)"
3212
+ },
3213
+ credential_id: {
3214
+ type: "string",
3215
+ description: "Provider credential ID for server creation"
3216
+ },
3217
+ type: {
3218
+ type: "string",
3219
+ description: "Resource type (e.g. app, web, worker for servers; mysql, postgres for databases; disk_usage, used_memory for monitors)"
3220
+ },
3221
+ domain: {
3222
+ type: "string",
3223
+ description: "Site domain name (e.g. example.com)"
3224
+ },
3225
+ project_type: {
3226
+ type: "string",
3227
+ description: "Site project type (e.g. php, html, symfony, laravel)"
3228
+ },
3229
+ directory: {
3230
+ type: "string",
3231
+ description: "Web directory relative to site root (e.g. /public)"
3232
+ },
3233
+ content: {
3234
+ type: "string",
3235
+ description: "Content body for env variables, nginx config, or deployment script updates"
3236
+ },
3237
+ command: {
3238
+ type: "string",
3239
+ description: "Shell command to execute (daemons: background process command; recipes: bash script inline; commands: site command)"
3240
+ },
3241
+ user: {
3242
+ type: "string",
3243
+ description: "Unix user to run as (daemons, scheduled jobs; e.g. forge, root)"
3244
+ },
3245
+ port: {
3246
+ type: ["string", "number"],
3247
+ description: "Port number or range (firewall rules, e.g. 80 or 8000-9000)"
3248
+ },
3249
+ ip_address: {
3250
+ type: "string",
3251
+ description: "IP address to allow/block (firewall rules, e.g. 192.168.1.1)"
3252
+ },
3253
+ key: {
3254
+ type: "string",
3255
+ description: "Public SSH key content (ssh-rsa ... or ssh-ed25519 ...)"
3256
+ },
3257
+ from: {
3258
+ type: "string",
3259
+ description: "Source path for redirect rules (e.g. /old-page)"
3260
+ },
3261
+ to: {
3262
+ type: "string",
3263
+ description: "Destination URL for redirect rules (e.g. /new-page)"
3264
+ },
3265
+ credentials: {
3266
+ type: "array",
3267
+ description: "HTTP basic auth credentials for security rules [{username, password}]",
3268
+ items: { type: "object" }
3269
+ },
3270
+ operator: {
3271
+ type: "string",
3272
+ description: "Comparison operator for monitors (e.g. gte, lte)"
3273
+ },
3274
+ threshold: {
3275
+ type: "number",
3276
+ description: "Threshold value that triggers the monitor alert"
3277
+ },
3278
+ minutes: {
3279
+ type: "number",
3280
+ description: "Check interval in minutes for monitors (e.g. 5)"
3281
+ },
3282
+ frequency: {
3283
+ type: "string",
3284
+ description: "Cron frequency for scheduled jobs (e.g. minutely, hourly, nightly, custom)"
3285
+ },
3286
+ script: {
3287
+ type: "string",
3288
+ description: "Bash script content for recipes"
3289
+ },
3290
+ servers: {
3291
+ type: "array",
3292
+ description: "Server IDs to run a recipe on (e.g. [123, 456])",
3293
+ items: { type: "number" }
3294
+ }
3295
+ },
3296
+ required: ["resource", "action"]
3297
+ },
3298
+ outputSchema: OUTPUT_SCHEMA
3299
+ }];
3300
+ /**
3301
+ * Get the list of core tools, optionally filtered.
3302
+ *
3303
+ * In read-only mode, forge_write is excluded entirely — it won't appear
3304
+ * in the tool listing and cannot be called.
3305
+ */
3306
+ function getTools(options) {
3307
+ if (options?.readOnly) return TOOLS.filter((t) => t.name !== "forge_write");
3308
+ return [...TOOLS];
3309
+ }
3310
+ /**
3311
+ * Additional tools only available in stdio mode.
3312
+ */
3313
+ const STDIO_ONLY_TOOLS = [{
3314
+ name: "forge_configure",
3315
+ title: "Configure Forge",
3316
+ description: "Configure Laravel Forge API token. The token is stored locally in the XDG config directory.",
3317
+ annotations: {
3318
+ title: "Configure Forge",
3319
+ readOnlyHint: false,
3320
+ destructiveHint: false,
3321
+ idempotentHint: true,
3322
+ openWorldHint: false
3323
+ },
3324
+ inputSchema: {
3325
+ type: "object",
3326
+ properties: { apiToken: {
3327
+ type: "string",
3328
+ description: "Your Laravel Forge API token"
3329
+ } },
3330
+ required: ["apiToken"]
3331
+ },
3332
+ outputSchema: {
3333
+ type: "object",
3334
+ properties: {
3335
+ success: { type: "boolean" },
3336
+ message: {
3337
+ type: "string",
3338
+ description: "Confirmation message"
3339
+ },
3340
+ apiToken: {
3341
+ type: "string",
3342
+ description: "Masked API token (last 4 chars)"
3343
+ }
3344
+ },
3345
+ required: ["success"]
3346
+ }
3347
+ }, {
3348
+ name: "forge_get_config",
3349
+ title: "Get Forge Config",
3350
+ description: "Get current Forge configuration (shows masked token and config status).",
3351
+ annotations: {
3352
+ title: "Get Forge Config",
3353
+ readOnlyHint: true,
3354
+ destructiveHint: false,
3355
+ idempotentHint: true,
3356
+ openWorldHint: false
3357
+ },
3358
+ inputSchema: {
3359
+ type: "object",
3360
+ properties: {}
3361
+ },
3362
+ outputSchema: {
3363
+ type: "object",
3364
+ properties: {
3365
+ apiToken: {
3366
+ type: "string",
3367
+ description: "Masked API token or 'not configured'"
3368
+ },
3369
+ configured: {
3370
+ type: "boolean",
3371
+ description: "Whether a token is configured"
3372
+ }
3373
+ },
3374
+ required: ["configured"]
3375
+ }
3376
+ }];
3377
+ /** Valid resources derived from core constants */
3378
+ var VALID_RESOURCES = [...RESOURCES];
3379
+ var _auditLogger = null;
3380
+ function getAuditLogger() {
3381
+ if (!_auditLogger) _auditLogger = createAuditLogger("mcp");
3382
+ return _auditLogger;
3383
+ }
3384
+ /**
3385
+ * Route to the appropriate resource handler.
3386
+ */
3387
+ function routeToHandler(resource, action, args, ctx) {
3388
+ switch (resource) {
3389
+ case "servers": return handleServers(action, args, ctx);
3390
+ case "sites": return handleSites(action, args, ctx);
3391
+ case "deployments": return handleDeployments(action, args, ctx);
3392
+ case "env": return handleEnv(action, args, ctx);
3393
+ case "nginx": return handleNginxConfig(action, args, ctx);
3394
+ case "certificates": return handleCertificates(action, args, ctx);
3395
+ case "databases": return handleDatabases(action, args, ctx);
3396
+ case "database-users": return handleDatabaseUsers(action, args, ctx);
3397
+ case "daemons": return handleDaemons(action, args, ctx);
3398
+ case "firewall-rules": return handleFirewallRules(action, args, ctx);
3399
+ case "ssh-keys": return handleSshKeys(action, args, ctx);
3400
+ case "security-rules": return handleSecurityRules(action, args, ctx);
3401
+ case "redirect-rules": return handleRedirectRules(action, args, ctx);
3402
+ case "monitors": return handleMonitors(action, args, ctx);
3403
+ case "nginx-templates": return handleNginxTemplates(action, args, ctx);
3404
+ case "recipes": return handleRecipes(action, args, ctx);
3405
+ case "backups": return handleBackups(action, args, ctx);
3406
+ case "commands": return handleCommands(action, args, ctx);
3407
+ case "scheduled-jobs": return handleScheduledJobs(action, args, ctx);
3408
+ case "user": return handleUser(action, args, ctx);
3409
+ default: return Promise.resolve(errorResult(`Unknown resource "${resource}". Valid resources: ${VALID_RESOURCES.join(", ")}. Use action="help" for documentation.`));
3410
+ }
3411
+ }
3412
+ /**
3413
+ * Execute a tool call with provided credentials.
3414
+ *
3415
+ * This is the main entry point shared between stdio and HTTP transports.
3416
+ * Validates that the action matches the tool (read vs write).
3417
+ */
3418
+ async function executeToolWithCredentials(name, args, credentials) {
3419
+ const { resource, action, compact, ...rest } = args;
3420
+ if (!resource || !action) return errorResult("Missing required fields: \"resource\" and \"action\".");
3421
+ if (name === "forge" && isWriteAction(action)) return errorResult(`Action "${action}" is a write operation. Use the "forge_write" tool instead.`);
3422
+ if (name === "forge_write" && isReadAction(action)) return errorResult(`Action "${action}" is a read operation. Use the "forge" tool instead.`);
3423
+ if (action === "help")
3424
+ /* v8 ignore start */
3425
+ return resource ? handleHelp(resource) : handleHelpOverview();
3426
+ if (action === "schema")
3427
+ /* v8 ignore start */
3428
+ return resource ? handleSchema(resource) : handleSchemaOverview();
3429
+ if (!VALID_RESOURCES.includes(resource)) return errorResult(`Unknown resource "${resource}". Valid resources: ${VALID_RESOURCES.join(", ")}. Use action="help" for documentation.`);
3430
+ const handlerContext = {
3431
+ executorContext: { client: new HttpClient({ token: credentials.apiToken }) },
3432
+ compact: compact ?? action !== "get",
3433
+ includeHints: action === "get"
3434
+ };
3435
+ try {
3436
+ const result = await routeToHandler(resource, action, {
3437
+ resource,
3438
+ action,
3439
+ ...rest
3440
+ }, handlerContext);
3441
+ if (name === "forge_write") getAuditLogger().log({
3442
+ source: "mcp",
3443
+ resource,
3444
+ action,
3445
+ args: rest,
3446
+ status: result.isError ? "error" : "success"
3447
+ });
3448
+ return result;
3449
+ } catch (error) {
3450
+ let errorMessage;
3451
+ if (isUserInputError(error)) errorMessage = error.toFormattedMessage();
3452
+ else if (isForgeApiError(error)) errorMessage = `Forge API error (${error.status}): ${error.message}`;
3453
+ else
3454
+ /* v8 ignore start */
3455
+ errorMessage = error instanceof Error ? error.message : String(error);
3456
+ if (name === "forge_write") getAuditLogger().log({
3457
+ source: "mcp",
3458
+ resource,
3459
+ action,
3460
+ args: rest,
3461
+ status: "error",
3462
+ error: errorMessage
3463
+ });
3464
+ return errorResult(errorMessage);
3465
+ }
3466
+ }
3467
+ const VERSION = "0.2.0";
3468
+ export { INSTRUCTIONS as a, getTools as i, executeToolWithCredentials as n, STDIO_ONLY_TOOLS as r, VERSION as t };
3469
+
3470
+ //# sourceMappingURL=version-DaD5zvGh.js.map