From 94f917a0d32acd2dc1908e0f17ef09861a887d0a Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 11 Aug 2026 10:38:38 +0530 Subject: [PATCH] feat: add mcp_enabled for the remote MCP surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authorizer 2.4.0 can serve its MCP tool surface over HTTP at POST /mcp as an OAuth 2.1 resource server (authorizerdev/authorizer#757). Off by default — it is a new internet-facing authenticated surface. It is served on the MAIN HTTP port, not one of its own, because it must be publicly reachable on the same origin as the OAuth metadata clients discover it through. So it needs no Service or Ingress change and inherits the existing CORS, security headers and rate limiting. mcp_enabled requires authorizer_url, and the chart FAILS AT RENDER TIME without it, following the SMTP guard already at the top of deployment.yaml. The server exits at boot in that configuration — every token presented at /mcp is checked against /mcp, and deriving that identifier from request headers instead would let a caller name their own token's audience. In-cluster the failure would otherwise be a CrashLoopBackOff whose cause is one line in a container log. Verified with helm template in all three states: off (renders, MCP_ENABLED false), on without a URL (fails at render with the explanatory message), on with a URL (renders, MCP_ENABLED true). helm lint clean. --- README.md | 3 +++ templates/deployment.yaml | 25 +++++++++++++++++++++++++ values.yaml | 10 ++++++++++ 3 files changed, 38 insertions(+) diff --git a/README.md b/README.md index 0532ad5..846582d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ The chart exposes three ports: | `9091` | gRPC | Same API over gRPC | Yes — opt-out via `service.grpc.enabled: false` | | `8081` | HTTP | Prometheus `/metrics` endpoint | No — opt-in via `metrics.service.enabled: true` | +The MCP surface is served on the **main HTTP port**, not a port of its own: it must be publicly reachable on the same origin as the OAuth metadata clients discover it through, so it inherits the existing Ingress, CORS, security headers and rate limiting. Enable it with `authorizer.mcp_enabled: true` (requires `authorizer.authorizer_url`). + The metrics port is never added to the main `Service` used for Ingress. Use `metrics.service.enabled: true` to create a dedicated internal `ClusterIP` service for in-cluster scraping, or `metrics.serviceMonitor.enabled: true` for Prometheus Operator integration. ## Values Reference @@ -113,6 +115,7 @@ The metrics port is never added to the main `Service` used for Ingress. Use `met | `authorizer.metrics_port` | Dedicated Prometheus `/metrics` listen port (`--metrics-port`) | false | `8081` | | `authorizer.metrics_host` | Bind address for `/metrics` (`--metrics-host`). Use `0.0.0.0` for in-cluster scraping | false | `0.0.0.0` | | `authorizer.authorizer_url` | Public URL of this Authorizer deployment | false | — | +| `authorizer.mcp_enabled` | Serve the MCP tool surface at `POST /mcp` as an OAuth 2.1 resource server. **Requires `authorizer.authorizer_url`** — the chart fails at render time without it, because the server exits at boot | false | `false` | | `authorizer.reset_password_url` | Custom URL for password reset emails | false | — | | `authorizer.backchannel_logout_uri` | Back-channel logout URI | false | — | | `authorizer.custom_access_token_script` | JavaScript snippet (URL-encoded) injected at token issuance | false | — | diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 7772a17..9cafd9f 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -13,6 +13,24 @@ port > 0, sender email set — so this never rejects a config the server accepts {{- fail "authorizer.enable_email_verification is true but SMTP is incomplete. Authorizer 2.4.0 exits at boot in this configuration (users would be created unverified with no way to ever verify), so the pod would CrashLoopBackOff. Set authorizer.smtp_host, authorizer.smtp_port and authorizer.smtp_sender_email, or set enable_email_verification: false." }} {{- end }} {{- end }} +{{/* +--mcp-enabled requires --url, and the server EXITS at boot without it rather +than starting in a degraded mode. + +The reason is not cosmetic: every token presented at /mcp is checked against +this deployment's canonical resource identifier, /mcp. With no --url that +identifier would be derived from request headers (X-Authorizer-URL, +X-Forwarded-Host, Host), which lets a caller name the audience their own token +must match — no check at all. So the server refuses the combination. + +Caught at render time for the same reason as the SMTP check above: in-cluster +the failure is a CrashLoopBackOff whose cause is one line in the container log. +*/}} +{{- if .Values.authorizer.mcp_enabled }} + {{- if not .Values.authorizer.authorizer_url }} + {{- fail "authorizer.mcp_enabled is true but authorizer.authorizer_url is not set. The MCP surface binds access tokens to /mcp, and deriving that identifier from request headers instead would let a caller choose their own token audience — so the server exits at boot in this configuration and the pod would CrashLoopBackOff. Set authorizer.authorizer_url (e.g. https://auth.example.com), or set mcp_enabled: false." }} + {{- end }} +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -112,6 +130,7 @@ spec: --grpc-insecure="${GRPC_INSECURE:-false}" \ --grpc-tls-cert="${GRPC_TLS_CERT}" \ --grpc-tls-key="${GRPC_TLS_KEY}" \ + --mcp-enabled="${MCP_ENABLED:-false}" \ --rate-limit-rps="${RATE_LIMIT_RPS}" \ --rate-limit-burst="${RATE_LIMIT_BURST}" \ --rate-limit-fail-closed="${RATE_LIMIT_FAIL_CLOSED}" \ @@ -372,6 +391,12 @@ spec: value: {{ .Values.authorizer.grpc_port | default 9091 | toString | quote }} - name: "ENABLE_GRPC_REFLECTION" value: {{ include "authorizer.bool" (list .Values.authorizer.enable_grpc_reflection true) | quote }} + # MCP tool surface, served at POST /mcp on the main HTTP listener + # (not its own port — it must be publicly reachable on the same origin + # as the OAuth metadata clients discover it through). Requires + # authorizer_url; guarded at render time above. + - name: "MCP_ENABLED" + value: {{ include "authorizer.bool" (list .Values.authorizer.mcp_enabled false) | quote }} - name: "GRPC_INSECURE" value: {{ .Values.authorizer.grpc_insecure | default false | toString | quote }} {{- if .Values.authorizer.grpc_tls_cert }} diff --git a/values.yaml b/values.yaml index adffaf6..f90b92d 100644 --- a/values.yaml +++ b/values.yaml @@ -181,6 +181,16 @@ authorizer: # URL for authorizer deployment authorizer_url: null + # Serve the MCP tool surface over HTTP at POST /mcp, as an + # OAuth 2.1 resource server (RFC 9728 discovery, RFC 8707 audience-bound + # tokens). Off by default: it is a new internet-facing authenticated surface. + # + # REQUIRES authorizer_url. The chart fails at render time without it, because + # the server exits at boot — the audience every MCP token is checked against + # is derived from that URL, and deriving it from request headers instead would + # let a caller choose their own audience. + mcp_enabled: false + # Couchbase bucket. In case of couchbase database_type couchbase_bucket: null