Skip to content

🤖 feat: sync the CoderMCPServer config with the spec on every reconcile - #328

Merged
ThomasK33 merged 1 commit into
mainfrom
mcp-sync
Oct 14, 2026
Merged

ThomasK33 merged 1 commit into
mainfrom
mcp-sync

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

The dormant CoderMCPServer reconciler now reads its config in Coder on every reconcile and sets back each field that differs, with one sparse PATCH. It also clears credentials of other auth types on every apply (#314), and the access list sync runs on every reconcile, so a user grant that an admin added in the dashboard goes. The kind is not registered, so users see no change.

This is the first of three PRs for plan step M6:

  1. This PR: drift read, sparse PATCH, 🤖 CoderMCPServer: check that inactive credentials are cleared on every apply #314, user grant removal.
  2. The next PR (stacked on this branch): the remaining kill switches (dashboard disable, credential clear, auth type change), the user_oidc correction, and the in-memory hash that skips a resend after a metadata-only Secret change.
  3. The last PR (stacked on the second): the disable when a credential Secret withdraws consent (🤖 CoderMCPServer: disable the config in Coder when credential consent is withdrawn #311, plan gate G8).

Background

#307 applied the spec once per generation and then made no Coder call until the spec or a Secret changed. The spec.access field doc says that every sync removes user grants added in the dashboard, but with no call, nothing removed them. I chose to add the drift read that the plan puts in M6, instead of changing the doc. TestCoderMCPServerSyncRemovesUserGrant shows a dashboard grant removed with no spec change.

How a sync works

Every reconcile of an object with status.configID, after the checks of #274:

  1. GET the config by ID.
  2. Build the full PATCH of 🤖 feat: apply the CoderMCPServer spec and access list to the created config #307, then drop every field that Coder already holds. Scopes compare as a set of tokens.
  3. Keep a credential when the PATCH sets url, auth_type or an OAuth2 URL (plan 4.6 item 5), when its change token differs from status.credentials, or when Coder reports it missing. A dashboard URL change therefore goes back with the credential.
  4. Clear every credential of another auth type that Coder holds (api_key_value: "", custom_headers: {}, oauth2_client_secret: ""). Coder keeps them while the auth type stays.
  5. If the PATCH is not empty: the remove-only ACL pass, the PATCH, then the check of the answer. The answer check now also requires, on every apply, that Coder holds exactly the credentials of the spec's auth type (🤖 CoderMCPServer: check that inactive credentials are cleared on every apply #314).
  6. If the PATCH is empty, an assertion checks that Coder matches the full request.
  7. The full ACL sync, the change tokens, appliedGeneration, Ready.

Known gap until the next PR: a dashboard disable or credential clear is set back on the next reconcile (at the latest after the 10-minute resync). The plan's kill switches for those, which hold every write until a spec edit, are in the next PR. The kind is not registered, so no install can hit this.

Validation

New tests in internal/controller/codermcpserver_sync_test.go (envtest and the fake Coder, with the leak scan on every reconcile). Dashboard edits go through a second SDK client against the fake.

  • TestCoderMCPServerSyncDrift (plan 4.7 item 5, S13): for each dashboard edit, the next reconcile sends exactly one PATCH with this body, Coder then holds the applied state again, and the reconcile after it sends nothing:
  • TestCoderMCPServerSyncRemovesUserGrant: a user grant added in the dashboard goes with one ACL PATCH and no config PATCH.
  • TestCoderMCPServerSyncDeletedConfig: a dashboard delete after the apply gives DeletedExternally with no write. A spec edit creates the config again, enabled with the key.
  • TestCoderMCPServerApplyFailures gains "update answers another type's credential" (has_custom_headers: true on an api_key config gives CoderAnswerMismatch, 🤖 CoderMCPServer: check that inactive credentials are cleared on every apply #314). Its "config gone" case moves to TestCoderMCPServerSyncDeletedConfig.
  • Existing tests that expected "no MCP request" after an apply now expect "no write", because each reconcile does one GET.
  • Mutation checks (run on the stack head): each of these 9 changes makes a test fail:
    • string field drift ignored
    • boolean field drift ignored
    • the credential not sent with a URL change
    • another type's custom headers not cleared
    • another type's API key not cleared
    • the inactive has_* check reduced to the active type
    • scopes compared as a string, not a set
    • a 404 after the apply creates the config again
    • a 404 before the apply does not create it again
  • Local gates on this head: make verify-vendor, make build, make lint, GOFLAGS=-mod=vendor go tool govulncheck ./..., make test, make test-integration, go test -race ./internal/controller/, and make docs-check pass. make codegen, make manifests, make build-installer and make docs-reference leave no diff.

Size

411 lines added and 125 removed, all hand-written: 293 changed lines in codermcpserver_apply.go (94 of them remove the #307 apply and its answer check), 39 in the reconciler, 176 in the new sync tests, and 28 in existing tests.

Fixes #314
Refs #155


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $127.81

The reconciler now reads the config on every reconcile and sets back
any field that differs with one sparse PATCH. A PATCH that sets a URL or
the auth type carries every credential of the auth type. Credentials of
other auth types are cleared and checked on every apply (#314). The ACL
sync also runs every reconcile, so a user grant added in the dashboard
goes. A config deleted before its generation is applied is created again.

Refs #155

Change-Id: Iecb93e07d2fe2d13630dd026827c948afa0c6a73
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T05:37:00.598930Z 0507afe PR opened
🔒 Security Review ✅ Completed 2026-10-14T05:36:59.018036Z 0507afe Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0507afef89

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

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.

🤖 CoderMCPServer: check that inactive credentials are cleared on every apply

1 participant