MCP: the production guard is bypassed by a selector that names production by name or reference
Summary
The MCP server's production guard keys on the selector's kind being "prod". A selector that names the production deployment by name or by reference carries a different kind, so the guard does not fire and every mutating tool — run, envSet, envRemove — is permitted against production without --dangerously-enable-production-deployments.
Where
cli/lib/mcp/requestContext.js:
async decodeDeploymentSelector(encoded) {
const { projectDir, deployment } = decodeDeploymentSelector(encoded);
if (deployment.kind === "prod" && !this.options.dangerouslyEnableProductionDeployments) {
return await this.crash({ … "This tool cannot be used with production deployments." … });
}
return { projectDir, deployment };
}decodeDeploymentSelectorReadOnly has the same shape for the PII check.
The payload's deployment is whatever the caller encoded. parseDeploymentSelector (cli/lib/deploymentSelector.js) accepts several forms that all reach production:
{kind: "deploymentName", deploymentName: "<prod-deployment-name>"}{kind: "deploymentSelector", selector: "prod"}{kind: "deploymentSelector", selector: "prod:<name>"}{kind: "deploymentSelector", selector: "<team>:<project>:prod"}
None of them is kind: "prod", so none is caught.
A server started with --deployment-name <production-deployment> produces exactly the first form from its own status call, so this is reachable without hand-crafting anything.
Reproduction
npx convex mcp start --project-dir /path/to/project --deployment-name <prod-deployment-name>Call status. Observed on convex 1.43.0, with no production flags set:
kind=deploymentName decoded deployment.kind=deploymentName readOnly=falseTwo things there. The selector's kind is deploymentName, so decodeDeploymentSelector's kind === "prod" check cannot match it. And status itself reports readOnly: false for that entry — the same server reports readOnly: true when the selector's kind is prod. So the server is telling the client that full access, including the mutating tools, is available against production.
To be explicit about what we did and did not run: we confirmed the selector shape and the readOnly flag via status, which is read-only. We did not execute a mutating tool against our production deployment. The guard being bypassed is decodeDeploymentSelector, which is the only thing standing between that selector and run / envSet / envRemove.
Impact
The guard reads as the safety boundary for MCP-driven agents: default-deny on production, opened only by an explicit --dangerously-… flag. Since agents routinely pass back whatever selector status gave them, a server pinned by name silently has full write access to production while presenting as guarded.
Suggested fix
Resolve the selector to a deployment type before the guard runs, rather than pattern-matching kind, so the check covers every way production can be named. Failing that, extend the check to the deploymentName and deploymentSelector forms above.
Related: the unspecified selector resolving to the wrong deployment entirely, filed separately.
Versions: convex 1.43.0 and 1.45.0, macOS, cloud deployments.
Source: get-convex/convex-backend