@studiometa/forge-mcp 0.1.0 → 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.
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { HttpClient, isForgeApiError } from "@studiometa/forge-api";
5
- import { ACTIONS, RESOURCES, activateCertificate, 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, deploySite, 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";
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
6
  /**
7
7
  * MCP Server Instructions
8
8
  *
@@ -114,10 +114,18 @@ function formatDeploymentList(deployments) {
114
114
  return `${deployments.length} deployment(s):\n${lines.join("\n")}`;
115
115
  }
116
116
  /**
117
- * Format a deployment action result (deploy/update-script).
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.
118
122
  */
119
- function formatDeployAction(siteId, serverId) {
120
- return `Deployment triggered for site ${siteId} on server ${serverId}.`;
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");
121
129
  }
122
130
  /**
123
131
  * Format deployment script content.
@@ -384,14 +392,23 @@ function isUserInputError(error) {
384
392
  return error instanceof UserInputError;
385
393
  }
386
394
  /**
387
- * Create a successful JSON result.
388
- * Accepts a string or an object (which will be JSON-serialized).
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.
389
400
  */
390
401
  function jsonResult(data) {
391
- return { content: [{
392
- type: "text",
393
- text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
394
- }] };
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
+ };
395
412
  }
396
413
  /**
397
414
  * Validate an ID-like value (must be alphanumeric/dashes only).
@@ -403,7 +420,7 @@ function sanitizeId(value) {
403
420
  return /^[\w-]+$/.test(value);
404
421
  }
405
422
  /**
406
- * Create an error result.
423
+ * Create an error result with structured error content.
407
424
  */
408
425
  function errorResult(message) {
409
426
  return {
@@ -411,6 +428,10 @@ function errorResult(message) {
411
428
  type: "text",
412
429
  text: `Error: ${message}`
413
430
  }],
431
+ structuredContent: {
432
+ success: false,
433
+ error: message
434
+ },
414
435
  isError: true
415
436
  };
416
437
  }
@@ -1099,9 +1120,10 @@ async function handleDeployments(action, args, ctx) {
1099
1120
  if (ctx.compact) return jsonResult(formatDeploymentList(result.data));
1100
1121
  return jsonResult(result.data);
1101
1122
  }
1102
- case "deploy":
1103
- await deploySite(opts, ctx.executorContext);
1104
- return jsonResult(formatDeployAction(args.site_id, args.server_id));
1123
+ case "deploy": {
1124
+ const deployResult = await deploySiteAndWait(opts, ctx.executorContext);
1125
+ return jsonResult(formatDeployAction(args.site_id, args.server_id, deployResult.data));
1126
+ }
1105
1127
  case "get":
