Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat: org-scope mcp server config schema - #27617

#27617
Draft
mafredri wants to merge 1 commit into
maincoder/coder:mainfrom
mathias/codagt-711-mcp-config-org-schemacoder/coder:mathias/codagt-711-mcp-config-org-schemaCopy head branch name to clipboard
Draft

feat: org-scope mcp server config schema#27617
mafredri wants to merge 1 commit into
maincoder/coder:mainfrom
mathias/codagt-711-mcp-config-org-schemacoder/coder:mathias/codagt-711-mcp-config-org-schemaCopy head branch name to clipboard

Conversation

@mafredri

@mafredri mafredri commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

Schema stage of CODAGT-711 (org-scoped MCP server configs), stacked as the root of the B-track.

mcp_server_configs gains organization_id (FK organizations, ON DELETE CASCADE), backfilled to the default organization; slug uniqueness moves from deployment-wide to UNIQUE (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

  • Migration 000560 (000467 recipe): nullable add + FK, backfill all rows to the default org (aborts loudly if none exists), SET NOT NULL, drop mcp_server_configs_slug_key, add UNIQUE (organization_id, slug), org index. Down restores the deployment-wide slug constraint and drops the column. Secret ciphertext and *_key_id pairs are untouched (Stepper-verified byte-identical).
  • Queries: GetMCPServerConfigBySlug becomes GetMCPServerConfigBySlugAndOrganization (org-first params); insert gains org; GetEnabledMCPServerConfigsByOrganization(s) and GetForcedMCPServerConfigsByOrganization added; unfiltered GetMCPServerConfigs/GetEnabledMCPServerConfigs retained for old handlers (deleted in B3).
  • Runtime: generation_preparer.go and subagent.go fetch chat-org OR default-org enabled configs via chatd.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 via chatd.MCPServerConfigsForChatOrgValidation (backed by the new GetMCPServerConfigsByIDsAndOrganizations query), 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 in chats.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 marked TODO(mafredri): remove after CODAGT-711 B3 (org-scoping cutover).
  • Create handlers assign the default org, resolved under a call-scoped dbauthz.AsChatd probe with a nolint justification (callers may hold only deployment_config permissions; post-gate permission discipline).
  • dbauthz: checks stay on ResourceDeploymentConfig (RBAC swap is B2); subjectChatd gains site-level ResourceOrganization read for the fallback's default-org resolution. MethodTestSuite entries updated.
  • dbcrypt decrypting wrappers for the new org queries.
  • dbgen.MCPServerConfig seeds the default org.
  • Tests: fixture + Stepper backfill test (backfill, secret-pair preservation, per-org slug uniqueness both directions, lossless down); fallback unit tests; handler regression tests (non-default-org chat using a default-org config succeeds; third-org config rejected); persisted site custom role (deployment_config read+update only, non-default-org member) proves the create path stays green. All regression tests are mutation-proven.

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. In prepareGeneration the 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 at mcpServerConfigOrganizationIDsForChatOrg. 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:

  1. subjectChatd site-level ResourceOrganization read: 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.
  2. Migration numbering: M1 carries 000555 unmerged. This PR takes 000560, free on origin/main at push time (000558 went to audit_oauth2_provider_settings, 000559 to chat_messages_last_by_role_index between rounds; renumbered via fix_migration_numbers.sh). When M1 merges, this branch re-checks and renumbers via fix_migration_numbers.sh + make gen if 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.
  • Mutation proofs (each reverted after): strict chat-org-only lookup fails exactly the 3 fallback subtests; handler reverted to the unfiltered existence check fails MCPServerIDsThirdOrgRejected; handler reverted to the enabled-filtered helper fails MCPServerIDsDisabledConfigAccepted (with third-org rejection still green); default-org probe under the caller's context fails the custom-role test with HTTP 500.
  • Suites: coderd/x/chatd (full package), coderd TestMCP* + TestPostChats, dbauthz (MethodTestSuite + TestAsChatd), enterprise/dbcrypt, race detector on both new suites.
  • make fmt, make lint, make gen idempotent (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)

  • Migration: org column backfilled to default org (000467 recipe); drop mcp_server_configs_slug_key, add UNIQUE (organization_id, slug); org index. NO ACL columns (deferred to B4 by ticket decision).
  • Queries: GetMCPServerConfigBySlug has 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.
  • Runtime: generation_preparer.go and subagent.go validate fetched configs against chat.OrganizationID OR the default org (fallback window; strict in B3).
  • dbgen.MCPServerConfig seeds 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

  • This PR (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.

@linear-code

linear-code Bot commented Jul 28, 2026

Copy link
Copy Markdown

CODAGT-711

@mafredri mafredri changed the title feat(coderd): org-scope mcp server config schema feat: org-scope mcp server config schema Jul 28, 2026
@mafredri
mafredri force-pushed the mathias/codagt-711-mcp-config-org-schema branch 4 times, most recently from 5578534 to 8f4c66e Compare July 29, 2026 07:56
Comment thread coderd/x/chatd/mcporgs.go Outdated
@@ -0,0 +1,130 @@
package chatd

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread coderd/database/dbauthz/dbauthz.go Outdated
}

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) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Wrong order, ByOrganizationAndSlug as configs are partitioned by org. Fix everywhere you've made the same mistake.

Comment thread coderd/x/chatd/generation_preparer.go Outdated
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Write clear technical English, yield is a misfit here and "an org-boundary skip" just sounds wrong.

Comment thread coderd/x/chatd/generation_preparer.go Outdated
for _, id := range chat.MCPServerIDs {
uniqueRequested[id] = struct{}{}
}
if dropped := len(uniqueRequested) - len(mcpConfigs); err == nil && dropped > 0 {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why does this even exist? How is it relevant in any way?

Comment thread coderd/exp_chats.go Outdated
//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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, this way of doing compatibility is a no-go. Think of a better way, less churn down the line.

@mafredri

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread coderd/x/chatd/mcporgs.go Outdated
if err != nil {
return nil, err
}
configs, err := db.GetEnabledMCPServerConfigsByOrganizations(ctx, organizationIDs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_id to mcp_server_configs, backfills all existing rows to the default org, and changes slug uniqueness to UNIQUE (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.

Comment thread coderd/x/chatd/generation_preparer.go Outdated
Comment on lines +74 to +78
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)))
}
Comment thread coderd/x/chatd/mcporgs.go Outdated
Comment on lines +37 to +51
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
@mafredri
mafredri force-pushed the mathias/codagt-711-mcp-config-org-schema branch from 8f4c66e to d13cc89 Compare July 29, 2026 09:31
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.
@mafredri
mafredri force-pushed the mathias/codagt-711-mcp-config-org-schema branch from d13cc89 to a64123b Compare July 29, 2026 10:29
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.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.