Skip to content

Commit b39ea3f

Browse files
hongkongkiwiclaude
andcommitted
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 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 49728b5 commit b39ea3f

File tree

7 files changed

+141
-25
lines changed

7 files changed

+141
-25
lines changed

packages/cli-v3/src/commands/install-mcp.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ const InstallMcpCommandOptions = z.object({
114114
projectRef: z.string().optional(),
115115
tag: z.string().default(cliTag),
116116
devOnly: z.boolean().optional(),
117+
envOnly: z.string().optional(),
118+
disableDeployment: z.boolean().optional(),
119+
readonly: z.boolean().optional(),
117120
yolo: z.boolean().default(false),
118121
scope: z.enum(scopes).optional(),
119122
client: z.enum(clients).array().optional(),
@@ -137,7 +140,19 @@ export function configureInstallMcpCommand(program: Command) {
137140
"The version of the trigger.dev CLI package to use for the MCP server",
138141
cliTag
139142
)
140-
.option("--dev-only", "Restrict the MCP server to the dev environment only")
143+
.option("--dev-only", "Restrict the MCP server to the dev environment only (Deprecated: use --env-only dev)")
144+
.option(
145+
"--env-only <environments>",
146+
"Restrict the MCP server to specific environments only. Comma-separated list of: dev, staging, prod, preview"
147+
)
148+
.option(
149+
"--disable-deployment",
150+
"Disable deployment-related tools in the MCP server. Overridden by --readonly."
151+
)
152+
.option(
153+
"--readonly",
154+
"Run MCP server in read-only mode. Disables all write operations. Overrides --disable-deployment."
155+
)
141156
.option("--yolo", "Install the MCP server into all supported clients")
142157
.option("--scope <scope>", "Choose the scope of the MCP server, either user or project")
143158
.option(
@@ -193,6 +208,13 @@ export async function installMcpServer(
193208

194209
writeConfigHasSeenMCPInstallPrompt(true);
195210

211+
// Check for mutual exclusivity
212+
if (opts.devOnly && opts.envOnly) {
213+
throw new OutroCommandError(
214+
"--dev-only and --env-only are mutually exclusive. Please use only one. Consider using --env-only dev instead of --dev-only."
215+
);
216+
}
217+
196218
const devOnly = await resolveDevOnly(opts);
197219

198220
opts.devOnly = devOnly;
@@ -480,6 +502,18 @@ function resolveMcpServerConfig(
480502
args.push("--dev-only");
481503
}
482504

505+
if (options.envOnly) {
506+
args.push("--env-only", options.envOnly);
507+
}
508+
509+
if (options.disableDeployment) {
510+
args.push("--disable-deployment");
511+
}
512+
513+
if (options.readonly) {
514+
args.push("--readonly");
515+
}
516+
483517
if (options.projectRef) {
484518
args.push("--project-ref", options.projectRef);
485519
}

packages/cli-v3/src/commands/mcp.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const McpCommandOptions = CommonCommandOptions.extend({
2020
projectRef: z.string().optional(),
2121
logFile: z.string().optional(),
2222
devOnly: z.boolean().default(false),
23+
envOnly: z.string().optional(),
24+
disableDeployment: z.boolean().default(false),
25+
readonly: z.boolean().default(false),
2326
rulesInstallManifestPath: z.string().optional(),
2427
rulesInstallBranch: z.string().optional(),
2528
});
@@ -34,7 +37,19 @@ export function configureMcpCommand(program: Command) {
3437
.option("-p, --project-ref <project ref>", "The project ref to use")
3538
.option(
3639
"--dev-only",
37-
"Only run the MCP server for the dev environment. Attempts to access other environments will fail."
40+
"Only run the MCP server for the dev environment. Attempts to access other environments will fail. (Deprecated: use --env-only dev instead)"
41+
)
42+
.option(
43+
"--env-only <environments>",
44+
"Restrict the MCP server to specific environments only. Comma-separated list of: dev, staging, prod, preview. Example: --env-only dev,staging"
45+
)
46+
.option(
47+
"--disable-deployment",
48+
"Disable deployment-related tools. When enabled, deployment tools won't be available. This option is overridden by --readonly."
49+
)
50+
.option(
51+
"--readonly",
52+
"Run in read-only mode. Disables all write operations including deployments, task triggering, and project creation. Overrides --disable-deployment."
3853
)
3954
.option("--log-file <log file>", "The file to log to")
4055
.addOption(
@@ -106,11 +121,30 @@ export async function mcpCommand(options: McpCommandOptions) {
106121
? new FileLogger(options.logFile, server)
107122
: undefined;
108123

124+
// Check for mutual exclusivity
125+
if (options.devOnly && options.envOnly) {
126+
logger.error("Error: --dev-only and --env-only are mutually exclusive. Please use only one.");
127+
process.exit(1);
128+
}
129+
130+
// Parse envOnly into an array if provided
131+
let envOnly: string[] | undefined;
132+
if (options.envOnly) {
133+
envOnly = options.envOnly.split(',').map(env => env.trim());
134+
} else if (options.devOnly) {
135+
// For backward compatibility, convert devOnly to envOnly
136+
envOnly = ['dev'];
137+
}
138+
109139
const context = new McpContext(server, {
110140
projectRef: options.projectRef,
111141
fileLogger,
112142
apiUrl: options.apiUrl ?? CLOUD_API_URL,
113143
profile: options.profile,
144+
devOnly: options.devOnly,
145+
envOnly,
146+
disableDeployment: options.disableDeployment,
147+
readonly: options.readonly,
114148
});
115149

116150
registerTools(context);

packages/cli-v3/src/mcp/context.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export type McpContextOptions = {
1919
apiUrl?: string;
2020
profile?: string;
2121
devOnly?: boolean;
22+
envOnly?: string[];
23+
disableDeployment?: boolean;
24+
readonly?: boolean;
2225
};
2326

2427
export class McpContext {
@@ -184,4 +187,19 @@ export class McpContext {
184187
public get hasElicitationCapability() {
185188
return hasElicitationCapability(this.server);
186189
}
190+
191+
public isEnvironmentAllowed(environment: string): boolean {
192+
// If envOnly is specified, use that
193+
if (this.options.envOnly && this.options.envOnly.length > 0) {
194+
return this.options.envOnly.includes(environment);
195+
}
196+
197+
// For backward compatibility, check devOnly
198+
if (this.options.devOnly) {
199+
return environment === "dev";
200+
}
201+
202+
// If neither is specified, all environments are allowed
203+
return true;
204+
}
187205
}

packages/cli-v3/src/mcp/tools.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,44 @@ import { getCurrentWorker, triggerTaskTool } from "./tools/tasks.js";
1818
import { respondWithError } from "./utils.js";
1919

2020
export function registerTools(context: McpContext) {
21-
const tools = [
21+
// Always available read-only tools
22+
const readOnlyTools = [
2223
searchDocsTool,
2324
listOrgsTool,
2425
listProjectsTool,
25-
createProjectInOrgTool,
26-
initializeProjectTool,
2726
getCurrentWorker,
28-
triggerTaskTool,
2927
listRunsTool,
3028
getRunDetailsTool,
3129
waitForRunToCompleteTool,
32-
cancelRunTool,
33-
deployTool,
34-
listDeploysTool,
3530
listPreviewBranchesTool,
31+
listDeploysTool, // This is a read operation, not a write
3632
];
3733

34+
// Write tools that are disabled in readonly mode
35+
const writeTools = [
36+
createProjectInOrgTool,
37+
initializeProjectTool,
38+
triggerTaskTool,
39+
cancelRunTool,
40+
];
41+
42+
// Deployment tools that can be independently disabled
43+
const deploymentTools = [
44+
deployTool, // Only the actual deploy command is a write operation
45+
];
46+
47+
let tools = [...readOnlyTools];
48+
49+
// Add write tools if not in readonly mode
50+
if (!context.options.readonly) {
51+
tools = [...tools, ...writeTools];
52+
}
53+
54+
// Add deployment tools if not disabled and not in readonly mode
55+
if (!context.options.disableDeployment && !context.options.readonly) {
56+
tools = [...tools, ...deploymentTools];
57+
}
58+
3859
for (const tool of tools) {
3960
context.server.registerTool(
4061
tool.name,

packages/cli-v3/src/mcp/tools/deploys.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ export const deployTool = {
1818
handler: toolHandler(DeployInput.shape, async (input, { ctx, createProgressTracker, _meta }) => {
1919
ctx.logger?.log("calling deploy", { input });
2020

21-
if (ctx.options.devOnly) {
21+
// Check if the deployment target environment is allowed
22+
if (!ctx.isEnvironmentAllowed(input.environment)) {
23+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
2224
return respondWithError(
23-
`This MCP server is only available for the dev environment. The deploy command is not allowed with the --dev-only flag.`
25+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You cannot deploy to ${input.environment}.`
2426
);
2527
}
2628

@@ -118,9 +120,10 @@ export const listDeploysTool = {
118120
handler: toolHandler(ListDeploysInput.shape, async (input, { ctx }) => {
119121
ctx.logger?.log("calling list_deploys", { input });
120122

121-
if (ctx.options.devOnly) {
123+
if (!ctx.isEnvironmentAllowed(input.environment)) {
124+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
122125
return respondWithError(
123-
`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.`
126+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
124127
);
125128
}
126129

packages/cli-v3/src/mcp/tools/runs.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ export const getRunDetailsTool = {
1212
handler: toolHandler(GetRunDetailsInput.shape, async (input, { ctx }) => {
1313
ctx.logger?.log("calling get_run_details", { input });
1414

15-
if (ctx.options.devOnly && input.environment !== "dev") {
15+
if (!ctx.isEnvironmentAllowed(input.environment)) {
16+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
1617
return respondWithError(
17-
`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.`
18+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
1819
);
1920
}
2021

@@ -69,9 +70,10 @@ export const waitForRunToCompleteTool = {
6970
handler: toolHandler(CommonRunsInput.shape, async (input, { ctx, signal }) => {
7071
ctx.logger?.log("calling wait_for_run_to_complete", { input });
7172

72-
if (ctx.options.devOnly && input.environment !== "dev") {
73+
if (!ctx.isEnvironmentAllowed(input.environment)) {
74+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
7375
return respondWithError(
74-
`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.`
76+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
7577
);
7678
}
7779

@@ -122,9 +124,10 @@ export const cancelRunTool = {
122124
handler: toolHandler(CommonRunsInput.shape, async (input, { ctx }) => {
123125
ctx.logger?.log("calling cancel_run", { input });
124126

125-
if (ctx.options.devOnly && input.environment !== "dev") {
127+
if (!ctx.isEnvironmentAllowed(input.environment)) {
128+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
126129
return respondWithError(
127-
`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.`
130+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
128131
);
129132
}
130133

@@ -162,9 +165,10 @@ export const listRunsTool = {
162165
handler: toolHandler(ListRunsInput.shape, async (input, { ctx }) => {
163166
ctx.logger?.log("calling list_runs", { input });
164167

165-
if (ctx.options.devOnly && input.environment !== "dev") {
168+
if (!ctx.isEnvironmentAllowed(input.environment)) {
169+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
166170
return respondWithError(
167-
`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.`
171+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
168172
);
169173
}
170174

packages/cli-v3/src/mcp/tools/tasks.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ export const getCurrentWorker = {
1111
handler: toolHandler(CommonProjectsInput.shape, async (input, { ctx }) => {
1212
ctx.logger?.log("calling get_current_worker", { input });
1313

14-
if (ctx.options.devOnly && input.environment !== "dev") {
14+
if (!ctx.isEnvironmentAllowed(input.environment)) {
15+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
1516
return respondWithError(
16-
`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.`
17+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
1718
);
1819
}
1920

@@ -90,9 +91,10 @@ export const triggerTaskTool = {
9091
handler: toolHandler(TriggerTaskInput.shape, async (input, { ctx }) => {
9192
ctx.logger?.log("calling trigger_task", { input });
9293

93-
if (ctx.options.devOnly && input.environment !== "dev") {
94+
if (!ctx.isEnvironmentAllowed(input.environment)) {
95+
const allowedEnvs = ctx.options.envOnly?.join(", ") || "dev";
9496
return respondWithError(
95-
`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.`
97+
`This MCP server is restricted to the following environments: ${allowedEnvs}. You tried to access the ${input.environment} environment.`
9698
);
9799
}
98100

0 commit comments

Comments
 (0)