Skip to content

Enable multi-upstream for MCPServer, MCPRemoteProxy, and proxy runner#4322

Open
jhrozek wants to merge 3 commits intomainfrom
authserver-multi-upstream/3-enable
Open

Enable multi-upstream for MCPServer, MCPRemoteProxy, and proxy runner#4322
jhrozek wants to merge 3 commits intomainfrom
authserver-multi-upstream/3-enable

Conversation

@jhrozek
Copy link
Contributor

@jhrozek jhrozek commented Mar 23, 2026

Summary

The embedded auth server has supported sequential multi-upstream authorization chains since Phase 2, but every consumer layer (MCPServer controller, MCPRemoteProxy controller, proxy runner) blocked configs with more than one upstream provider. This PR lifts those restrictions and fixes the env var naming scheme to support multiple providers correctly.

  • The CRD now accepts multiple upstream providers at admission time with duplicate-name detection and DNS-label validation (MaxLength=63, RFC 1123 pattern)
  • Env var derivation switched from single TOOLHIVE_UPSTREAM_CLIENT_SECRET to name-keyed TOOLHIVE_UPSTREAM_CLIENT_SECRET_<PROVIDER>, making bindings position-independent across provider reordering
  • Runtime config validation enforces explicit naming for multi-upstream (empty names default to "default" only for single-upstream; "default" is reserved)
  • Upstream names validated against DNS-label regex to prevent storage key delimiter injection
  • Upstreamswap middleware log lines now include provider name for multi-upstream debugging

Fixes #4138

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

Changes

File Change
cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go Remove single-upstream restriction, add duplicate-name + DNS-label validation
cmd/thv-operator/pkg/controllerutil/authserver.go Name-keyed env var binding via upstreamSecretBinding, multi-provider iteration
pkg/authserver/config.go Multi-upstream name validation, remove GetUpstream()
pkg/auth/upstreamswap/middleware.go Add provider name to structured log lines
deploy/charts/operator-crds/ Regenerated CRD manifests with Pattern + MaxLength
docs/operator/crd-api.md Regenerated API reference

Special notes for reviewers

  • The upstreamSecretBinding struct is the single source of truth for env var naming, shared between GenerateAuthServerEnvVars (Pod env) and buildUpstreamRunConfig (runtime config) to prevent desync.
  • The env var suffix is derived from provider Name (uppercased, hyphens→underscores). Since the CRD regex forbids underscores, the transform is injective — no two valid names can produce the same suffix.
  • Multi-upstream proxy runner swap middleware defaults to Upstreams[0] for now; explicit per-backend ProviderName selection is addressed in the follow-up branch (authserver-multi-upstream/4-tokens-in-identity).

Generated with Claude Code

jhrozek and others added 3 commits March 23, 2026 22:51
Move multi-upstream restrictions from the authserver library to consumer
layers. The library now accepts multi-upstream configs but enforces name
semantics: single-upstream defaults empty names to "default", while
multi-upstream requires explicit non-"default" names with distinct error
messages for empty vs reserved names.

Validate upstream names against a DNS-label regex (no leading/trailing
hyphens, lowercase alphanumeric only) to prevent delimiter injection in
storage keys. Add test coverage for invalid name formats (uppercase,
underscores, leading/trailing hyphens).

Remove the GetUpstream() convenience method (no callers remain after
Phase 2). Add a cardinality guard in the proxy runner's Run() that
rejects len(Upstreams) > 1 with an actionable error pointing to
VirtualMCPServer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the len > 1 guard from MCPExternalAuthConfig.validateEmbeddedAuthServer()
so the CRD accepts multi-upstream configs. Add multi-upstream rejection to
MCPServer and MCPRemoteProxy controllers in handleExternalAuthConfig(), setting
a ConditionFalse status with reason MultiUpstreamNotSupported and an actionable
error directing users to VirtualMCPServer.

Add duplicate upstream name validation in the CRD webhook so conflicts
are caught at admission time rather than Pod startup. Tighten the Name
field pattern to disallow trailing hyphens and add MaxLength=63 for
RFC 1123 compliance.

VirtualMCPServer remains unrestricted as the intended multi-upstream consumer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The embedded auth server has supported sequential multi-upstream chains
since the Phase 2 work, but every consumer layer (MCPServer controller,
MCPRemoteProxy controller, proxy runner) blocked configs with more than
one upstream provider. This commit lifts all those restrictions and fixes
the two bugs that would have broken multi-upstream even without the guards.

Guard removal:
- MCPServer controller: remove len > 1 check in handleExternalAuthConfig
- MCPRemoteProxy controller: same removal
- Proxy runner Run(): remove len > 1 early-return
- Remove associated condition type/reason constants and rejection tests

Converter fix (authserver.go):
- GenerateAuthServerEnvVars now iterates all providers and emits
  name-keyed env vars (TOOLHIVE_UPSTREAM_CLIENT_SECRET_OKTA, _GITHUB, …)
  derived from each provider's Name field, instead of only reading
  UpstreamProviders[0] into a single unindexed var. Name-keyed bindings
  are position-independent, so reordering providers in the CRD does not
  change the secret-to-provider mapping.
- buildEmbeddedAuthServerRunnerConfig now builds Upstreams from all
  providers instead of only the first; buildUpstreamRunConfig gains an
  envVarName parameter to match the env var naming

Middleware fix (middleware.go):
- Restore ProviderName auto-derivation in addUpstreamSwapMiddleware,
  defaulting to the first upstream's name for single-upstream configs.
  Multi-upstream ProviderName selection will be addressed when a CRD
  field is added in a follow-up.

Observability (upstreamswap/middleware.go):
- Add provider field to all upstreamswap log lines (injection success,
  empty-token warning, get-tokens error) to make confused-deputy
  debugging tractable when multiple providers are in play.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the size/M Medium PR: 300-599 lines changed label Mar 23, 2026
@codecov
Copy link

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 79.06977% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.45%. Comparing base (e735122) to head (17addd2).

Files with missing lines Patch % Lines
...erator/api/v1alpha1/mcpexternalauthconfig_types.go 0.00% 1 Missing and 3 partials ⚠️
pkg/authserver/config.go 70.00% 0 Missing and 3 partials ⚠️
cmd/thv-operator/pkg/controllerutil/authserver.go 90.90% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4322      +/-   ##
==========================================
- Coverage   68.61%   68.45%   -0.16%     
==========================================
  Files         478      478              
  Lines       48450    48491      +41     
==========================================
- Hits        33243    33196      -47     
- Misses      12367    12394      +27     
- Partials     2840     2901      +61     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move multi-upstream restriction from authserver library to consumer layers (RFC-0052 Phase 3)

1 participant