coder / coder/coder

feat: per-user custom_headers for MCP servers

Abierto
#25,921 0 comentarios 0 reacciones 0 asignados Ver en GitHub
Lenguaje dominante
Go
Estrellas
14.5k
Forks
1.5k
Merge medio
1 d 20 h
PR fusionados (30 d)
601

Descripción

## 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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.