Skip to content

🤖 feat: activate the CoderMCPServer controller - #381

Merged
ThomasK33 merged 17 commits into
mainfrom
mcp-activate
Oct 14, 2026
Merged

ThomasK33 merged 17 commits into
mainfrom
mcp-activate

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

The CoderMCPServer controller now runs. A CoderMCPServer creates and manages one MCP server config in a Coder organization, which Coder chats offer to the model. Before this PR the kind was dormant: its controller was not registered and its CRD was not installed.

This is plan step M9 of #155. Steps M1 to M8 landed in earlier PRs, the last ones as #368 (delete path), #371 (E2E stub), #376 (test S9) and #377 (the opt-in /status admission policy).

What changes

  1. internal/app/controllerapp/controllerapp.go registers CoderMCPServerReconciler.
  2. The CRD moves from config/crd/dormant/ to config/crd/bases/, into config/default/kustomization.yaml and dist/install.yaml. hack/update-manifests.sh drops it from DORMANT_CRDS, and the CI dormancy guard step for the kind goes.
  3. RBAC markers: get, list, watch, patch on codermcpservers, get and update on codermcpservers/status, update on codermcpservers/finalizers. The secrets rule does not change (test S9, 🤖 test: pin the Secret RBAC rules and the standalone API server RBAC #376, passes).
  4. A sample with its credential Secret: config/samples/coder_v1alpha1_codermcpserver.yaml.
  5. The how-to docs/how-to/manage-mcp-servers.md, plus the nav, the index, the architecture page, the controller deploy page and the getting-started cleanup.
  6. The API reference page docs/reference/api/codermcpserver.md. The "no effect yet" sentences leave the spec.mcpServers field docs of CoderControlPlane and the CoderMCPServer types.
  7. The E2E path filter gains internal/controller/codermcpserver*.

What the how-to documents

  • The control plane opt-in (spec.mcpServers.organizations, allowForceOn): organization names resolve to IDs, and a withdrawn opt-in disables each affected server with one enabled: false update and no credential read. The object then reports DisabledExternally until a spec edit.
  • The credential Secret contract (label, destinations annotation, value rules), and that Coder sends the credential again after a redirect.
  • Access: spec.access lists groups only, so every sync removes user grants added in the dashboard. With access omitted, Coder's default lets the organization's Everyone group read the server.
  • The 🤖 CoderMCPServer: decide whether an unresolved group blocks removing old access #313 fail-closed behavior on a missing group: remove unnamed entries, add nothing, send enabled: false if the generation disables the server, and add the groups once they exist.
  • The CoderMCPServer: test the opt-in disable after a crash between marker write and PATCH, and document the window (before M9) #365 disable window and its known limit.
  • Rotation, URL changes (and their order), dashboard kill switches, the credential kill switch, the emergency path, and deletion (from 🤖 feat: delete the Coder MCP server config of a deleted CoderMCPServer #368).
  • The Coder-side limits (plan 5.5): access checked only at chat selection, tool lists by exact name, OAuth2 tokens deleted on a URL change, encryption at rest only with enterprise keys, no address guard.
  • RBAC guidance: what each permission means in Coder, a viewer Role, an editor Role, the wildcard warning, a ResourceQuota note.
  • config/admission/: opt-in (the install bundle does not apply it), needs ValidatingAdmissionPolicy (GA in Kubernetes 1.30), and hard-codes the ServiceAccount system:serviceaccount:coder-system:coder-k8s. Edit it when the operator runs as another ServiceAccount.

Dogfood

A full run of plan section 8 on a private Kind cluster (kind v0.26.0, kindest/node v1.32.0, Coder v2.37.2, no license), with images built from 091c7fcf. That commit has the same controller code as this head. Later commits changed only docs, the CI path filter, and moved the GitOps health rules out. Every step passed:

Step Result
Opt-in, Secret, apply Ready 233 ms after apply. Coder holds the slug, URL, enabled, has_api_key, and an ACL of Everyone read only. The key appears 0 times in the CR JSON and the operator logs.
Dashboard and chat The server shows in AI settings > MCP servers. A chat that selects it gets the stub's answer. The stub got a matching key hash, and the LLM got e2e-stub__alpha, e2e-stub__beta and e2e-stub__Gamma.
Rotation Coder got the new key 99 ms after the Secret change, and the next turn showed the new hash. An annotation-only change left updated_at unchanged.
Refusals (P4 a to f) CredentialNotOptedIn, CredentialRefused, CredentialDestinationNotAllowed, SlugConflict (the other config unchanged), MCPServersNotAllowed, ForceOnNotAllowed. No Coder config for a, b, c, e, f.
Kill switch A dashboard disable gives DisabledExternally, and Coder stays disabled. A displayName edit enables it again.
URL change, both orders The chat reached only the intended Service, with a matching key hash.
Access groups: []: a member's chat gets 400. [Everyone]: 201.
Admission policy Status patches by an impersonated system:masters user and by the Kind admin are denied with the policy message. The controller still reconciles.
Delete, namespace delete Coder answers 404 after the delete. In a namespace deletion, a second CR releases with ControlPlaneGone (7 s), and the namespace goes (17 s).

For P4 (e), unlicensed Coder allows one organization, so the run listed [no-such-org] instead. The dogfood found one how-to mismatch (the reason after a restored opt-in), which this PR fixes. The screenshots and the recording are in the evidence comment below.

Validation

  • 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. actionlint passes.
  • TestSetupControllersStartsEveryController (S18) now also needs a CoderMCPServer to get its finalizer and ControlPlaneNotReady, which proves the registration. A mutation that removes the registration fails it.
  • A new case in TestCoderMCPServerControlPlaneAndOptIn: a listed name that resolves to the object's organization ID is allowed (IDs are compared, not names). A mutation to a name compare fails it.
  • bash ./hack/e2e-workspace-lifecycle_test.sh times out locally under host load, also on unchanged main. CI runs it.

Size

451 hand-written changed lines (300 of them docs) and 740 generated (dist/install.yaml, the CRDs, role.yaml, the kustomization, the reference pages).

Follow-ups

Refs #155


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

A deleted CoderMCPServer now deletes the config that it created and then
releases its finalizer (plan 4.10):

- With a config ID, one DELETE by ID. Coder's own 404 also releases. Any
  other answer keeps the finalizer and retries every 60 s.
- With only a create marker, the slug read decides: only the nonce
  proves that the config is this object's. A missing config waits until
  a late create cannot commit any more (2 minutes), and needs a site
  owner.
- A deleted control plane releases the finalizer without a Coder
  request. A control plane that is being deleted gets one DELETE attempt
  bounded at 10 s, then the finalizer goes.
- The scope gate never blocks the delete.
- The released object's credential hashes leave the reconciler's map.

Refs #155

Change-Id: Iaa3d096523bb12408656377958c20563409982f3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Refs #155

Change-Id: Ia6b1789a575744cc04e056fd4cfbd20e4ff6b057
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I44afe5ff6138f70fcb974f57384b23b445dc6f66
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ie5d1d6db06850b4571b0cfeb697b94e203e46cca
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Refs #155

Change-Id: I87e3d28b11cffb1a553df0ed73072ac3bf8b59a7
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Refs #155

Change-Id: If4f948b82f1d598d4b49b3f861379b6c87001937
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I9d69335cc1c02fe315c28a16a5516dd296fe2c78
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I84ac2f3c9478f30d0b95ca40e5f0d4f0f58788d4
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Refs #155

Change-Id: I4559e944c3ea6cf0e0d1506234ae964bd089cac8
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…w-to ships

Change-Id: Ieaabbcdd0d9d86fee7c40ae1c75a1d3be5c7c39c
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I683bf385bb5c085554d0142e79bdb010439542af
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ic5ecff3c40758ebbfa45e8ef225240737741e6f4
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I5b4346439f1f02c38cda613a32c45b37781acd5f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ib12d8baad8b551aa8ce2561db582b6f794d29673
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I5f46e212918e0211f4d7b7ce91c60e4787f94485
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I0aa9d5a3fbd27961db36dfb3c74f2353df242ef0
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I687b13b09e120b2382289b186e58a21944b7f7ff
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@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-14T18:05:59.209988Z 9930bc7 Manual request
🔒 Security Review ✅ Completed 2026-10-14T17:55:27.167602Z 9930bc7 Manual request

Security findings

Advisory findings (1)

ℹ️ 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.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@ThomasK33

ThomasK33 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

Dogfood evidence (plan section 8)

Images built from 091c7fcf9a0f8f75867545a8c2a53558b2715417, which has the same controller code as this PR head. Private Kind cluster coder-k8s-mcp-dogfood (kind v0.26.0, kindest/node v1.32.0), Coder v2.37.2, no license. For the operator image and the stub image, the running container image ID equals the built image ID. All keys were synthetic and never printed. Hash checks print only "match" or "no match".

Dashboard: the managed server (step 5)

AI settings, MCP servers list with the managed server, desktop width

The managed server's page, desktop width

AI settings, MCP servers list, 390 px width

The managed server's page, 390 px width

Chat with the server selected (step 6)

A new chat with the E2E Stub server selected in the MCP picker

The chat answer from the stub LLM, with the server selected

Kill switch: disabled in the dashboard (step 9)

The server disabled in the dashboard; the object then reports DisabledExternally

After the delete (step 13)

The MCP servers list after the CoderMCPServer delete: the server is gone

Recording of the whole run

Rendered from the asciinema cast with idle gaps capped at 3 s (4 min 48 s, the run took about 14 min).

run-small.mp4
Step results
Step Check Result Timing
Setup 1-5 cluster, CNPG, CCP, images, port-forward, token header 0600, stub (one pod, Services mcp-stub, mcp-stub-2), openai-compat provider + model PASS cluster 44 s, CCP Ready ~1 min
1 CCP opt-in organizations: [coder] PASS
2 Secret, no trailing newline, label + annotation PASS
3 apply CR (api_key, X-Api-Key, Everyone) -> Ready PASS 233 ms
4 / P1 Coder GET: slug, url, enabled, has_api_key; ACL Everyone read only; key count in CR JSON and operator logs = 0 PASS
5 dashboard list + server page at 1440 and 390 px PASS
6 / P2 dashboard chat with server selected, "stub answer"; key hash match; LLM got e2e-stub__alpha, __beta, __Gamma PASS
7 / P3 rotation: Coder write after Secret change, next turn new hash match, old no match; annotation-only change keeps updated_at; both keys count 0 in logs and CR PASS Coder write 99 ms after the patch
8 / P4 a unlabeled Secret -> CredentialNotOptedIn PASS <1 s
8 / P4 b operator token Secret -> CredentialRefused PASS <1 s
8 / P4 b extra same Secret with label + annotation INCONCLUSIVE (CCP controller removes the label within seconds; reason stayed CredentialRefused)
8 / P4 c url other.invalid on the P1 Secret -> CredentialDestinationNotAllowed PASS <1 s
8 / P4 d slug e2e-taken created via API -> SlugConflict; its updated_at unchanged; deleting ref-d keeps it PASS <1 s
8 / P4 e org not listed (CCP list set to [no-such-org], second org is Premium-gated: 403) -> MCPServersNotAllowed, no config PASS <1 s
8 / P4 f force_on without allowForceOn -> ForceOnNotAllowed PASS <1 s
8 no Coder config for a, b, c, e, f; e2e-stub unchanged PASS
How-to opt-in withdrawal disables the existing server (enabled=false, key kept); stays disabled after restore; spec edit re-enables PASS (see mismatch 1)
9 / P7 dashboard disable -> after CR annotation DisabledExternally, Coder stays enabled=false; displayName edit -> enabled again PASS annotation -> reason 117 ms; edit -> Ready 230 ms
10 / P5 order 1 url first: CredentialDestinationNotAllowed, Coder URL and updated_at unchanged, still enabled; then origin -> URL mcp-stub-2, chat reached mcp-stub-2 only, key match PASS origin -> Ready 199 ms
10 / P5 order 2 origin first (no write), then url -> Coder URL mcp-stub, chat reached mcp-stub only, key match; old origin removed, still Ready PASS url -> Ready 432 ms
11 / P6 groups [] -> ACL empty, member chat 400 "One or more MCP server IDs are invalid or disabled."; [Everyone] -> 201 PASS
Admission config/admission applied; status patch as impersonated system:masters user and as kind admin denied with the policy message; configID unchanged; spec edit -> Ready; policy deleted PASS policy active ~2 s
13 / P9 a delete CR -> Coder GET 404; dashboard list without the server PASS 167 ms
P9 b namespace deletion with CR keeper alive: finalizer released (ControlPlaneGone, log has slug + configID), namespace gone PASS keeper gone +7 s, namespace +17 s

Step 12 and P8 (tool lists) are M11 only and were skipped.

@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 Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 9930bc719b

ℹ️ 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.

Comment thread internal/app/controllerapp/controllerapp.go
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 9930bc719b

ℹ️ 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".

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 5cfffaa Oct 14, 2026
13 checks passed
@ThomasK33
ThomasK33 deleted the mcp-activate branch October 14, 2026 18:25
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.

1 participant