feat: add mcp per-user headers auth type with credential storage and submission flow#3703
Conversation
|
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (36)
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughThis PR implements per-user MCP headers authentication as an alternative to OAuth, featuring unified error types that discriminate between OAuth and headers flows, a full credential lifecycle with persistence/encryption, temporary verification endpoints for header discovery, and integrated session management. ChangesPer-User Headers MCP Authentication
🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Suggested Reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Comment |
bb1238f to
a605ec2
Compare
9a1e82a to
7b10bef
Compare
Security Policy Alert: Secret Policy ViolationThis workflow run has been blocked by StepSecurity's secrets policy because it accesses secrets and the workflow file differs from the default branch. To approve this workflow, please add the Note: The label must be added by someone other than the PR author (Pratham-Mishra04) or automation bots to ensure proper security review. After the label is added, you can re-run the blocked workflow to proceed. This workflow will be automatically approved once merged into the default branch. For more information, see StepSecurity's Secret Exfiltration Policy documentation. |
Confidence Score: 4/5The core auth flows are structurally sound but the dedicated revoke endpoint does not purge in-flight flow rows, leaving a short window where a credential can be re-created after explicit revocation. The credential-store resolver, storage layer, cascade deletes, and sweep worker mirror the established OAuth patterns and are correct. The dedicated revoke endpoint in mcp_per_user_headers.go still does not purge in-flight flow rows, and the sessions routing logic silently swallows DB lookup errors. transports/bifrost-http/handlers/mcp_per_user_headers.go (revoke endpoint) and transports/bifrost-http/handlers/mcp_sessions.go (silent DB-error swallowing) Important Files Changed
Reviews (12): Last reviewed commit: "feat: backend for per user headers mcp a..." | Re-trigger Greptile |
7b10bef to
1cd8fd3
Compare
a605ec2 to
1435f09
Compare
1435f09 to
0cc89eb
Compare
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
transports/bifrost-http/lib/config.go (1)
1433-1468:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize
PerUserHeaderKeysbefore hashing/persisting.This path feeds
GenerateMCPClientHash(...)with the rawPerUserHeaderKeysslice. Because header-key order is not semantically meaningful, a config-file reorder of the same keys will look like a schema change and can spuriously trigger MCP client updates / per-user credential re-submission.♻️ Proposed fix
func mcpClientConfigToTable(clientConfig *schemas.MCPClientConfig) (configstoreTables.TableMCPClient, error) { if clientConfig == nil { return configstoreTables.TableMCPClient{}, nil } if clientConfig.ToolSyncInterval%time.Second != 0 { return configstoreTables.TableMCPClient{}, fmt.Errorf( "tool_sync_interval must be a whole number of seconds, got %q", clientConfig.ToolSyncInterval.String(), ) } authType := string(clientConfig.AuthType) if authType == "" { authType = string(schemas.MCPAuthTypeHeaders) } + perUserHeaderKeys := slices.Clone(clientConfig.PerUserHeaderKeys) + slices.Sort(perUserHeaderKeys) + perUserHeaderKeys = slices.Compact(perUserHeaderKeys) + return configstoreTables.TableMCPClient{ ClientID: clientConfig.ID, Name: clientConfig.Name, IsCodeModeClient: clientConfig.IsCodeModeClient, ConnectionType: string(clientConfig.ConnectionType), ConnectionString: clientConfig.ConnectionString, @@ - PerUserHeaderKeys: clientConfig.PerUserHeaderKeys, + PerUserHeaderKeys: perUserHeaderKeys, ConfigHash: clientConfig.ConfigHash, }, nil }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@transports/bifrost-http/lib/config.go` around lines 1433 - 1468, The mcpClientConfigToTable path currently passes raw PerUserHeaderKeys into persistence and GenerateMCPClientHash causing spurious diffs when key order changes; fix by normalizing PerUserHeaderKeys (create a copy, deduplicate if necessary, then sort the slice) before assigning it to the TableMCPClient or before calling GenerateMCPClientHash so order is deterministic; update mcpClientConfigToTable to use the normalized slice for ConfigHash and the PerUserHeaderKeys field, referencing mcpClientConfigToTable, PerUserHeaderKeys and GenerateMCPClientHash to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@core/mcp/credstore/per_user_headers.go`:
- Around line 114-125: The function missingRequiredHeaderKeys currently does
exact map lookups which treat header names case-sensitively; change it to
compare header names case-insensitively by normalizing storedHeaders keys (e.g.,
build a new map with strings.ToLower(key) -> value) and compare each required
key using strings.ToLower(key) so "Authorization" and "authorization" match;
apply the same normalization approach to the other header helper in this file
that performs header existence/lookup logic so all header name checks are
case-insensitive.
In `@core/mcp/utils.go`:
- Around line 639-651: When config.AuthType == schemas.MCPAuthTypePerUserHeaders
also validate that the client uses an HTTP transport: reject configs whose
transport/type is SSE, STDIO, in-process or any non-HTTP scheme so they cannot
pass validation; add this check alongside the existing PerUserHeaderKeys and
OauthConfigID validations in the same validation block (where config.AuthType,
config.PerUserHeaderKeys and config.OauthConfigID are checked), and surface an
error message saying something like "per_user_headers requires an HTTP transport
(transport_type must be HTTP) for client '<name>'" so VerifyHeadersConnection
and transport.NewStreamableHTTP will only be invoked for valid HTTP clients.
In `@core/mcp/utils/utils.go`:
- Around line 28-44: The
BuildOAuthRedirectURIFromContext/BuildMCPCallbackBaseURL logic can produce a
double-slash when the stored base has a trailing slash; update
BuildMCPCallbackBaseURL to normalize the returned base by trimming any trailing
'/' (use strings.TrimRight or equivalent) so BuildOAuthRedirectURIFromContext
can safely concatenate "/api/oauth/callback" without producing "//"; locate the
functions BuildMCPCallbackBaseURL and BuildOAuthRedirectURIFromContext and apply
the trimming to the base before returning it.
In `@framework/configstore/rdb.go`:
- Around line 5383-5385: The MCPClient preload Select currently omits the
encryption_status column, causing TableMCPClient.AfterFind to be unable to
detect encrypted rows; update the Preload("MCPClient", func(db *gorm.DB)
*gorm.DB { ... }) Select to include "encryption_status" alongside "client_id,
name, headers_json, allowed_extra_headers_json, per_user_header_keys_json" so
AfterFind can properly trigger decryption logic for headers_json.
- Around line 1840-1846: When reconciling authoritative client config
(clientConfig.ConfigHash != ""), ensure auth-specific DB columns are cleared
when omitted in the payload: instead of only writing
updates["per_user_header_keys_json"] when clientConfig.PerUserHeaderKeys != nil,
detect the authoritative path (clientConfig.ConfigHash != "") and explicitly set
updates["per_user_header_keys_json"] to a NULL/empty sentinel when
PerUserHeaderKeys is nil so stale header schemas are removed; do the same for
the OAuth column by clearing updates["oauth_config_id"] when the payload does
not include an OAuth config (e.g., clientConfig.OAuthConfigId is nil/empty)
during authoritative sync. Ensure you reference the same updates map and keys
("per_user_header_keys_json", "oauth_config_id") so the DB update will
overwrite/clear prior values in the authoritative ConfigHash path.
In `@framework/mcp_headers/main.go`:
- Around line 84-91: Before converting the DB row into a credential, reject rows
that are not active by checking the row's needs_update and orphaned flags: in
the code path around the current return in main.go (before calling
rowToCredential), if row.NeedsUpdate is true return a specific error (e.g.
schemas.ErrHeadersCredentialNeedsUpdate or, if that error doesn't exist,
schemas.ErrHeadersCredentialNotFound), and if row.Orphaned is true return a
specific orphaned error (e.g. schemas.ErrHeadersCredentialOrphaned or
schemas.ErrHeadersCredentialNotFound); only call rowToCredential and return the
credential when both flags are false so drifted/orphaned rows are not treated as
usable.
In `@plugins/governance/main.go`:
- Around line 1734-1745: PreMCPConnectionHook currently only sets governance
team/customer context when vk.Team or vk.Customer relations are loaded; update
the block to fall back to foreign-key fields if relations are nil: when vk.Team
!= nil set Team ID/Name from vk.Team as before, else if vk.TeamID != nil set
schemas.BifrostContextKeyGovernanceTeamID from vk.TeamID (and skip name or set
empty); likewise for customer — if vk.Customer != nil use vk.Customer.ID/Name,
else if vk.CustomerID != nil set schemas.BifrostContextKeyGovernanceCustomerID
from vk.CustomerID; keep ctx.SetValue calls (and the same keys) and avoid
overwriting more specific relation values with FK fallbacks.
In `@transports/bifrost-http/handlers/mcp_per_user_headers.go`:
- Around line 214-226: The missing-header check and the filtering loop perform
exact map lookups so header names like "Authorization" vs "authorization" fail;
make header matching case-insensitive by normalizing request header names to a
single case (e.g. strings.ToLower) before lookups. Concretely: build a temporary
map (e.g. reqHeadersLower map[string]string) populated with lowercased keys from
req.Headers, call missingPerUserHeaderValues against config.PerUserHeaderKeys
after lowercasing those keys or change missingPerUserHeaderValues to lowercase
its incoming keys, and in the filtered population loop use the original config
key name as the map key but retrieve the value from
reqHeadersLower[strings.ToLower(key)]. Apply the same normalization in the
second occurrence referenced (lines ~337-344).
In `@transports/bifrost-http/handlers/mcp_sessions.go`:
- Around line 215-220: The header-credential and header-flow lookups (e.g.,
calls to h.store.ConfigStore.GetMCPPerUserHeaderCredentialByID and the similar
header-flow lookup used by revoke/reauth) are currently ignoring errors and
treating any error as a cache miss, allowing header-backed rows to fall through
to the OAuth path and return 404; change those call sites so that if the store
call returns a non-nil error you surface a 500 (do not fallthrough to the OAuth
path), logging the error and short-circuiting (return) before calling
reauthHeaderCredential or the OAuth handlers; update both the reauth path
(reauthHeaderCredential invocation) and the revoke flow (the similar lookup
block around the 355-399 range) to check and handle err != nil explicitly.
In `@transports/bifrost-http/handlers/mcp.go`:
- Around line 409-425: The validation lowercases header names only for duplicate
detection but leaves the original-cased PerUserHeaderKeys in place, causing
case-only edits (e.g. "Authorization" → "authorization") to be treated as schema
changes; update the handler to canonicalise the header keys permanently after
validation—use strings.ToLower(strings.TrimSpace(key)) (the same logic that
produces normalisedHeaders) to replace the incoming req.PerUserHeaderKeys (or
the stored representation used by perUserHeaderKeysChanged) so storage and
change-detection both use the canonicalised form; keep using lib.HasDuplicates
and missingPerUserHeaderValues for validation but persist normalisedHeaders for
comparison and storage to avoid spurious perUserHeaderKeysChanged flips.
- Around line 822-838: The validation currently allows an explicitly provided
empty slice for req.PerUserHeaderKeys which lets resolvePerUserHeaderKeys
persist an empty schema; update the validation in the request-handling path (the
block that examines req.PerUserHeaderKeys in mcp.go) to reject an explicitly
provided zero-length slice for per-user-headers clients by returning a
BadRequest (similar to how empty strings and duplicates are handled);
specifically, if req.PerUserHeaderKeys != nil and len(req.PerUserHeaderKeys) ==
0, call SendError with a clear message like "per_user_header_keys must contain
at least one entry" so resolvePerUserHeaderKeys cannot be given an empty list.
- Around line 1000-1009: The code flips per-user-header credential rows to
'needs_update' (MarkMCPPerUserHeaderCredentialsNeedsUpdate) before calling
h.mcpManager.UpdateMCPClient, which can leave credentials marked when the
runtime update later fails; move the call to
MarkMCPPerUserHeaderCredentialsNeedsUpdate so it runs only after
h.mcpManager.UpdateMCPClient returns success, and in the failure path ensure any
partial DB row rollback (the existing rollback logic around UpdateMCPClient)
does not leave credentials in 'needs_update' (i.e., revert the mark or skip
marking). Apply the same reorder/fix for the analogous block at the other
location (lines ~1013-1022) and reference existingConfig,
perUserHeaderKeysChanged, and h.store.ConfigStore when making the change.
---
Outside diff comments:
In `@transports/bifrost-http/lib/config.go`:
- Around line 1433-1468: The mcpClientConfigToTable path currently passes raw
PerUserHeaderKeys into persistence and GenerateMCPClientHash causing spurious
diffs when key order changes; fix by normalizing PerUserHeaderKeys (create a
copy, deduplicate if necessary, then sort the slice) before assigning it to the
TableMCPClient or before calling GenerateMCPClientHash so order is
deterministic; update mcpClientConfigToTable to use the normalized slice for
ConfigHash and the PerUserHeaderKeys field, referencing mcpClientConfigToTable,
PerUserHeaderKeys and GenerateMCPClientHash to locate the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f59f8454-557a-4b71-9528-9a6c4d445308
📒 Files selected for processing (35)
core/bifrost.gocore/mcp/agent.gocore/mcp/clientmanager.gocore/mcp/codemode/starlark/executecode.gocore/mcp/credstore/credstore.gocore/mcp/credstore/identity.gocore/mcp/credstore/per_user_headers.gocore/mcp/credstore/per_user_oauth.gocore/mcp/exec.gocore/mcp/interface.gocore/mcp/mcp.gocore/mcp/utils.gocore/mcp/utils/utils.gocore/schemas/bifrost.gocore/schemas/mcp.gocore/schemas/mcp_headers.goframework/configstore/migrations.goframework/configstore/rdb.goframework/configstore/store.goframework/configstore/tables/mcp.goframework/configstore/tables/mcp_per_user_headers.goframework/mcp_headers/main.goframework/mcp_headers/sweep.goframework/temptoken/scope.goplugins/governance/main.gotransports/bifrost-http/handlers/mcp.gotransports/bifrost-http/handlers/mcp_per_user_headers.gotransports/bifrost-http/handlers/mcp_sessions.gotransports/bifrost-http/handlers/mcpserver.gotransports/bifrost-http/handlers/temp_token_scopes.gotransports/bifrost-http/lib/config.gotransports/bifrost-http/lib/config_test.gotransports/bifrost-http/lib/ctx.gotransports/bifrost-http/lib/lib.gotransports/bifrost-http/server/server.go
0cc89eb to
cb69509
Compare
1cd8fd3 to
a0b1f5c
Compare
c7d9551 to
1a2dcbf
Compare
1a2dcbf to
c2f1282
Compare
c2f1282 to
e27f0c0
Compare
a0b1f5c to
0707fe5
Compare
7bb0272 to
d389c49
Compare
0707fe5 to
3559654
Compare
3559654 to
ebcb864
Compare
d389c49 to
dbe84c6
Compare
Merge activity
|
The base branch was changed.
dbe84c6 to
2c05f60
Compare
…submission flow (#3703) ## Summary Adds `MCPAuthTypePerUserHeaders` — a new per-user MCP authentication type where each caller submits their own API keys or signed-token header values (e.g. `Authorization`, `X-Api-Key`) rather than going through an OAuth dance. The admin declares the required header names (`per_user_header_keys`) at MCP client creation time; end users supply values on first tool use via an inline-401 submission flow that mirrors the existing per-user OAuth surface. ## Changes - **New `MCPAuthTypePerUserHeaders` auth type**: Added to `MCPAuthType` constants, `MCPClientConfig`, and all validation paths. Requires a non-empty `PerUserHeaderKeys` list; `oauth_config_id` must not be set. - **Unified `MCPAuthRequiredError`**: Replaced `MCPUserOAuthRequiredError` with `MCPAuthRequiredError` (with a `Kind` field: `"oauth"` or `"headers"`). `MCPUserOAuthRequiredError` is retained as a type alias for backward compatibility. - **`perUserHeadersResolver`**: New credstore resolver that looks up per-user header credentials by `(auth_mode, identity, mcp_client_id)`. On miss or stale schema, initiates a submission flow and returns an `MCPAuthRequiredError` with `Kind="headers"` and a `SubmitURL`. - **`MCPHeadersProvider` interface and `mcp_headers.Provider`**: Storage backend for per-user header credentials and pending submission flow rows. Mirrors `OAuth2Provider` structurally. Includes `InitiateUserSubmissionFlow` which mints a `mcp_headers_auth` temp token embedded in the auth-page URL fragment. - **`VerifyHeadersConnection`**: New method on `MCPManager` and `Bifrost` that opens a temporary MCP connection using caller-supplied header values, runs the Initialize handshake, and discovers tools. Used during admin MCP client creation (sample values) and user submission (validate before persisting). - **Database migrations**: Two new migrations — `add_mcp_per_user_header_credentials_table` (creates `mcp_per_user_header_credentials`, adds `per_user_header_keys_json` to `config_mcp_clients`, and creates partial unique indexes per auth mode) and `add_mcp_per_user_header_flows_table` (creates `mcp_per_user_header_flows`). - **`StaticConfigHeaders` updated**: Now strips any header whose name appears in `PerUserHeaderKeys` from the plugin-visible static header set, preventing admin-set static values from leaking through the connect-plugin gate for per-user-headers clients. - **`BifrostContextKeyMCPCallbackBaseURL`**: Replaces `BifrostContextKeyOAuthRedirectURI`. The base URL is now set once; OAuth resolver appends `/api/oauth/callback`, headers resolver appends the workspace submit path. - **`GovernancePlugin.PreMCPConnectionHook`**: New hook that resolves the caller's VK identity onto the `BifrostContext` before the credential-store resolver runs, so per-user auth types can key stored credentials by VK row ID. - **HTTP handlers**: - `POST /api/mcp/client` extended to handle `per_user_headers` auth type: validates `per_user_header_keys`, runs `VerifyHeadersConnection` with admin sample values, persists discovered tools, and discards the sample values. - New `MCPPerUserHeadersHandler` with routes: `GET /api/mcp/per-user-headers/flows/{id}` (flow detail), `PUT /api/mcp/per-user-headers/flows/{id}` (submit), `DELETE /api/mcp/per-user-headers/credential/{id}` (revoke). - `MCPSessionsHandler` extended to list, reauth, and revoke header credential and header flow rows alongside OAuth rows. Added `auth_kind` field to `mcpSessionRow` to disambiguate OAuth vs headers rows on the wire. - **`mcpHeadersAuthScope` temp token scope**: Grants `GET` and `PUT` access to the per-user-headers flow endpoints, bound to the flow ID. Registered at server startup alongside `mcpAuthScope`. - **`CredentialSweepWorker`**: Background worker that reaps orphaned credential rows (24h cadence, 30-day retention) and expired pending flow rows (15-min cadence). Started and stopped alongside the OAuth sweep worker. - **`identityForMCPAuthMode`** moved to its own file (`credstore/identity.go`) so both `perUserOAuthResolver` and `perUserHeadersResolver` share it without duplication. - **Cascade deletes**: `DeleteMCPClientConfig` and `DeleteVirtualKey` now also delete `mcp_per_user_header_credentials` and `mcp_per_user_header_flows` rows for the affected client/VK. - **`MarkMCPPerUserHeaderCredentialsNeedsUpdate`**: Called by `updateMCPClient` when `PerUserHeaderKeys` changes, flipping existing active credential rows to `needs_update` so callers are forced to resubmit on next tool use. ## Type of change - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./... ``` 1. Create an MCP client with `auth_type: per_user_headers` and `per_user_header_keys: ["Authorization"]`, supplying a sample `user_headers` map in the request body. Verify the client is created with discovered tools and the sample values are not persisted. 2. Make a tool call through the client without submitting credentials. Confirm the response contains `extra_fields.mcp_auth_required` with `kind: "headers"` and a `submit_url`. 3. Visit the `submit_url`, call `GET /api/mcp/per-user-headers/flows/{id}` to confirm the form schema, then `PUT` with valid header values. Confirm the credential row is created and the flow row is deleted. 4. Retry the tool call — it should succeed using the stored credential. 5. Call `DELETE /api/mcp/per-user-headers/credential/{id}` and confirm the credential and any pending flow rows are removed. 6. Update the MCP client's `per_user_header_keys` and confirm existing credential rows flip to `needs_update`, triggering the inline-401 on next tool use. 7. Delete the MCP client and confirm all associated credential and flow rows are removed. ## Breaking changes - [ ] Yes - [x] No `MCPUserOAuthRequiredError` is retained as a type alias for `MCPAuthRequiredError`, so existing callers that reference the old type continue to compile. The `BifrostContextKeyOAuthRedirectURI` context key is replaced by `BifrostContextKeyMCPCallbackBaseURL`; any code outside this repository that sets the old key directly will need to be updated. ## Security considerations - Admin-supplied sample header values used during MCP client creation are never persisted — they are used once for upstream verification and discarded. - Per-user header values are stored encrypted at rest using the same encryption key as OAuth tokens (`BIFROST_ENCRYPTION_KEY`). - `PerUserHeaderKeys` entries are stripped from the plugin-visible static header set so connect-plugin hooks cannot read or rewrite per-user credential values. - Temp tokens for the headers submission flow are embedded as URL fragments (`#t=<token>`), which are not sent to servers in `Referer` headers and do not appear in server access logs. - Flow rows are bound to a single `(mode, identity, mcp_client_id)` triple; the submit endpoint validates the caller's values against the upstream before persisting. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary This PR releases **core v1.5.14**, **framework v1.3.14**, **transports v1.5.6**, and bumps all dependent plugins to their respective `.14` patch versions. It delivers a broad set of new capabilities across MCP authentication, key rotation, OTel metrics, Bedrock/Anthropic compatibility, and UI improvements, alongside a number of targeted bug fixes and refactors. ## Changes - **Direct API Key Header** — Providers can now receive an API key passed directly via a request header (#3817) - **MCP Per-User Auth** — Introduced `MCPCredentialStore` abstraction, per-user MCP credential reconciliation, and a new per-user header auth type with lazy-auth submission flow (#3656, #3702, #3703, #3704, #3705) - **MCP TLS Configuration** — Added configurable TLS (`insecureSkipVerify`, `caCertPem`) for HTTP/SSE MCP client connections (#3779, #3783) - **MCP Sessions Management** — Filter, search, and pagination on the MCP sessions list API and table, plus a `can_reauth` identity gate (#3823, #3824, #3825) - **Key Rotation** — Keys now rotate on 401/402/403 responses; returns `502 upstream_credentials_exhausted` when all keys are permanently exhausted. Added `triggered_rotation` to `KeyAttemptRecord` and tightened `bifrost_key_rotation_events_total` semantics (#3430, #3491) - **OTel Metrics** — Added OTel spec-compatible metrics (backward compatible) with provider cache and semantic cache attributes in metrics export (#3865, #3816) - **Opus 4.8 Support** — System message handling and general compatibility for Opus 4.8 (#3868, #3878) - **Dimension Rankings** — New `GetDimensionRankings` API and dashboard tabs for team, customer, BU, and user rankings (#3766) - **Model Pricing Attributes** — `additional_attributes` field on model pricing rows with management API and UI editor (#3829) - **Prompt Cache Retention** — Added prompt cache retention parameter on responses requests (#3810) - **Tool Call Execution UI** — Inline tool-call execution, stop streaming, bulk execute/submit, and a redesigned tool-call UI (#3837, #3843) - **Sheet Navigation** — Prev/next keyboard navigation and URL state across virtual key, MCP client, and routing rule sheets (#3739, #3740, #3744, #3745) - **Bedrock Tool Name Truncation** — Truncate Bedrock function/tool names to the provider length limit - **Bedrock Guardrails** — Set guardrail config in Bedrock requests built from responses (#3862) - **Anthropic Tool Use** — Default `tool_use` input to `{}` when arguments are absent (#3880) - **Responses Streaming** — Fixed responses stream events (#3838) - **Compat Flow** — Fixed missing parameter parsing on the compat flow (#3881) - **Passthrough API Version** — Set a default API version in passthrough requests as a fallback (#3853) - **Virtual Key Updates** — Avoid overriding optional fields during virtual key update (#3855) - **User-Mode Flows** — Gate user-mode flows on caller `user_id`, skip temp token mint, and unify flow/credential kind filtering for pending flows (#3841, #3859) - **Partial Tool Calls** — Handle partial tool call execution failures and return successful results (#3849) - **URL Query Escaping** — Support escaped characters in URL query parameters (#3826) - **MCP Auth Errors** — Inline banner and retry support for MCP auth-required errors (#3856) - **Renamed Resolvers** — `staticHeadersResolver`/`serverOAuthResolver` renamed to `sharedHeadersResolver`/`sharedOAuthResolver` (#3840) - **Starlark Nested Tool Calls** — Exposed `RunWithPluginPipeline` on `ClientManager` and routed Starlark nested tool calls through the canonical plugin gate (#3794) - **Deferred-Fill OAuth Removed** — Removed deferred-fill user-mode OAuth flow support (#3839) - **Go 1.26.3** — Upgraded toolchain to Go 1.26.3 (#3782) ## Type of change - [x] Bug fix - [x] Feature - [x] Refactor - [ ] Documentation - [x] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [x] Providers/Integrations - [x] Plugins - [x] UI (React) - [ ] Docs ## How to test ```sh # Core/Transports go version # should report go1.26.3 go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` - Validate MCP per-user auth by configuring a per-user header auth type and confirming credentials are stored and reconciled on virtual key and MCP client changes. - Validate key rotation by triggering a 401/402/403 from an upstream provider and confirming rotation occurs; exhaust all keys and confirm a `502 upstream_credentials_exhausted` is returned. - Validate OTel metrics output includes `provider_cache` and `semantic_cache` attributes. - Validate Bedrock requests with tool names exceeding the provider limit are truncated correctly. - Validate Opus 4.8 system message handling by sending a request with a system message to an Opus 4.8 endpoint. ## Breaking changes - [x] Yes - [ ] No The deferred-fill user-mode OAuth flow has been removed (#3839). Any integrations relying on that flow must migrate to the new per-user credential store approach. The `staticHeadersResolver` and `serverOAuthResolver` identifiers have been renamed to `sharedHeadersResolver` and `sharedOAuthResolver` respectively (#3840); any direct references must be updated. ## Related issues #3817, #3656, #3702, #3703, #3704, #3705, #3779, #3783, #3823, #3824, #3825, #3430, #3491, #3865, #3816, #3868, #3878, #3766, #3829, #3810, #3837, #3843, #3739, #3740, #3744, #3745, #3862, #3880, #3838, #3881, #3853, #3855, #3841, #3859, #3849, #3826, #3856, #3840, #3794, #3839, #3782, #3724, #3814, #3836, #3869, #3886 ## Security considerations - MCP per-user credentials are stored via the new `MCPCredentialStore` abstraction; ensure the backing store is appropriately access-controlled and that credential values are encrypted at rest. - The direct API key header feature passes provider secrets via HTTP headers; ensure TLS is enforced on all ingress paths and that headers are not logged in plaintext. - User-mode flows are now gated on `caller user_id` and temp token minting is skipped where appropriate, reducing the surface for privilege escalation. - TLS configuration for MCP HTTP/SSE connections supports `insecureSkipVerify`; this should only be enabled in controlled environments. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## ✨ Features - **Direct API Key Header** - Pass a provider API key directly via request header (#3817) - **MCP Per-User Authentication** - New per-user header auth type with credential storage and lazy-auth submission flow (#3703, #3704, #3705) - **MCP TLS Configuration** - Configurable TLS (insecureSkipVerify, caCertPem) for HTTP/SSE MCP client connections (#3779, #3783) - **MCP Sessions Management** - Filter, search, and pagination on the MCP sessions list API and table, plus a can_reauth identity gate (#3823, #3824, #3825) - **Tool Call Execution UI** - Inline tool-call execution, stop streaming, bulk execute/submit, and a redesigned tool-call UI (#3837, #3843) - **Dimension Rankings Dashboard** - New dashboard tabs for team, customer, BU, and user rankings, backed by a GetDimensionRankings API (#3766) - **Model Pricing Attributes** - additional_attributes on model pricing rows with management API and UI editor (#3829) - **Prompt Cache Retention** - Prompt cache retention parameter on responses requests (#3810) - **Opus 4.8 Support** - System message handling and compatibility for Opus 4.8 (#3878, #3868) - **Key Rotation** - Rotate keys on 401/402/403 and return 502 upstream_credentials_exhausted when all keys are permanently dead (#3491) - **OTel Metrics** - OTel spec compatible metrics plus provider and semantic cache attributes in metrics export (#3865, #3816) - **Sheet Navigation** - Prev/next keyboard navigation and URL state across virtual key, MCP client, and routing rule sheets (#3739, #3740, #3744, #3745) - **Go 1.26.3** - Upgraded toolchain to Go 1.26.3 (#3782) ## 🐞 Fixed - **Bedrock Tool Names** - Truncate Bedrock function/tool names to the provider length limit - **Bedrock Guardrails** - Set guardrail config in Bedrock request built from responses (#3862) - **Anthropic Tool Use** - Default Anthropic tool_use input to {} when arguments are absent (#3880) - **Responses Streaming** - Fixed responses stream events (#3838) - **Compat Flow** - Fixed missing parameter parsing on the compat flow (#3881) - **Passthrough API Version** - Set a default API version in passthrough requests as a fallback (#3853) - **Virtual Key Updates** - Avoid overriding optional fields during virtual key update (#3855) - **User-Mode Flows** - Gate user-mode flows on caller user_id, skip temp token mint, and unify flow/credential kind filtering for pending flows (#3841, #3859) - **Partial Tool Calls** - Handle partial tool call execution failures and return successful results (#3849) - **URL Query Escaping** - Support escaped characters in URL query parameters (#3826) - **MCP Auth Errors** - Inline banner and retry support for MCP auth-required errors (#3856) - **JSON Editor Height** - Cap JSON editor max height at 400px in message views (#3842)

Summary
Adds
MCPAuthTypePerUserHeaders— a new per-user MCP authentication type where each caller submits their own API keys or signed-token header values (e.g.Authorization,X-Api-Key) rather than going through an OAuth dance. The admin declares the required header names (per_user_header_keys) at MCP client creation time; end users supply values on first tool use via an inline-401 submission flow that mirrors the existing per-user OAuth surface.Changes
MCPAuthTypePerUserHeadersauth type: Added toMCPAuthTypeconstants,MCPClientConfig, and all validation paths. Requires a non-emptyPerUserHeaderKeyslist;oauth_config_idmust not be set.MCPAuthRequiredError: ReplacedMCPUserOAuthRequiredErrorwithMCPAuthRequiredError(with aKindfield:"oauth"or"headers").MCPUserOAuthRequiredErroris retained as a type alias for backward compatibility.perUserHeadersResolver: New credstore resolver that looks up per-user header credentials by(auth_mode, identity, mcp_client_id). On miss or stale schema, initiates a submission flow and returns anMCPAuthRequiredErrorwithKind="headers"and aSubmitURL.MCPHeadersProviderinterface andmcp_headers.Provider: Storage backend for per-user header credentials and pending submission flow rows. MirrorsOAuth2Providerstructurally. IncludesInitiateUserSubmissionFlowwhich mints amcp_headers_authtemp token embedded in the auth-page URL fragment.VerifyHeadersConnection: New method onMCPManagerandBifrostthat opens a temporary MCP connection using caller-supplied header values, runs the Initialize handshake, and discovers tools. Used during admin MCP client creation (sample values) and user submission (validate before persisting).add_mcp_per_user_header_credentials_table(createsmcp_per_user_header_credentials, addsper_user_header_keys_jsontoconfig_mcp_clients, and creates partial unique indexes per auth mode) andadd_mcp_per_user_header_flows_table(createsmcp_per_user_header_flows).StaticConfigHeadersupdated: Now strips any header whose name appears inPerUserHeaderKeysfrom the plugin-visible static header set, preventing admin-set static values from leaking through the connect-plugin gate for per-user-headers clients.BifrostContextKeyMCPCallbackBaseURL: ReplacesBifrostContextKeyOAuthRedirectURI. The base URL is now set once; OAuth resolver appends/api/oauth/callback, headers resolver appends the workspace submit path.GovernancePlugin.PreMCPConnectionHook: New hook that resolves the caller's VK identity onto theBifrostContextbefore the credential-store resolver runs, so per-user auth types can key stored credentials by VK row ID.POST /api/mcp/clientextended to handleper_user_headersauth type: validatesper_user_header_keys, runsVerifyHeadersConnectionwith admin sample values, persists discovered tools, and discards the sample values.MCPPerUserHeadersHandlerwith routes:GET /api/mcp/per-user-headers/flows/{id}(flow detail),PUT /api/mcp/per-user-headers/flows/{id}(submit),DELETE /api/mcp/per-user-headers/credential/{id}(revoke).MCPSessionsHandlerextended to list, reauth, and revoke header credential and header flow rows alongside OAuth rows. Addedauth_kindfield tomcpSessionRowto disambiguate OAuth vs headers rows on the wire.mcpHeadersAuthScopetemp token scope: GrantsGETandPUTaccess to the per-user-headers flow endpoints, bound to the flow ID. Registered at server startup alongsidemcpAuthScope.CredentialSweepWorker: Background worker that reaps orphaned credential rows (24h cadence, 30-day retention) and expired pending flow rows (15-min cadence). Started and stopped alongside the OAuth sweep worker.identityForMCPAuthModemoved to its own file (credstore/identity.go) so bothperUserOAuthResolverandperUserHeadersResolvershare it without duplication.DeleteMCPClientConfigandDeleteVirtualKeynow also deletemcp_per_user_header_credentialsandmcp_per_user_header_flowsrows for the affected client/VK.MarkMCPPerUserHeaderCredentialsNeedsUpdate: Called byupdateMCPClientwhenPerUserHeaderKeyschanges, flipping existing active credential rows toneeds_updateso callers are forced to resubmit on next tool use.Type of change
Affected areas
How to test
go test ./...auth_type: per_user_headersandper_user_header_keys: ["Authorization"], supplying a sampleuser_headersmap in the request body. Verify the client is created with discovered tools and the sample values are not persisted.extra_fields.mcp_auth_requiredwithkind: "headers"and asubmit_url.submit_url, callGET /api/mcp/per-user-headers/flows/{id}to confirm the form schema, thenPUTwith valid header values. Confirm the credential row is created and the flow row is deleted.DELETE /api/mcp/per-user-headers/credential/{id}and confirm the credential and any pending flow rows are removed.per_user_header_keysand confirm existing credential rows flip toneeds_update, triggering the inline-401 on next tool use.Breaking changes
MCPUserOAuthRequiredErroris retained as a type alias forMCPAuthRequiredError, so existing callers that reference the old type continue to compile. TheBifrostContextKeyOAuthRedirectURIcontext key is replaced byBifrostContextKeyMCPCallbackBaseURL; any code outside this repository that sets the old key directly will need to be updated.Security considerations
BIFROST_ENCRYPTION_KEY).PerUserHeaderKeysentries are stripped from the plugin-visible static header set so connect-plugin hooks cannot read or rewrite per-user credential values.#t=<token>), which are not sent to servers inRefererheaders and do not appear in server access logs.(mode, identity, mcp_client_id)triple; the submit endpoint validates the caller's values against the upstream before persisting.Checklist
docs/contributing/README.mdand followed the guidelines