1106
1128
  if (args.id) {
1107
1129
  const result = await getDeploymentOutput({
@@ -3017,93 +3039,147 @@ const handleUser = createResourceHandler({
3017
3039
  return formatUser(data);
3018
3040
  }
3019
3041
  });
3020
- /** Valid resources derived from core constants */
3021
- var VALID_RESOURCES = [...RESOURCES];
3022
3042
  /**
3023
- * Route to the appropriate resource handler.
3043
+ * Read-only actions safe operations that don't modify server state.
3024
3044
  */
3025
- function routeToHandler(resource, action, args, ctx) {
3026
- switch (resource) {
3027
- case "servers": return handleServers(action, args, ctx);
3028
- case "sites": return handleSites(action, args, ctx);
3029
- case "deployments": return handleDeployments(action, args, ctx);
3030
- case "env": return handleEnv(action, args, ctx);
3031
- case "nginx": return handleNginxConfig(action, args, ctx);
3032
- case "certificates": return handleCertificates(action, args, ctx);
3033
- case "databases": return handleDatabases(action, args, ctx);
3034
- case "database-users": return handleDatabaseUsers(action, args, ctx);
3035
- case "daemons": return handleDaemons(action, args, ctx);
3036
- case "firewall-rules": return handleFirewallRules(action, args, ctx);
3037
- case "ssh-keys": return handleSshKeys(action, args, ctx);
3038
- case "security-rules": return handleSecurityRules(action, args, ctx);
3039
- case "redirect-rules": return handleRedirectRules(action, args, ctx);
3040
- case "monitors": return handleMonitors(action, args, ctx);
3041
- case "nginx-templates": return handleNginxTemplates(action, args, ctx);
3042
- case "recipes": return handleRecipes(action, args, ctx);
3043
- case "backups": return handleBackups(action, args, ctx);
3044
- case "commands": return handleCommands(action, args, ctx);
3045
- case "scheduled-jobs": return handleScheduledJobs(action, args, ctx);
3046
- case "user": return handleUser(action, args, ctx);
3047
- default: return Promise.resolve(errorResult(`Unknown resource "${resource}". Valid resources: ${VALID_RESOURCES.join(", ")}. Use action="help" for documentation.`));
3048
- }
3049
- }
3045
+ const READ_ACTIONS = [
3046
+ "list",
3047
+ "get",
3048
+ "help",
3049
+ "schema"
3050
+ ];
3050
3051
  /**
3051
- * Execute a tool call with provided credentials.
3052
- *
3053
- * This is the main entry point shared between stdio and HTTP transports.
3052
+ * Write actions operations that modify server state.
3053
+ * These are separated into the `forge_write` tool for safety.
3054
3054
  */
3055
- async function executeToolWithCredentials(_name, args, credentials) {
3056
- const { resource, action, compact, ...rest } = args;
3057
- if (!resource || !action) return errorResult("Missing required fields: \"resource\" and \"action\".");
3058
- if (action === "help")
3059
- /* v8 ignore start */
3060
- return resource ? handleHelp(resource) : handleHelpOverview();
3061
- if (action === "schema")
3062
- /* v8 ignore start */
3063
- return resource ? handleSchema(resource) : handleSchemaOverview();
3064
- if (!VALID_RESOURCES.includes(resource)) return errorResult(`Unknown resource "${resource}". Valid resources: ${VALID_RESOURCES.join(", ")}. Use action="help" for documentation.`);
3065
- const handlerContext = {
3066
- executorContext: { client: new HttpClient({ token: credentials.apiToken }) },
3067
- compact: compact ?? action !== "get",
3068
- includeHints: action === "get"
3069
- };
3070
- try {
3071
- return await routeToHandler(resource, action, {
3072
- resource,
3073
- action,
3074
- ...rest
3075
- }, handlerContext);
3076
- } catch (error) {
3077
- if (isUserInputError(error)) return errorResult(error.toFormattedMessage());
3078
- if (isForgeApiError(error)) return errorResult(`Forge API error (${error.status}): ${error.message}`);
3079
- /* v8 ignore stop */
3080
- return errorResult(error instanceof Error ? error.message : String(error));
3081
- }
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);
3082
3070
  }
3083
3071
  /**
3084
- * Generate the tool description dynamically from the constants.
3072
+ * Check if an action is a read action.
3085
3073
  */
3086
- function generateDescription() {
3087
- return [
3088
- "Laravel Forge API.",
3089
- `Resources: ${RESOURCES.join(", ")}.`,
3090
- `Actions: ${ACTIONS.join(", ")} (varies by resource).`,
3091
- "Discovery: action=help with any resource for filters and examples.",
3092
- "Server operations require id. Site operations require server_id.",
3093
- "Deployment operations require server_id and site_id."
3094
- ].join("\n");
3074
+ function isReadAction(action) {
3075
+ return READ_ACTIONS.includes(action);
3095
3076
  }
3096
3077
  /**
3097
- * Single consolidated tool for Laravel Forge MCP server.
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.
3098
3134
  *
3099
- * The resource/action enums and description are derived from
3100
- * the shared constants in forge-core the single source of truth.
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)
3101
3138
  */
3102
3139
  const TOOLS = [{
3103
3140
  name: "forge",
3104
- description: generateDescription(),
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"),
3105
3150
  annotations: {
3106
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)",
3107
3183
  readOnlyHint: false,
3108
3184
  destructiveHint: true,
3109
3185
  idempotentHint: false,
@@ -3112,108 +3188,132 @@ const TOOLS = [{
3112
3188
  inputSchema: {
3113
3189
  type: "object",
3114
3190
  properties: {
3115
- resource: {
3191
+ ...SHARED_INPUT_PROPERTIES,
3192
+ action: {
3116
3193
  type: "string",
3117
- enum: [...RESOURCES]
3194
+ enum: [...WRITE_ACTIONS],
3195
+ description: "Write action to perform"
3118
3196
  },
3119
- action: {
3197
+ name: {
3120
3198
  type: "string",
3121
- enum: [...ACTIONS],
3122
- description: "Use \"help\" for resource documentation"
3199
+ description: "Resource name (servers, databases, daemons, etc.)"
3123
3200
  },
3124
- id: {
3201
+ provider: {
3125
3202
  type: "string",
3126
- description: "Resource ID"
3203
+ description: "Server provider (e.g. ocean2, linode, aws)"
3127
3204
  },
3128
- server_id: {
3205
+ region: {
3129
3206
  type: "string",
3130
- description: "Server ID (required for most resources)"
3207
+ description: "Server region (e.g. nyc3, us-east-1)"
3131
3208
  },
3132
- site_id: {
3209
+ size: {
3133
3210
  type: "string",
3134
- description: "Site ID (required for site-level resources)"
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)"
3135
3232
  },
3136
- compact: {
3137
- type: "boolean",
3138
- description: "Compact output (default: true for list, false for get)"
3139
- },
3140
- name: { type: "string" },
3141
- provider: { type: "string" },
3142
- region: { type: "string" },
3143
- size: { type: "string" },
3144
- credential_id: { type: "string" },
3145
- type: { type: "string" },
3146
- domain: { type: "string" },
3147
- project_type: { type: "string" },
3148
- directory: { type: "string" },
3149
3233
  content: {
3150
3234
  type: "string",
3151
- description: "Content for env, nginx, or deployment script"
3235
+ description: "Content body for env variables, nginx config, or deployment script updates"
3152
3236
  },
3153
3237
  command: {
3154
3238
  type: "string",
3155
- description: "Shell command (daemons, recipes)"
3239
+ description: "Shell command to execute (daemons: background process command; recipes: bash script inline; commands: site command)"
3156
3240
  },
3157
3241
  user: {
3158
3242
  type: "string",
3159
- description: "Execution user (daemons, recipes)"
3243
+ description: "Unix user to run as (daemons, scheduled jobs; e.g. forge, root)"
3160
3244
  },
3161
3245
  port: {
3162
3246
  type: ["string", "number"],
3163
- description: "Port number or range (firewall rules)"
3247
+ description: "Port number or range (firewall rules, e.g. 80 or 8000-9000)"
3164
3248
  },
3165
3249
  ip_address: {
3166
3250
  type: "string",
3167
- description: "IP address (firewall rules)"
3251
+ description: "IP address to allow/block (firewall rules, e.g. 192.168.1.1)"
3168
3252
  },
3169
3253
  key: {
3170
3254
  type: "string",
3171
- description: "Public SSH key content"
3255
+ description: "Public SSH key content (ssh-rsa ... or ssh-ed25519 ...)"
3172
3256
  },
3173
3257
  from: {
3174
3258
  type: "string",
3175
- description: "Source path (redirect rules)"
3259
+ description: "Source path for redirect rules (e.g. /old-page)"
3176
3260
  },
3177
3261
  to: {
3178
3262
  type: "string",
3179
- description: "Destination URL (redirect rules)"
3263
+ description: "Destination URL for redirect rules (e.g. /new-page)"
3180
3264
  },
3181
3265
  credentials: {
3182
3266
  type: "array",
3183
- description: "Credentials array [{username, password}] (security rules)",
3267
+ description: "HTTP basic auth credentials for security rules [{username, password}]",
3184
3268
  items: { type: "object" }
3185
3269
  },
3186
3270
  operator: {
3187
3271
  type: "string",
3188
- description: "Comparison operator (monitors)"
3272
+ description: "Comparison operator for monitors (e.g. gte, lte)"
3189
3273
  },
3190
3274
  threshold: {
3191
3275
  type: "number",
3192
- description: "Threshold value (monitors)"
3276
+ description: "Threshold value that triggers the monitor alert"
3193
3277
  },
3194
3278
  minutes: {
3195
3279
  type: "number",
3196
- description: "Check interval in minutes (monitors)"
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)"
3197
3285
  },
3198
3286
  script: {
3199
3287
  type: "string",
3200
- description: "Bash script content (recipes)"
3288
+ description: "Bash script content for recipes"
3201
3289
  },
3202
3290
  servers: {
3203
3291
  type: "array",
3204
- description: "Server IDs to run recipe on",
3292
+ description: "Server IDs to run a recipe on (e.g. [123, 456])",
3205
3293
  items: { type: "number" }
3206
3294
  }
3207
3295
  },
3208
3296
  required: ["resource", "action"]
3209
- }
3297
+ },
3298
+ outputSchema: OUTPUT_SCHEMA
3210
3299
  }];
3211
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
+ /**
3212
3311
  * Additional tools only available in stdio mode.
3213
3312
  */
3214
3313
  const STDIO_ONLY_TOOLS = [{
3215
3314
  name: "forge_configure",
3216
- description: "Configure Laravel Forge API token",
3315
+ title: "Configure Forge",
3316
+ description: "Configure Laravel Forge API token. The token is stored locally in the XDG config directory.",
3217
3317
  annotations: {
3218
3318
  title: "Configure Forge",
3219
3319
  readOnlyHint: false,
@@ -3228,10 +3328,26 @@ const STDIO_ONLY_TOOLS = [{
3228
3328
  description: "Your Laravel Forge API token"
3229
3329
  } },
3230
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"]
3231
3346
  }
3232
3347
  }, {
3233
3348
  name: "forge_get_config",
3234
- description: "Get current Forge configuration",
3349
+ title: "Get Forge Config",
3350
+ description: "Get current Forge configuration (shows masked token and config status).",
3235
3351
  annotations: {
3236
3352
  title: "Get Forge Config",
3237
3353
  readOnlyHint: true,
@@ -3242,9 +3358,113 @@ const STDIO_ONLY_TOOLS = [{
3242
3358
  inputSchema: {
3243
3359
  type: "object",
3244
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"]
3245
3375
  }
3246
3376
  }];
3247
- const VERSION = "0.1.0";
3248
- export { INSTRUCTIONS as a, executeToolWithCredentials as i, STDIO_ONLY_TOOLS as n, TOOLS as r, VERSION as t };
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 };
3249
3469
 
3250
- //# sourceMappingURL=version-Cw8OGt4r.js.map
3470
+ //# sourceMappingURL=version-DaD5zvGh.js.map