coder / coder/coder

feat: per-user custom_headers for MCP servers

Open
#25,921 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
14.5k
Forks
1.5k
Avg merge
1d 20h
Merged PRs (30d)
601

Description

## TL;DR

Today an admin who configures an MCP server with `custom_headers` auth has to put a single set of header values in the deployment that every caller shares. When the MCP server identifies callers by header (e.g. a long-lived per-user PAT or JWT), the only options are:

1. Give every user the admin's credential. (Loses per-user identity, defeats audit.)
2. Stand up one MCP server config per user. (Doesn't scale, and the admin can't even know all users in advance.)

This is the gap I want to close. The proposal: let an admin mark specific `custom_headers` keys as **user-set**, and let each user supply their own value via a settings page. Coder merges the admin static headers with the calling user's values at request time.

## Motivating use case

Most concrete examples we hit:

- An internal MCP server (Honcho, an internal Linear-like, a self-hosted code-search) that authenticates callers via `Authorization: Bearer `. The admin knows the header name and the auth shape but cannot know each user's token.
- A team-shared MCP server that wants a `X-User-Email` or `X-Trace-User` header for per-user attribution that the admin shouldn't have to maintain a mapping for.

The pattern in both: the admin owns the header **name** and the auth shape; the user owns the **value**.

## Proposed feature

### Admin experience

In the MCP server config form (when `auth_type=custom_headers`), admins can:

- Mark a subset of header keys as user-set. Those keys move out of the admin-set `CustomHeaders` map and into a new `CustomHeadersUserKeys` list. The two sets must be disjoint.
- Optionally attach a short description per user-set key, rendered above the corresponding input on the user's settings page. (E.g. `Authorization` → "Personal access token from your Honcho profile.")

The admin never sees per-user values. They only see "which keys are user-set" and "(optionally) what description I wrote for the user."

### User experience

A new page at `Agents > Settings > MCP Servers` lists every enabled MCP server that has user-set keys and lets the user:

- Fill in a value per user-set key. Empty string clears that single key.
- See the admin-authored description above each input.
- Use one button to clear all of their values for a server.

`MCPServerPicker` (in the chat composer) badges any enabled server that has user-set keys without stored values for the current user, with a deep link to the settings page. So the prompt to "go finish setting this up" shows up exactly where the user is about to use it.

### At request time

When the chat agent makes an outbound MCP request:

1. Coder loads the admin's static `CustomHeaders` for the server.
2. Coder loads the calling user's stored values for that server's `CustomHeadersUserKeys`.
3. The two maps are merged (user-set keys only override the slots the admin marked user-set; the user can't introduce arbitrary new headers) and sent on the request.

If a user has no stored values, admin headers still ship (with the user-keyed slots simply absent) and a warning is logged. No way for a missing user-header row to block an entire chat.

## Security model

- Per-user values are encrypted at rest with the same `dbcrypt` key set as the rest of the MCP server config (`oauth2_client_secret`, `api_key_value`, `custom_headers`).
- Per-user values are **never** returned to any caller (not even the user that stored them). The GET endpoint returns a `has_values: {key: bool}` map so the UI can render "value set / not set" without exposing the secret.
- The admin cannot read or list per-user values. The only ways for a value to leave the database are (a) sent to the configured MCP server URL on an outbound request, or (b) cleared by the owning user.
- `coder server dbcrypt rotate / decrypt / delete` covers the new tables, so key rotation and tear-down don't leave orphaned ciphertext.

## API shape (experimental)

Three new endpoints under the existing experimental MCP namespace:

```
GET /api/experimental/mcp/servers/{id}/user-headers
PUT /api/experimental/mcp/servers/{id}/user-headers
DELETE /api/experimental/mcp/servers/{id}/user-headers
```

GET returns `{mcp_server_config_id, has_values: {key: bool}}`. PUT accepts a partial `{values: {key: value}}` map (empty string clears one key, missing keys are left alone). DELETE clears all values for the calling user on that server. Keys are case-insensitive and validated against the server's declared `CustomHeadersUserKeys`.

The admin `CreateMCPServerConfig` / `UpdateMCPServerConfig` payloads grow `custom_headers_user_keys` and `custom_headers_user_key_descriptions`. Validation: disjoint from admin-set `CustomHeaders` (case-insensitive), unique among themselves, only allowed when `auth_type=custom_headers`. Orphan descriptions are dropped automatically when a key leaves the list.

## Implementation

Sliced into a 6-PR Graphite stack so each layer is reviewable in isolation. The original combined PR is #25823 (being superseded by this stack).

1. **#25913** — db foundation (migration `000514`, queries, dbgen, dbauthz, dbcrypt interceptors)
2. **#25914** — `coder server dbcrypt` rotate / decrypt / delete coverage for the three MCP tables
3. **#25915** — backend API + SDK (`coderd/mcp.go`, `codersdk/mcp.go`, regenerated TS types)
4. **#25916** — chatd runtime overlay (merges user values into outbound requests)
5. **#25917** — frontend (admin panel + `/agents/settings/mcp-servers` user page)
6. **#25918** — docs

The endpoints are marked experimental (`@x-apidocgen skip`); the feature is opt-in per server (only triggered when an admin marks at least one key as user-set), so existing `custom_headers` configurations are unaffected.

## Out of scope (for this stack)

- Group / role-scoped header overrides. The unit of ownership stays "individual user" for now.
- Auto-import of user values from an external identity provider. The user supplies values manually.
- Cross-server value sharing. Each value is keyed by `(user_id, mcp_server_config_id, header_key)`.

These can be revisited if real demand shows up; for the use cases above the simpler "user fills in a value per key" model is sufficient.

This issue was generated by Coder Agents on behalf of @Emyrk.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.