From 5f6f63594298d402919358d8fc3a32b0bb4dacf6 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 23 Sep 2025 15:20:45 +0800 Subject: [PATCH 1/4] feat(cli): add disable-deployment, readonly, and env-only options to MCP server Add three new options to the MCP server for better control: - --disable-deployment: Disables deployment-related tools - --readonly: Run in read-only mode, disabling all write operations (deployments, task triggering, project creation, run cancellation) - --env-only: Restrict the MCP server to specific environments (dev, staging, prod, preview) Key features: - The --env-only flag allows fine-grained environment control and deprecates --dev-only (kept for backward compatibility) - The --readonly flag overrides --disable-deployment since deployments are write operations - Deployments are allowed to any environment that the MCP server has access to - --dev-only and --env-only are mutually exclusive with proper error handling - listDeploysTool correctly categorized as read-only operation Signed-off-by: Andy --- packages/cli-v3/src/commands/install-mcp.ts | 36 ++++++++++++++++++++- packages/cli-v3/src/commands/mcp.ts | 36 ++++++++++++++++++++- packages/cli-v3/src/mcp/context.ts | 18 +++++++++++ packages/cli-v3/src/mcp/tools.ts | 35 ++++++++++++++++---- packages/cli-v3/src/mcp/tools/deploys.ts | 11 ++++--- packages/cli-v3/src/mcp/tools/runs.ts | 20 +++++++----- packages/cli-v3/src/mcp/tools/tasks.ts | 10 +++--- 7 files changed, 141 insertions(+), 25 deletions(-) diff --git a/packages/cli-v3/src/commands/install-mcp.ts b/packages/cli-v3/src/commands/install-mcp.ts index 545014292d..08eea5c24a 100644 --- a/packages/cli-v3/src/commands/install-mcp.ts +++ b/packages/cli-v3/src/commands/install-mcp.ts @@ -114,6 +114,9 @@ const InstallMcpCommandOptions = z.object({ projectRef: z.string().optional(), tag: z.string().default(cliTag), devOnly: z.boolean().optional(), + envOnly: z.string().optional(), + disableDeployment: z.boolean().optional(), + readonly: z.boolean().optional(), yolo: z.boolean().default(false), scope: z.enum(scopes).optional(), client: z.enum(clients).array().optional(), @@ -137,7 +140,19 @@ export function configureInstallMcpCommand(program: Command) { "The version of the trigger.dev CLI package to use for the MCP server", cliTag ) - .option("--dev-only", "Restrict the MCP server to the dev environment only") + .option("--dev-only", "Restrict the MCP server to the dev environment only (Deprecated: use --env-only dev)") + .option( + "--env-only ", + "Restrict the MCP server to specific environments only. Comma-separated list of: dev, staging, prod, preview" + ) + .option( + "--disable-deployment", + "Disable deployment-related tools in the MCP server. Overridden by --readonly." + ) + .option( + "--readonly", + "Run MCP server in read-only mode. Disables all write operations. Overrides --disable-deployment." + ) .option("--yolo", "Install the MCP server into all supported clients") .option("--scope ", "Choose the scope of the MCP server, either user or project") .option( @@ -193,6 +208,13 @@ export async function installMcpServer( writeConfigHasSeenMCPInstallPrompt(true); + // Check for mutual exclusivity + if (opts.devOnly && opts.envOnly) { + throw new OutroCommandError( + "--dev-only and --env-only are mutually exclusive. Please use only one. Consider using --env-only dev instead of --dev-only." + ); + } + const devOnly = await resolveDevOnly(opts); opts.devOnly = devOnly; @@ -480,6 +502,18 @@ function resolveMcpServerConfig( args.push("--dev-only"); } + if (options.envOnly) { + args.push("--env-only", options.envOnly); + } + + if (options.disableDeployment) { + args.push("--disable-deployment"); + } + + if (options.readonly) { + args.push("--readonly"); + } + if (options.projectRef) { args.push("--project-ref", options.projectRef); } diff --git a/packages/cli-v3/src/commands/mcp.ts b/packages/cli-v3/src/commands/mcp.ts index 8604a455da..b54e5019e9 100644 --- a/packages/cli-v3/src/commands/mcp.ts +++ b/packages/cli-v3/src/commands/mcp.ts @@ -20,6 +20,9 @@ const McpCommandOptions = CommonCommandOptions.extend({ projectRef: z.string().optional(), logFile: z.string().optional(), devOnly: z.boolean().default(false), + envOnly: z.string().optional(), + disableDeployment: z.boolean().default(false), + readonly: z.boolean().default(false), rulesInstallManifestPath: z.string().optional(), rulesInstallBranch: z.string().optional(), }); @@ -34,7 +37,19 @@ export function configureMcpCommand(program: Command) { .option("-p, --project-ref ", "The project ref to use") .option( "--dev-only", - "Only run the MCP server for the dev environment. Attempts to access other environments will fail." + "Only run the MCP server for the dev environment. Attempts to access other environments will fail. (Deprecated: use --env-only dev instead)" + ) + .option( + "--env-only ", + "Restrict the MCP server to specific environments only. Comma-separated list of: dev, staging, prod, preview. Example: --env-only dev,staging" + ) + .option( + "--disable-deployment", + "Disable deployment-related tools. When enabled, deployment tools won't be available. This option is overridden by --readonly." + ) + .option( + "--readonly", + "Run in read-only mode. Disables all write operations including deployments, task triggering, and project creation. Overrides --disable-deployment." ) .option("--log-file ", "The file to log to") .addOption( @@ -106,11 +121,30 @@ export async function mcpCommand(options: McpCommandOptions) { ? new FileLogger(options.logFile, server) : undefined; + // Check for mutual exclusivity + if (options.devOnly && options.envOnly) { + logger.error("Error: --dev-only and --env-only are mutually exclusive. Please use only one."); + process.exit(1); + } + + // Parse envOnly into an array if provided + let envOnly: string[] | undefined; + if (options.envOnly) { + envOnly = options.envOnly.split(',').map(env => env.trim()); + } else if (options.devOnly) { + // For backward compatibility, convert devOnly to envOnly + envOnly = ['dev']; + } + const context = new McpContext(server, { projectRef: options.projectRef, fileLogger, apiUrl: options.apiUrl ?? CLOUD_API_URL, profile: options.profile, + devOnly: options.devOnly, + envOnly, + disableDeployment: options.disableDeployment, + readonly: options.readonly, }); registerTools(context); diff --git a/packages/cli-v3/src/mcp/context.ts b/packages/cli-v3/src/mcp/context.ts index 75f6abd2a3..a986a1480c 100644 --- a/packages/cli-v3/src/mcp/context.ts +++ b/packages/cli-v3/src/mcp/context.ts @@ -19,6 +19,9 @@ export type McpContextOptions = { apiUrl?: string; profile?: string; devOnly?: boolean; + envOnly?: string[]; + disableDeployment?: boolean; + readonly?: boolean; }; export class McpContext { @@ -184,4 +187,19 @@ export class McpContext { public get hasElicitationCapability() { return hasElicitationCapability(this.server); } + + public isEnvironmentAllowed(environment: string): boolean { + // If envOnly is specified, use that + if (this.options.envOnly && this.options.envOnly.length > 0) { + return this.options.envOnly.includes(environment); + } + + // For backward compatibility, check devOnly + if (this.options.devOnly) { + return environment === "dev"; + } + + // If neither is specified, all environments are allowed + return true; + } } diff --git a/packages/cli-v3/src/mcp/tools.ts b/packages/cli-v3/src/mcp/tools.ts index 5c69bb82fe..316c51a934 100644 --- a/packages/cli-v3/src/mcp/tools.ts +++ b/packages/cli-v3/src/mcp/tools.ts @@ -18,23 +18,44 @@ import { getCurrentWorker, triggerTaskTool } from "./tools/tasks.js"; import { respondWithError } from "./utils.js"; export function registerTools(context: McpContext) { - const tools = [ + // Always available read-only tools + const readOnlyTools = [ searchDocsTool, listOrgsTool, listProjectsTool, - createProjectInOrgTool, - initializeProjectTool, getCurrentWorker, - triggerTaskTool, listRunsTool, getRunDetailsTool, waitForRunToCompleteTool, - cancelRunTool, - deployTool, - listDeploysTool, listPreviewBranchesTool, + listDeploysTool, // This is a read operation, not a write ]; + // Write tools that are disabled in readonly mode + const writeTools = [ + createProjectInOrgTool, + initializeProjectTool, + triggerTaskTool, + cancelRunTool, + ]; + + // Deployment tools that can be independently disabled + const deploymentTools = [ + deployTool, // Only the actual deploy command is a write operation + ]; + + let tools = [...readOnlyTools]; + + // Add write tools if not in readonly mode + if (!context.options.readonly) { + tools = [...tools, ...writeTools]; + } + + // Add deployment tools if not disabled and not in readonly mode + if (!context.options.disableDeployment && !context.options.readonly) { + tools = [...tools, ...deploymentTools]; + } + for (const tool of tools) { context.server.registerTool( tool.name, diff --git a/packages/cli-v3/src/mcp/tools/deploys.ts b/packages/cli-v3/src/mcp/tools/deploys.ts index ab09659a54..27e755e24d 100644 --- a/packages/cli-v3/src/mcp/tools/deploys.ts +++ b/packages/cli-v3/src/mcp/tools/deploys.ts @@ -18,9 +18,11 @@ export const deployTool = { handler: toolHandler(DeployInput.shape, async (input, { ctx, createProgressTracker, _meta }) => { ctx.logger?.log("calling deploy", { input }); - if (ctx.options.devOnly) { + // Check if the deployment target environment is allowed + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. The deploy command is not allowed with the --dev-only flag.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You cannot deploy to ${input.environment}.` ); } @@ -118,9 +120,10 @@ export const listDeploysTool = { handler: toolHandler(ListDeploysInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling list_deploys", { input }); - if (ctx.options.devOnly) { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } diff --git a/packages/cli-v3/src/mcp/tools/runs.ts b/packages/cli-v3/src/mcp/tools/runs.ts index 13fe601da0..1954e4e7c8 100644 --- a/packages/cli-v3/src/mcp/tools/runs.ts +++ b/packages/cli-v3/src/mcp/tools/runs.ts @@ -12,9 +12,10 @@ export const getRunDetailsTool = { handler: toolHandler(GetRunDetailsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling get_run_details", { input }); - if (ctx.options.devOnly && input.environment !== "dev") { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } @@ -69,9 +70,10 @@ export const waitForRunToCompleteTool = { handler: toolHandler(CommonRunsInput.shape, async (input, { ctx, signal }) => { ctx.logger?.log("calling wait_for_run_to_complete", { input }); - if (ctx.options.devOnly && input.environment !== "dev") { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } @@ -122,9 +124,10 @@ export const cancelRunTool = { handler: toolHandler(CommonRunsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling cancel_run", { input }); - if (ctx.options.devOnly && input.environment !== "dev") { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } @@ -162,9 +165,10 @@ export const listRunsTool = { handler: toolHandler(ListRunsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling list_runs", { input }); - if (ctx.options.devOnly && input.environment !== "dev") { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } diff --git a/packages/cli-v3/src/mcp/tools/tasks.ts b/packages/cli-v3/src/mcp/tools/tasks.ts index 41c988ce1a..c63076dc69 100644 --- a/packages/cli-v3/src/mcp/tools/tasks.ts +++ b/packages/cli-v3/src/mcp/tools/tasks.ts @@ -11,9 +11,10 @@ export const getCurrentWorker = { handler: toolHandler(CommonProjectsInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling get_current_worker", { input }); - if (ctx.options.devOnly && input.environment !== "dev") { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } @@ -90,9 +91,10 @@ export const triggerTaskTool = { handler: toolHandler(TriggerTaskInput.shape, async (input, { ctx }) => { ctx.logger?.log("calling trigger_task", { input }); - if (ctx.options.devOnly && input.environment !== "dev") { + if (!ctx.isEnvironmentAllowed(input.environment)) { + const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` ); } From 3526f0f65141d0d2c8dfadd8438211b8f9c07cd7 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 23 Sep 2025 15:38:39 +0800 Subject: [PATCH 2/4] refactor: improve MCP server option validation and error messages - Add environment name validation for --env-only flag - Simplify environment validation logic (remove redundant devOnly check) - Standardize error messages across all tools - Add getAllowedEnvironments() helper method for consistent messaging Signed-off-by: Andy --- packages/cli-v3/src/commands/mcp.ts | 9 +++++++++ packages/cli-v3/src/mcp/context.ts | 16 +++++++++------- packages/cli-v3/src/mcp/tools/deploys.ts | 6 ++---- packages/cli-v3/src/mcp/tools/runs.ts | 12 ++++-------- packages/cli-v3/src/mcp/tools/tasks.ts | 6 ++---- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/cli-v3/src/commands/mcp.ts b/packages/cli-v3/src/commands/mcp.ts index b54e5019e9..40e41e5776 100644 --- a/packages/cli-v3/src/commands/mcp.ts +++ b/packages/cli-v3/src/commands/mcp.ts @@ -131,6 +131,15 @@ export async function mcpCommand(options: McpCommandOptions) { let envOnly: string[] | undefined; if (options.envOnly) { envOnly = options.envOnly.split(',').map(env => env.trim()); + + // Validate environment names + const validEnvironments = ['dev', 'staging', 'prod', 'preview']; + const invalidEnvs = envOnly.filter(env => !validEnvironments.includes(env)); + if (invalidEnvs.length > 0) { + logger.error(`Error: Invalid environment(s): ${invalidEnvs.join(', ')}`); + logger.error(`Valid environments are: ${validEnvironments.join(', ')}`); + process.exit(1); + } } else if (options.devOnly) { // For backward compatibility, convert devOnly to envOnly envOnly = ['dev']; diff --git a/packages/cli-v3/src/mcp/context.ts b/packages/cli-v3/src/mcp/context.ts index a986a1480c..c142937d24 100644 --- a/packages/cli-v3/src/mcp/context.ts +++ b/packages/cli-v3/src/mcp/context.ts @@ -189,17 +189,19 @@ export class McpContext { } public isEnvironmentAllowed(environment: string): boolean { - // If envOnly is specified, use that + // If envOnly is specified, use that (devOnly is already converted to envOnly) if (this.options.envOnly && this.options.envOnly.length > 0) { return this.options.envOnly.includes(environment); } - // For backward compatibility, check devOnly - if (this.options.devOnly) { - return environment === "dev"; - } - - // If neither is specified, all environments are allowed + // If no restrictions, all environments are allowed return true; } + + public getAllowedEnvironments(): string { + if (this.options.envOnly && this.options.envOnly.length > 0) { + return this.options.envOnly.join(", "); + } + return "all environments"; + } } diff --git a/packages/cli-v3/src/mcp/tools/deploys.ts b/packages/cli-v3/src/mcp/tools/deploys.ts index 27e755e24d..cb5262b9ae 100644 --- a/packages/cli-v3/src/mcp/tools/deploys.ts +++ b/packages/cli-v3/src/mcp/tools/deploys.ts @@ -20,9 +20,8 @@ export const deployTool = { // Check if the deployment target environment is allowed if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You cannot deploy to ${input.environment}.` + `Cannot deploy to ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -121,9 +120,8 @@ export const listDeploysTool = { ctx.logger?.log("calling list_deploys", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } diff --git a/packages/cli-v3/src/mcp/tools/runs.ts b/packages/cli-v3/src/mcp/tools/runs.ts index 1954e4e7c8..c42a6a86db 100644 --- a/packages/cli-v3/src/mcp/tools/runs.ts +++ b/packages/cli-v3/src/mcp/tools/runs.ts @@ -13,9 +13,8 @@ export const getRunDetailsTool = { ctx.logger?.log("calling get_run_details", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -71,9 +70,8 @@ export const waitForRunToCompleteTool = { ctx.logger?.log("calling wait_for_run_to_complete", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -125,9 +123,8 @@ export const cancelRunTool = { ctx.logger?.log("calling cancel_run", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -166,9 +163,8 @@ export const listRunsTool = { ctx.logger?.log("calling list_runs", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } diff --git a/packages/cli-v3/src/mcp/tools/tasks.ts b/packages/cli-v3/src/mcp/tools/tasks.ts index c63076dc69..d18cdb4ab9 100644 --- a/packages/cli-v3/src/mcp/tools/tasks.ts +++ b/packages/cli-v3/src/mcp/tools/tasks.ts @@ -12,9 +12,8 @@ export const getCurrentWorker = { ctx.logger?.log("calling get_current_worker", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -92,9 +91,8 @@ export const triggerTaskTool = { ctx.logger?.log("calling trigger_task", { input }); if (!ctx.isEnvironmentAllowed(input.environment)) { - const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev"; return respondWithError( - `This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } From 28c8cddab877626cdc65315d230f75bcf5c3abc9 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 23 Sep 2025 15:53:57 +0800 Subject: [PATCH 3/4] fix: address CodeRabbit review feedback - Fix manual config for unsupported clients to include all new flags - Improve environment parsing to handle edge cases (empty strings, duplicates) - Normalize environment names (lowercase, trim) for consistent matching - Ensure envOnly takes precedence over devOnly in all places Signed-off-by: Andy --- packages/cli-v3/src/commands/install-mcp.ts | 13 ++++++++++++- packages/cli-v3/src/commands/mcp.ts | 10 +++++++++- packages/cli-v3/src/mcp/context.ts | 6 +++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/cli-v3/src/commands/install-mcp.ts b/packages/cli-v3/src/commands/install-mcp.ts index 08eea5c24a..3014b5caae 100644 --- a/packages/cli-v3/src/commands/install-mcp.ts +++ b/packages/cli-v3/src/commands/install-mcp.ts @@ -287,10 +287,21 @@ function handleUnsupportedClientOnly(options: InstallMcpCommandOptions): Install args.push("--api-url", options.apiUrl); } - if (options.devOnly) { + // Handle environment restrictions - envOnly takes precedence + if (options.envOnly) { + args.push("--env-only", options.envOnly); + } else if (options.devOnly) { args.push("--dev-only"); } + if (options.disableDeployment) { + args.push("--disable-deployment"); + } + + if (options.readonly) { + args.push("--readonly"); + } + if (options.projectRef) { args.push("--project-ref", options.projectRef); } diff --git a/packages/cli-v3/src/commands/mcp.ts b/packages/cli-v3/src/commands/mcp.ts index 40e41e5776..961c8f72a5 100644 --- a/packages/cli-v3/src/commands/mcp.ts +++ b/packages/cli-v3/src/commands/mcp.ts @@ -130,7 +130,15 @@ export async function mcpCommand(options: McpCommandOptions) { // Parse envOnly into an array if provided let envOnly: string[] | undefined; if (options.envOnly) { - envOnly = options.envOnly.split(',').map(env => env.trim()); + // Parse, normalize, and deduplicate environments + envOnly = Array.from( + new Set( + options.envOnly + .split(',') + .map(env => env.trim().toLowerCase()) + .filter(Boolean) // Remove empty strings + ) + ); // Validate environment names const validEnvironments = ['dev', 'staging', 'prod', 'preview']; diff --git a/packages/cli-v3/src/mcp/context.ts b/packages/cli-v3/src/mcp/context.ts index c142937d24..e7a2a5751b 100644 --- a/packages/cli-v3/src/mcp/context.ts +++ b/packages/cli-v3/src/mcp/context.ts @@ -189,9 +189,13 @@ export class McpContext { } public isEnvironmentAllowed(environment: string): boolean { + // Normalize the environment name for comparison + const normalizedEnv = environment.trim().toLowerCase(); + // If envOnly is specified, use that (devOnly is already converted to envOnly) if (this.options.envOnly && this.options.envOnly.length > 0) { - return this.options.envOnly.includes(environment); + // Note: envOnly is already normalized to lowercase in mcp.ts + return this.options.envOnly.includes(normalizedEnv); } // If no restrictions, all environments are allowed From 180e506da0e92b5efb7c1d850a62163bfc67671b Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 23 Sep 2025 16:14:10 +0800 Subject: [PATCH 4/4] fix: ensure envOnly takes precedence over devOnly in install-mcp - Skip devOnly prompt when envOnly is already provided - Ensure only one of --env-only or --dev-only is emitted in config - Addresses CodeRabbit review feedback --- packages/cli-v3/src/commands/install-mcp.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli-v3/src/commands/install-mcp.ts b/packages/cli-v3/src/commands/install-mcp.ts index 3014b5caae..ab1d1b170f 100644 --- a/packages/cli-v3/src/commands/install-mcp.ts +++ b/packages/cli-v3/src/commands/install-mcp.ts @@ -215,7 +215,8 @@ export async function installMcpServer( ); } - const devOnly = await resolveDevOnly(opts); + // Skip devOnly prompt when envOnly is already provided + const devOnly = opts.envOnly ? false : await resolveDevOnly(opts); opts.devOnly = devOnly; @@ -509,12 +510,11 @@ function resolveMcpServerConfig( args.push("--api-url", options.apiUrl); } - if (options.devOnly) { - args.push("--dev-only"); - } - + // Handle environment restrictions - envOnly takes precedence if (options.envOnly) { args.push("--env-only", options.envOnly); + } else if (options.devOnly) { + args.push("--dev-only"); } if (options.disableDeployment) {