diff --git a/packages/cli-v3/src/commands/install-mcp.ts b/packages/cli-v3/src/commands/install-mcp.ts index 545014292d..ab1d1b170f 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,7 +208,15 @@ export async function installMcpServer( writeConfigHasSeenMCPInstallPrompt(true); - const devOnly = await resolveDevOnly(opts); + // 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." + ); + } + + // Skip devOnly prompt when envOnly is already provided + const devOnly = opts.envOnly ? false : await resolveDevOnly(opts); opts.devOnly = devOnly; @@ -265,10 +288,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); } @@ -476,10 +510,21 @@ function resolveMcpServerConfig( 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 8604a455da..961c8f72a5 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,47 @@ 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) { + // 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']; + 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']; + } + 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..e7a2a5751b 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,25 @@ export class McpContext { public get hasElicitationCapability() { return hasElicitationCapability(this.server); } + + 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) { + // Note: envOnly is already normalized to lowercase in mcp.ts + return this.options.envOnly.includes(normalizedEnv); + } + + // 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.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..cb5262b9ae 100644 --- a/packages/cli-v3/src/mcp/tools/deploys.ts +++ b/packages/cli-v3/src/mcp/tools/deploys.ts @@ -18,9 +18,10 @@ 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)) { return respondWithError( - `This MCP server is only available for the dev environment. The deploy command is not allowed with the --dev-only flag.` + `Cannot deploy to ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -118,9 +119,9 @@ 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)) { 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.` + `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 13fe601da0..c42a6a86db 100644 --- a/packages/cli-v3/src/mcp/tools/runs.ts +++ b/packages/cli-v3/src/mcp/tools/runs.ts @@ -12,9 +12,9 @@ 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)) { 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.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -69,9 +69,9 @@ 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)) { 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.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -122,9 +122,9 @@ 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)) { 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.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -162,9 +162,9 @@ 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)) { 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.` + `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 41c988ce1a..d18cdb4ab9 100644 --- a/packages/cli-v3/src/mcp/tools/tasks.ts +++ b/packages/cli-v3/src/mcp/tools/tasks.ts @@ -11,9 +11,9 @@ 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)) { 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.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); } @@ -90,9 +90,9 @@ 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)) { 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.` + `Cannot access ${input.environment} environment. This MCP server is restricted to: ${ctx.getAllowedEnvironments()}` ); }