Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions packages/cli-v3/src/commands/install-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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 <environments>",
"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 <scope>", "Choose the scope of the MCP server, either user or project")
.option(
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
53 changes: 52 additions & 1 deletion packages/cli-v3/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand All @@ -34,7 +37,19 @@ export function configureMcpCommand(program: Command) {
.option("-p, --project-ref <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 <environments>",
"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 <log file>", "The file to log to")
.addOption(
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions packages/cli-v3/src/mcp/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type McpContextOptions = {
apiUrl?: string;
profile?: string;
devOnly?: boolean;
envOnly?: string[];
disableDeployment?: boolean;
readonly?: boolean;
};

export class McpContext {
Expand Down Expand Up @@ -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";
}
}
35 changes: 28 additions & 7 deletions packages/cli-v3/src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions packages/cli-v3/src/mcp/tools/deploys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`
);
}

Expand Down Expand Up @@ -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()}`
);
}

Expand Down
16 changes: 8 additions & 8 deletions packages/cli-v3/src/mcp/tools/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`
);
}

Expand Down Expand Up @@ -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()}`
);
}

Expand Down Expand Up @@ -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()}`
);
}

Expand Down Expand Up @@ -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()}`
);
}

Expand Down
8 changes: 4 additions & 4 deletions packages/cli-v3/src/mcp/tools/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`
);
}

Expand Down Expand Up @@ -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()}`
);
}

Expand Down