feat: org-scope mcp server config schema - #27617
#27617feat: org-scope mcp server config schema#27617mafredri wants to merge 1 commit intomaincoder/coder:mainfrom mathias/codagt-711-mcp-config-org-schemacoder/coder:mathias/codagt-711-mcp-config-org-schemaCopy head branch name to clipboard
Conversation
5578534 to
8f4c66e
Compare
| @@ -0,0 +1,130 @@ | ||
| package chatd |
There was a problem hiding this comment.
This file should not exist, find the right placement for the code. Also, the naming of functions here is poor.
Be sure to architect this in a way that will reduce churn further down up stack.
| } | ||
|
|
||
| func (q *querier) GetMCPServerConfigBySlug(ctx context.Context, slug string) (database.MCPServerConfig, error) { | ||
| func (q *querier) GetMCPServerConfigBySlugAndOrganization(ctx context.Context, arg database.GetMCPServerConfigBySlugAndOrganizationParams) (database.MCPServerConfig, error) { |
There was a problem hiding this comment.
Wrong order, ByOrganizationAndSlug as configs are partitioned by org. Fix everywhere you've made the same mistake.
| logger.Warn(ctx, "failed to load MCP server configs", slog.Error(err)) | ||
| } | ||
| // Compare against unique requested IDs: a duplicated ID in the | ||
| // chat's list yields one config and is not an org-boundary skip. |
There was a problem hiding this comment.
Write clear technical English, yield is a misfit here and "an org-boundary skip" just sounds wrong.
| for _, id := range chat.MCPServerIDs { | ||
| uniqueRequested[id] = struct{}{} | ||
| } | ||
| if dropped := len(uniqueRequested) - len(mcpConfigs); err == nil && dropped > 0 { |
There was a problem hiding this comment.
Why does this even exist? How is it relevant in any way?
| //nolint:gocritic // Need to validate MCP server IDs exist. | ||
| existingConfigs, err := api.Database.GetMCPServerConfigsByIDs(dbauthz.AsSystemRestricted(ctx), req.MCPServerIDs) | ||
| //nolint:gocritic // Need to validate MCP server IDs exist and are usable by the chat's organization. | ||
| existingConfigs, err := chatd.MCPServerConfigsForChatOrgValidation(dbauthz.AsSystemRestricted(ctx), api.Database, req.OrganizationID, req.MCPServerIDs) |
There was a problem hiding this comment.
Yeah, this way of doing compatibility is a no-go. Think of a better way, less churn down the line.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f4c66ef24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if err != nil { | ||
| return nil, err | ||
| } | ||
| configs, err := db.GetEnabledMCPServerConfigsByOrganizations(ctx, organizationIDs) |
There was a problem hiding this comment.
Filter requested MCP config IDs in SQL
When a chat requests any MCP server, this query loads every enabled config in the chat and default organizations and only filters by ID afterward. During the fallback window, all existing and newly created configs belong to the default organization, so every generation now fetches and, with dbcrypt, decrypts the deployment's entire enabled MCP catalog instead of the requested rows as the previous GetMCPServerConfigsByIDs call did. This makes per-turn cost scale with the total catalog and lets a decryption error in an unrelated config suppress all requested MCP tools; include the requested IDs in the database predicate.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR introduces the schema and query groundwork for organization-scoped MCP server configurations while keeping current runtime behavior via a temporary “chat org OR default org” fallback window.
Changes:
- Adds
organization_idtomcp_server_configs, backfills all existing rows to the default org, and changes slug uniqueness toUNIQUE (organization_id, slug)(migration 000560). - Extends sqlc queries and generated code for org-aware MCP config lookups, plus dbcrypt/dbmetrics/dbauthz/dbmock updates to match.
- Updates chatd generation/subagent MCP config resolution and coderd handlers/tests to preserve pre-cutover behavior (including default-org fallback and disabled-config validation semantics).
Reviewed changes
Copilot reviewed 21 out of 27 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| enterprise/dbcrypt/dbcrypt.go | Adds dbcrypt wrappers for new org-aware MCP config queries. |
| enterprise/dbcrypt/dbcrypt_internal_test.go | Updates tests for slug+org lookup behavior and decryption expectations. |
| coderd/x/chatd/subagent.go | Uses org-aware MCP config resolution for Explore snapshot inheritance. |
| coderd/x/chatd/mcporgs.go | Adds fallback-window helpers for MCP config resolution across chat org + default org. |
| coderd/x/chatd/mcporgs_test.go | Adds unit tests covering fallback semantics, disabled filtering, and third-org exclusion. |
| coderd/x/chatd/generation_preparer.go | Loads MCP configs via fallback-window helper and adds dropped-config warning. |
| coderd/x/chatd/generation_preparer_internal_test.go | Adds regression test for duplicate MCP IDs not producing an unusable warning. |
| coderd/mcp.go | Ensures newly created MCP configs are assigned to the default organization. |
| coderd/mcp_test.go | Adds custom-role regression test to ensure create path works without org:read. |
| coderd/exp_chats.go | Validates MCP server IDs against chat org + default org during fallback window. |
| coderd/exp_chats_test.go | Adds handler-level regression tests for fallback acceptance and third-org rejection. |
| coderd/database/unique_constraint.go | Updates unique-constraint constants for new composite uniqueness. |
| coderd/database/queries/mcpserverconfigs.sql | Adds org-aware MCP server config queries and org-aware inserts. |
| coderd/database/queries.sql.go | Regenerates sqlc output for MCP config org column and new queries. |
| coderd/database/querier.go | Extends sqlcQuerier interface with org-aware MCP config methods. |
| coderd/database/models.go | Adds OrganizationID to database.MCPServerConfig. |
| coderd/database/migrations/testdata/fixtures/000560_mcp_server_configs_organization_id.up.sql | Adds fixture data exercising the new FK column. |
| coderd/database/migrations/migrate_test.go | Adds Stepper migration test validating backfill and constraint swap. |
| coderd/database/migrations/000560_mcp_server_configs_organization_id.up.sql | Migration adding/backfilling org column and swapping constraints/indexes. |
| coderd/database/migrations/000560_mcp_server_configs_organization_id.down.sql | Down migration restoring deployment-wide slug uniqueness and dropping org column. |
| coderd/database/foreign_key_constraint.go | Updates FK-constraint constants for new org FK. |
| coderd/database/dump.sql | Updates schema dump to include org column, new constraints, and index. |
| coderd/database/dbmock/dbmock.go | Extends mock Store with new org-aware MCP config methods. |
| coderd/database/dbmetrics/querymetrics.go | Adds query metrics wrappers for new org-aware MCP config methods. |
| coderd/database/dbgen/dbgen.go | Seeds MCP server configs into the default org when org not provided. |
| coderd/database/dbauthz/dbauthz.go | Adds org read to chatd subject for fallback default-org resolution and authorizes new methods. |
| coderd/database/dbauthz/dbauthz_test.go | Updates MethodTestSuite and AsChatd assertions for new methods/permissions. |
Files not reviewed (6)
- coderd/database/dbmetrics/querymetrics.go: Generated file
- coderd/database/dbmock/dbmock.go: Generated file
- coderd/database/foreign_key_constraint.go: Generated file
- coderd/database/models.go: Generated file
- coderd/database/querier.go: Generated file
- coderd/database/unique_constraint.go: Generated file
Comments suppressed due to low confidence (2)
coderd/exp_chats.go:3352
- The MCP server ID validation compares len(existingConfigs) to len(*req.MCPServerIDs). If the request contains duplicate IDs, existingConfigs will be de-duplicated by the SQL query, and this path returns a 400 with an empty "Invalid IDs" detail. Consider validating based on the computed missing list instead of comparing lengths so duplicates do not cause false negatives.
existingConfigs, err := chatd.MCPServerConfigsForChatOrgValidation(dbauthz.AsSystemRestricted(ctx), api.Database, chat.OrganizationID, *req.MCPServerIDs)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate MCP server IDs.",
Detail: err.Error(),
coderd/exp_chats.go:1325
- The MCP server ID validation compares len(existingConfigs) to len(req.MCPServerIDs). If the request contains duplicate IDs, existingConfigs will be de-duplicated by the SQL query, and this path returns a 400 with an empty "Invalid IDs" detail. Consider validating based on the computed missing list instead of comparing lengths so duplicates do not cause false negatives.
This issue also appears on line 3348 of the same file.
existingConfigs, err := chatd.MCPServerConfigsForChatOrgValidation(dbauthz.AsSystemRestricted(ctx), api.Database, req.OrganizationID, req.MCPServerIDs)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate MCP server IDs.",
Detail: err.Error(),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if dropped := len(uniqueRequested) - len(mcpConfigs); err == nil && dropped > 0 { | ||
| logger.Warn(ctx, "skipped MCP server configs not usable by the chat organization", | ||
| slog.F("requested", len(uniqueRequested)), | ||
| slog.F("usable", len(mcpConfigs))) | ||
| } |
| eligibleConfigs, err := mcpServerConfigsByChatOrg(ctx, db, organizationID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // Iterate the enabled set (display_name ordered), not the requested ID | ||
| // slice, so a duplicated ID in chats.mcp_server_ids yields one config | ||
| // per unique ID, as the SQL query did before org-scoping. | ||
| configs := make([]database.MCPServerConfig, 0, len(eligibleConfigs)) | ||
| for _, cfg := range eligibleConfigs { | ||
| if !slices.Contains(ids, cfg.ID) { | ||
| continue | ||
| } | ||
| configs = append(configs, cfg) | ||
| } | ||
| return configs, nil |
8f4c66e to
d13cc89
Compare
Add organization_id to mcp_server_configs, backfilling every existing row to the default organization and making slug uniqueness per-organization, as the schema stage of CODAGT-711. Runtime behavior is preserved during the stage window: every path that fetches an MCP server config for a chat (generation preparation, Explore subagent snapshots, and the chat create/update request validation) accepts configs from the chat's organization or the default organization, marked for removal at the B3 org-scoping cutover. New configs are created in the default organization, with the organization resolved under a call-scoped chatd subject so callers holding only deployment_config permissions are unaffected. - Migration 000558: nullable add, backfill to default org (aborting loudly when none exists), SET NOT NULL, replace mcp_server_configs_slug_key with UNIQUE (organization_id, slug), org index; the down migration restores the deployment-wide slug constraint. - GetMCPServerConfigBySlug becomes GetMCPServerConfigBySlugAndOrganization; enabled/forced queries gain ByOrganization variants; the insert gains the org column; unfiltered list queries stay for the old handlers until B3. - dbgen.MCPServerConfig seeds the default org; subjectChatd gains site-level organization read for the fallback resolution. - Fixture + Stepper test covering the backfill, secret-pair preservation, per-org slug uniqueness, and the down migration.
d13cc89 to
a64123b
Compare
Summary
Schema stage of CODAGT-711 (org-scoped MCP server configs), stacked as the root of the B-track.
mcp_server_configsgainsorganization_id(FKorganizations,ON DELETE CASCADE), backfilled to the default organization; slug uniqueness moves from deployment-wide toUNIQUE (organization_id, slug). Runtime behavior is preserved during the schema-stage window: every path that fetches an MCP config for a chat accepts configs from the chat's organization OR the default organization (fallback removed at the B3 org-scoping cutover). New configs are created in the default organization.Changes
SET NOT NULL, dropmcp_server_configs_slug_key, addUNIQUE (organization_id, slug), org index. Down restores the deployment-wide slug constraint and drops the column. Secret ciphertext and*_key_idpairs are untouched (Stepper-verified byte-identical).GetMCPServerConfigBySlugbecomesGetMCPServerConfigBySlugAndOrganization(org-first params); insert gains org;GetEnabledMCPServerConfigsByOrganization(s)andGetForcedMCPServerConfigsByOrganizationadded; unfilteredGetMCPServerConfigs/GetEnabledMCPServerConfigsretained for old handlers (deleted in B3).generation_preparer.goandsubagent.gofetch chat-org OR default-org enabled configs viachatd.MCPServerConfigsForChatOrg(matches pre-org-scoping behavior, where the MCP client skipped disabled configs at connect time). Chat create + message MCP-ID update (exp_chats.go) validate existence within chat-org OR default-org without the enabled filter viachatd.MCPServerConfigsForChatOrgValidation(backed by the newGetMCPServerConfigsByIDsAndOrganizationsquery), so a chat can keep or attach a disabled config exactly as before; third-org configs stay rejected. The generation helper reconstructs results from the SQL-ordered enabled set, so a duplicated ID inchats.mcp_server_ids(legacy/hostile rows only; the column has no uniqueness constraint) yields one config per unique ID in pre-org-scoping display_name order, not duplicate MCP connections. Both markedTODO(mafredri): remove after CODAGT-711 B3 (org-scoping cutover).dbauthz.AsChatdprobe with a nolint justification (callers may hold onlydeployment_configpermissions; post-gate permission discipline).ResourceDeploymentConfig(RBAC swap is B2);subjectChatdgains site-levelResourceOrganizationread for the fallback's default-org resolution. MethodTestSuite entries updated.Window-limited error-path note
The fallback helpers resolve the default organization on every chat MCP lookup (
GetDefaultOrganization), a second transient-failure surface that did not exist pre-org-scoping. InprepareGenerationthe fetch error is logged and swallowed (pre-existing behavior for config-load failures), so a transient default-org failure strips all MCP tools for that turn rather than just failing the lookup. I assessed caching the default org ID; the clean fix is non-trivial and the exposure is window-limited: B3's strict org scoping removes the default-org lookup entirely. Documented atmcpServerConfigOrganizationIDsForChatOrg. No action beyond this note.Declared overlaps with unmerged M1 (#27547)
Two, per the parallel-track operator ruling. Both resolve as trivial dedupes on rebase after M1 merges:
subjectChatdsite-levelResourceOrganizationread: M1 adds the identical grant (its model-config fallback resolves the default org under the chatd subject for the same reason). The B-track fallback window needs the same capability before M1 lands; the duplicate will collapse on rebase.000555unmerged. This PR takes000560, free onorigin/mainat push time (000558 went toaudit_oauth2_provider_settings, 000559 tochat_messages_last_by_role_indexbetween rounds; renumbered viafix_migration_numbers.sh). When M1 merges, this branch re-checks and renumbers viafix_migration_numbers.sh+make genif needed.Verification
TestMigration000560MCPServerConfigsOrganizationID(Stepper): backfill to default org, secret-pair preservation, NOT NULL, org index, constraint swap, cross-org slug reuse allowed, same-org duplicate rejected (pq constraint asserted), lossless down.TestMigrate/TestMigrateUpWithFixtures: fixture (config + user token + chat referencing it) applies and all migrations up/down cleanly.TestMCPServerConfigsForChatOrg(6 subtests): chat-org + default-org acceptance, third-org exclusion, disabled-config exclusion, empty-input, OR-semantics (chat-org config does not hide default-org configs).TestPostChats/MCPServerIDsDefaultOrgFallback,/MCPServerIDsThirdOrgRejected, and/MCPServerIDsDisabledConfigAccepted(handler level; the last covers chat create AND message update with a disabled default-org config).TestCreateMCPServerConfigCustomRole: persisted site custom role, non-default-org member.MCPServerIDsThirdOrgRejected; handler reverted to the enabled-filtered helper failsMCPServerIDsDisabledConfigAccepted(with third-org rejection still green); default-org probe under the caller's context fails the custom-role test with HTTP 500.coderd/x/chatd(full package),coderdTestMCP*+TestPostChats,dbauthz(MethodTestSuite + TestAsChatd),enterprise/dbcrypt, race detector on both new suites.make fmt,make lint,make genidempotent (tree stays clean), pre-commit hooks green.Plan excerpt (B1 entry from the CODAGT-74 implementation plan)
B1
feat(coderd/database): org-scope mcp server config schema(base: main, after M1 merges)mcp_server_configs_slug_key, addUNIQUE (organization_id, slug); org index. NO ACL columns (deferred to B4 by ticket decision).GetMCPServerConfigBySlughas no non-generated callers: drop it (or convert to (org, slug) if tests need it); insert gains org; enabled/forced queries gain org variants; unfiltered list retained for old handlers (deleted in B3). dbcrypt wrappers pick up params via regeneration; any new decrypting read wrapper added alongside its query.generation_preparer.goandsubagent.govalidate fetched configs againstchat.OrganizationIDOR the default org (fallback window; strict in B3).dbgen.MCPServerConfigseeds org. Fixture + Stepper backfill test (incl. slug uniqueness per org assertions).Per-PR CI obligations for M1/B1: migration fixture + Stepper backfill test; sqlc-vet; MethodTestSuite arg updates; dbgen org seeding.
Stack
feat(coderd): org-scope mcp server config schema), root of the B-track stack. B2 (feat(coderd/rbac): mcp server config resource) will stack on this branch.🤖 This PR was generated by Coder Agents (B1 implementer of CODAGT-74, dispatched by the conductor). Plan, surveys, and worklog live in the durable bundle; the unit's progress file is
logs/b1-progress.md.