You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Derive the MCP proxy configuration and share OSMO's Redis
Three reductions to what a deployer must state, none of which changes
what the deployment does.
**Values the chart already knew.** accessTokenAudience had to equal
resourceUrl, scope had to equal resourceUrl plus the scope name, and the
issuer had to be resourceUrl minus /mcp -- and the chart refused to
render unless each matched what it had already computed. A deployer was
asked for an answer the chart held, then rejected for giving a different
one. They are derived now, so the 30-line cross-checking block in
MCPAuthConfig, the matching Helm fail blocks, the three environment
variables carrying the restated values, and the three CI cases that
existed only to prove the cross-checks fired all go.
This also drops trustedHttpsRedirectOrigins, which no deployment sets and
which cost a CSV environment variable, two properties, a validator branch
and a Helm regex loop. Native clients use loopback redirects, still
allowed.
**A URL discovery publishes.** accessTokenJwksUrl is jwks_uri in every
OIDC discovery document, which FastMCP already fetches at startup. OSMO
hand-built a JWTVerifier only to pin it, and carried a process-lifetime
httpx client to serve it. OIDCProxy builds the verifier now, and OSMO
overrides only what discovery cannot supply: an Entra resource
application configured for v1 access tokens issues them from
https://sts.windows.net/<tenant>/ even when its discovery document
advertises the v2.0 issuer, so accessTokenIssuer stays configured.
The audience is applied inside that override rather than through
OIDCProxy's own audience argument, because that argument is forwarded to
the provider's authorize and token endpoints (oidc_proxy.py:432-434),
which Entra does not accept. An existing test asserting that no extra
token parameters are sent caught this.
**Redis that every sibling already shares.** MCP carried its own host,
port and TLS settings. serviceName and port fell back to services.redis
but tlsEnabled did not, so a deployment whose Redis requires TLS could
satisfy every other service and still CrashLoop MCP. All three come from
services.redis now; only dbNumber stays, to isolate proxy state from
OSMO's other Redis users, and its default moves off 0 for that reason.
The chart also refused more than one MCP replica, on the premise that
FastMCP serializes token refresh within a process. FastMCP says the
opposite -- "All state is stored in the configured client_storage
backend (Redis, disk, etc.) enabling horizontal scaling across multiple
instances" (oauth_proxy/proxy.py:212-213), with :1903-1912 handling the
distributed refresh race explicitly. The rule also contradicted itself,
since sharing state through Redis only matters across replicas it
forbade. The negative test asserting the ban is replaced by a positive
one that two replicas render, negative-tested by reintroducing a ban.
Required OIDC proxy values: 10 -> 6.
Note for anyone using --config rather than environment variables: a stale
file carrying issuer_url, auth_scope, oidc_access_token_audience or
oidc_access_token_jwks_url now fails at startup with an unrecognized-key
error rather than being ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|`services.mcp.requestTimeoutSeconds`| Total timeout for each MCP-initiated Gateway request, from 1 through 60 seconds. |`10`|
103
-
|`services.mcp.replicas`| Number of MCP replicas. Must remain `1` with `oidcProxy.enabled` because FastMCP 3.4.7 refresh serialization is process-local. |`1`|
103
+
|`services.mcp.replicas`| Number of MCP replicas. The OIDC proxy keeps its state in Redis, so it scales out. |`1`|
104
104
|`services.mcp.extraEnv`| Additional non-managed environment variables. It cannot override MCP host, port, Gateway origin, or request timeout. |`[]`|
|`services.mcp.extraVolumes`| Additional MCP pod volumes. |`[]`|
107
107
|`services.mcp.oidcProxy.enabled`| Enable FastMCP's built-in OIDC proxy inside the existing MCP process. It advertises CIMD and retains DCR as a compatibility fallback. |`false`|
108
-
|`services.mcp.oidcProxy.scope`| Full delegated scope URI advertised to MCP clients and requested upstream, normally `<resourceUrl>/access_as_user`. |`""`|
109
-
|`services.mcp.oidcProxy.trustedHttpsRedirectOrigins`| Exact HTTPS origins allowed for pre-registered web-client redirects; native clients use loopback redirects. |`[]`|
|`services.mcp.oidcProxy.oidc.clientSecretFile`| Mounted file containing the upstream OIDC client secret. |`/etc/osmo/mcp-auth/client-secret`|
113
111
|`services.mcp.oidcProxy.oidc.accessTokenIssuer`| Exact issuer required on upstream API access tokens. |`""`|
114
-
|`services.mcp.oidcProxy.oidc.accessTokenAudience`| Exact OSMO MCP resource audience required on upstream API access tokens; must equal `resourceUrl`. |`""`|
115
-
|`services.mcp.oidcProxy.oidc.accessTokenJwksUrl`| HTTPS JWKS URL used to verify upstream API access tokens. |`""`|
116
112
|`services.mcp.oidcProxy.oidc.accessTokenRequiredScope`| Short scope value required in the upstream access token's `scp` claim. |`access_as_user`|
117
-
|`services.mcp.oidcProxy.redis`| Redis connection used by FastMCP for registrations, authorization state, and encrypted upstream tokens; blank host/port inherit `services.redis`. |See `values.yaml`|
113
+
|`services.mcp.oidcProxy.redis.dbNumber`|Logical Redis database for proxy state. Host, port and TLS come from `services.redis`. |`1`|
118
114
|`services.mcp.oidcProxy.accessTokenTtlSeconds`| Lifetime of proxy access tokens, from 60 through 3600 seconds. |`600`|
119
115
|`services.mcp.oidcProxy.refreshTokenTtlSeconds`| Lifetime of proxy refresh tokens, from 300 through 604800 seconds. |`28800`|
120
116
|`services.mcp.oidcProxy.upstreamTimeoutSeconds`| Timeout for upstream OIDC requests, from 1 through 60 seconds. |`10`|
@@ -123,7 +119,7 @@ destination, but it cannot validate external DNS.
123
119
The in-process proxy follows OSMO's OIDC profile: a full delegated scope URI is requested
124
120
from the upstream provider while its short suffix is enforced in the verified
125
121
API access token. Register the single stable upstream redirect URI
126
-
`<resourceUrl origin>/auth/callback`. MCP clients still configure only `resourceUrl`.
122
+
`<resourceUrl origin>/mcp/auth/callback`. MCP clients still configure only `resourceUrl`.
127
123
CIMD-capable clients identify themselves with a hosted metadata document;
128
124
older clients can use FastMCP's `/register` DCR endpoint. Both paths use
129
125
authorization-code flow with PKCE and end in the same OSMO Gateway and semantic
{{- $originPort := trimPrefix ":" $originPortSuffix | int }}
52
-
{{- if or (lt $originPort 1) (gt $originPort 65535) }}
53
-
{{- fail "services.mcp.oidcProxy.trustedHttpsRedirectOrigins ports must be between 1 and 65535" }}
54
-
{{- end }}
55
-
{{- end }}
56
-
{{- end }}
57
-
{{- $scope = required "services.mcp.oidcProxy.scope is required when the OIDC proxy is enabled" $oidcProxy.scope }}
58
36
{{- $oidcConfigUrl = required "services.mcp.oidcProxy.oidc.configUrl is required when the OIDC proxy is enabled" $oidcProxy.oidc.configUrl }}
59
37
{{- $oidcClientId = required "services.mcp.oidcProxy.oidc.clientId is required when the OIDC proxy is enabled" $oidcProxy.oidc.clientId }}
60
38
{{- $oidcClientSecretFile = required "services.mcp.oidcProxy.oidc.clientSecretFile is required when the OIDC proxy is enabled" $oidcProxy.oidc.clientSecretFile }}
61
39
{{- $oidcAccessTokenIssuer = required "services.mcp.oidcProxy.oidc.accessTokenIssuer is required when the OIDC proxy is enabled" $oidcProxy.oidc.accessTokenIssuer }}
62
-
{{- $oidcAccessTokenAudience = required "services.mcp.oidcProxy.oidc.accessTokenAudience is required when the OIDC proxy is enabled" $oidcProxy.oidc.accessTokenAudience }}
63
-
{{- $oidcAccessTokenJwksUrl = required "services.mcp.oidcProxy.oidc.accessTokenJwksUrl is required when the OIDC proxy is enabled" $oidcProxy.oidc.accessTokenJwksUrl }}
64
40
{{- $oidcAccessTokenRequiredScope = required "services.mcp.oidcProxy.oidc.accessTokenRequiredScope is required when the OIDC proxy is enabled" $oidcProxy.oidc.accessTokenRequiredScope }}
65
41
{{- if not (regexMatch "^[A-Za-z0-9:._~-]{1,128}$" $oidcAccessTokenRequiredScope) }}
66
42
{{- fail "services.mcp.oidcProxy.oidc.accessTokenRequiredScope must be one non-empty scope" }}
67
43
{{- end }}
68
-
{{- if or (gt (len $oidcAccessTokenAudience) 2048) (not (regexMatch "^[^[:space:][:cntrl:]]+$" $oidcAccessTokenAudience)) }}
69
-
{{- fail "services.mcp.oidcProxy.oidc.accessTokenAudience must be one non-empty value without whitespace or control characters" }}
70
-
{{- end }}
71
-
{{- if ne $oidcAccessTokenAudience $mcpResourceUrl }}
72
-
{{- fail "services.mcp.oidcProxy.oidc.accessTokenAudience must equal services.mcp.resourceUrl" }}
73
-
{{- end }}
74
-
{{- if ne $scope (printf "%s/%s" $mcpResourceUrl $oidcAccessTokenRequiredScope) }}
75
-
{{- fail "services.mcp.oidcProxy.scope must equal services.mcp.resourceUrl followed by oidc.accessTokenRequiredScope" }}
0 commit comments