Skip to content

cli: check the resolved deployment in the MCP production guard - #610

Open
felixlaga wants to merge 2 commits into
convex:mainfrom
felixlaga:mcp-guard-resolved-prod-deployments
Open

felixlaga wants to merge 2 commits into
convex:mainfrom
felixlaga:mcp-guard-resolved-prod-deployments

Conversation

@felixlaga

@felixlaga felixlaga commented Oct 16, 2026 •

Copy link
Copy Markdown

Fixes #544

Problem

The MCP production guard checks the deployment selector's kind, and only rejects kind: "prod". A selector can name the production deployment in other ways:

  • mcp start --deployment-name <prod deployment> gives kind: "deploymentName".
  • mcp start --deployment prod (or team:project:prod) gives kind: "deploymentSelector".
  • A production CONVEX_DEPLOY_KEY keeps kind: "unspecified".

None of these is caught. So run, envList, envGet, envSet, envRemove, data, logs and runOneoffQuery work against production without --dangerously-enable-production-deployments. status also reports readOnly: false for these selectors, which tells agents that every tool is allowed.

Fix

  • The guarded tools now check the deployment after it's resolved. Right after loadSelectedDeploymentCredentials, they call ctx.checkResolvedDeployment(credentials) or ctx.checkResolvedDeploymentReadOnly(credentials). The check is deploymentFields.deploymentType === "prod", the same test convex run uses.
  • The early kind === "prod" check in decodeDeploymentSelector stays. It rejects prod selectors before they're resolved, because resolving one can provision a production deployment.
  • status sets readOnly from the resolved deployment type instead of the selector's kind.
  • The two error messages are unchanged. Each one now lives in one place.

Self-hosted deployments (--url / --admin-key) have no deployment type, so they behave as before.

Behavior change

Starting the server with --deployment-name <prod deployment> and no production flag now gives the same errors as --prod. The docs already describe this behavior, but anyone who relied on the gap will notice.

This also matters for #545. Once deploy keys work with the MCP server, the guard will cover a production deploy key too.

Tests

There were no MCP tests, so I added two files:

  • requestContext.test.ts covers the new checks for each deployment type and flag combination.
  • tools/index.test.ts mocks deployment resolution. The mock resolves each selector to its own deployment and throws for any other selector.
    • Every guarded tool gets three selectors that resolve to production: by name, prod, and team:project:prod. Each tool must reject them before it reads the admin key.
    • Every guarded tool also gets two selectors that resolve to a dev deployment. Each tool must get past the guard for them.
    • It also checks the readOnly flag from status.

Without the fix, 26 of the 44 tool tests fail: all 24 production tool tests and 2 status tests. With the fix, all 63 tests pass. tsc, eslint and prettier --check pass on the changed files.

Notes

  • I didn't add a CHANGELOG entry, because the next version isn't open yet. A possible entry: "The MCP server's production guard now also applies when production is selected by name (--deployment-name), with --deployment prod, or with a production deploy key."
  • @superezzdev offered to take this issue on 2026-09-06. I couldn't find a PR, so I went ahead. I'm happy to close this one if they have work in progress.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

🤖 Generated with Claude Code

The MCP production guard only rejected deployment selectors of kind `prod`.
A selector that names the production deployment another way, like
`--deployment-name <name>`, `--deployment prod` or a production deploy key,
passed it. So `run`, the env tools, `data`, `logs` and `runOneoffQuery` worked
against production without `--dangerously-enable-production-deployments`, and
`status` reported `readOnly: false` for those selectors.

The guarded tools now also check the type of the deployment the selector
resolved to, and `status` marks deployments read-only by that type. Selectors
of kind `prod` are still rejected before they're resolved, since resolving one
can provision a production deployment.

Fixes convex#544

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

Re-trigger cubic

The mock of `loadSelectedDeploymentCredentials` returned production
credentials for any selection, so a tool that resolved the wrong selection
still passed. The mock now resolves each selection to its own deployment and
throws for any other.

The tests now also cover development deployments. Each guarded tool must get
past the guard for them, and `status` must not mark them read-only. Reading
the admin key of the mocked credentials throws, so no request leaves the
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: the production guard is bypassed by a selector that names production by name or reference

1 participant