Skip to content

005. OAuth protection for the muster server

Context

muster is being deployed to a central Management Cluster to aggregate MCP servers from other clusters. This public exposure requires protecting the muster server itself with authentication.

Previously, in 004-oauth-proxy.md, we defined how muster acts as an OAuth Proxy to handle authentication for remote MCP servers. Now, we must address how to authenticate users to the muster server itself.

Decision

We will implement OAuth 2.1 protection for the muster server using the mcp-oauth library, similar to mcp-kubernetes.

1. Dual Role of muster server

muster server will play two distinct OAuth roles:

  1. OAuth Resource Server: It protects its own endpoints (e.g., /mcp, /workflows). Users (via the Agent) must present a valid Access Token issued by the trusted IdP (eg via Dex, or Github, Google etc) to access these resources.
  2. OAuth Client (Proxy): As defined in 004, it acts as a client to obtain tokens for downstream remote MCP servers.

2. Architecture

┌─────────────────────────────────────────────────────────────┐
│                      muster server                          │
│                                                             │
│  [ OAuth Middleware (Resource Server) ]                     │
│       Validates Token from Agent                            │
│               │                                             │
│               ▼                                             │
│  [ Aggregator / Tool Handler ]                              │
│               │                                             │
│               ▼                                             │
│  [ OAuth Proxy (Client) ]                                   │
│       Injects Token for Remote MCPs                         │
│               │                                             │
│               ▼                                             │
│        Remote MCP Server                                    │
└─────────────────────────────────────────────────────────────┘

3. MCP Roles and Architecture

Each component plays dual roles in the MCP protocol:

┌────────────────┐         ┌────────────────┐            ┌────────────────┐            ┌────────────────┐
│     Cursor     │  stdio  │  muster agent  │  HTTP/SSE  │ muster server  │  HTTP/SSE  │   Remote MCP   │
│                │ <-----> │                │ ---------> │                │ ---------> │                │
│   MCP Host     │         │   MCP Server   │            │   MCP Server   │            │   MCP Server   │
│   MCP Client   │         │   MCP Client   │            │   MCP Client   │            │                │
└────────────────┘         └────────────────┘            └────────────────┘            └────────────────┘

Cursor configuration (e.g., .cursor/mcp.json):

{
  "muster": {
    "command": "muster",
    "args": ["agent", "--mcp-server", "--endpoint=http://localhost:8090/mcp"]
  }
}

Key points: 1. Cursor starts muster agent as a local stdio MCP server (subprocess) 2. Cursor is an MCP Host and MCP Client; it communicates with the Agent via stdio 3. muster agent is an MCP Server (for Cursor) AND an MCP Client (for muster server) 4. muster server is an MCP Server (for Agent) AND an MCP Client (for Remote MCPs) 5. The Agent proxies tool calls, resources, and prompts between Cursor and Server

4. Key Insight: Same Pattern as Downstream Auth (Lazy Initialization)

Critical Learning from ADR 004 Implementation: Authentication happens during the MCP handshake (initialize request), not during a tool call. This is exactly the same situation for both: - Agent -> muster server: Agent (as MCP Client) gets 401 when connecting to protected Server - muster server -> Remote MCP: Server (as MCP Client) gets 401 when connecting to protected Remote

The solution is the same: Use lazy initialization with synthetic authentication tools.

Since the Agent is an MCP Server (for Cursor), it can expose synthetic tools even when its upstream connection to muster server fails. This provides a consistent user experience where auth URLs always appear as tool results in Cursor.

5. Authentication Flow (Agent -> muster)

  1. Startup: Cursor starts muster agent --mcp-server --endpoint=<server-url> as a stdio subprocess.
  2. Agent Connects to Server: Agent (as MCP Client) attempts to establish connection to muster server (SSE/Streamable-HTTP).
  3. Handshake Rejection: Server responds with 401 Unauthorized and WWW-Authenticate header pointing to its IdP.
  4. Lazy Initialization (Pending Auth State):
    • Agent detects the 401 during connection.
    • Agent parses WWW-Authenticate header to extract issuer/realm.
    • Agent enters "pending auth" state (similar to RegisterPendingAuth() in ADR 004).
    • Agent exposes a synthetic authenticate_muster tool to Cursor.
    • Agent completes MCP handshake with Cursor (stdio) - Cursor sees the synthetic tool.
  5. User Calls Synthetic Auth Tool:
    • User (via Cursor) calls authenticate_muster tool.
    • Agent generates authorization URL (Authorization Code Flow with PKCE).
    • Agent starts a temporary local listener (e.g., on port 3000).
    • Tool result contains the auth URL: "Please sign in: [Link]".
  6. Browser Flow:
    • User clicks link -> IdP (Dex) -> User logs in.
    • IdP redirects to http://localhost:3000/callback with code.
  7. Token Exchange:
    • Agent receives code via the local listener.
    • Agent exchanges code for Access/Refresh tokens (direct to IdP).
    • Agent stores tokens locally (in memory or XDG-compliant secure file).
  8. Upgrade to Connected:
    • Agent retries MCP connection to Server with Authorization: Bearer <token> header.
    • Server validates token; handshake succeeds.
    • Agent receives tools, resources, and prompts from Server.
    • Agent replaces synthetic authenticate_muster tool with real tools (similar to UpgradeToConnected()).
    • Agent sends tools/list_changed notification to Cursor.
    • User can now interact with muster through Cursor.

6. Consistency with Downstream Auth

Aspect Agent -> muster (this ADR) muster -> Remote (ADR 004)
When 401 occurs Agent connecting to Server Server connecting to Remote MCP
Who is the MCP Client? Agent muster server
Who is the MCP Server (for user)? Agent (for Cursor) muster server (for Agent/Cursor)
Synthetic tools? Yes - authenticate_muster Yes - authenticate_<server>
Lazy init pattern Same: pending auth -> upgrade RegisterPendingAuth() + UpgradeToConnected()
Token storage Agent-side (local filesystem) Server-side (session store)
User experience Auth URL in tool result Auth URL in tool result

7. Relation to "OAuth Proxy" (Downstream Auth)

Once the Agent is authenticated and connected (Step 8), the "OAuth Proxy" logic from 004 kicks in if a request is destined for a remote MCP server.

  • Scenario A: Same IdP (Token Forwarding)

    • If muster server and Remote MCP Server share the same IdP and trust the same audiences/clients, muster can forward the user's token directly using Token Forwarding (auth.forwardToken: true).
  • Scenario B: Different IdPs (Token Exchange)

    • muster server validates the incoming token (User -> muster).
    • muster server exchanges its token for one valid on the remote IdP using RFC 8693 Token Exchange.
    • The exchanged token is used for downstream requests.

Implementation Steps

  1. Agent: Implement lazy initialization with synthetic auth tool:

    • Handle 401 responses during SSE/Streamable-HTTP connection to muster server
    • Parse WWW-Authenticate header to discover the authorization server
    • Enter "pending auth" state and expose synthetic authenticate_muster tool to Cursor
    • Implement Authorization Code Flow with PKCE when synthetic tool is called
    • Start a temporary local HTTP listener for the callback (e.g., port 3000)
    • Store tokens locally (in memory or XDG-compliant secure file)
    • Retry connection with Authorization: Bearer <token> header
    • Replace synthetic tool with real tools from Server (send tools/list_changed)
    • CIMD muster-agent.json hosted on GitHub Pages
  2. Server: Add ValidateToken middleware to the main HTTP entry point:

    • Return 401 Unauthorized with proper WWW-Authenticate header for unauthenticated requests
    • Validate token signature, issuer, and audience using mcp-oauth library
    • Extract session identity from validated token for downstream auth reuse
  3. Docs: Update deployment guide to include Dex client registration for the Agent.

Consequences

  • Consistent UX: Auth flows work identically whether authenticating to muster server or Remote MCPs - users always see auth URLs as tool results in Cursor.
  • Agent Complexity: The Agent must implement the same lazy initialization pattern as muster server (pending auth state, synthetic tools, upgrade to connected).
  • Code Reuse: The Agent can reuse much of the OAuth and lazy initialization logic from the Server implementation.
  • SSO: Token Forwarding and Token Exchange minimize the need for multiple authentications.
  • Security: muster is now secure by default when exposed.

Addendum: mcp-oauth Integration (Learning from mcp-kubernetes)

Overview

After analyzing mcp-kubernetes's implementation, we will use the github.com/giantswarm/mcp-oauth library (v0.2.26+) to implement OAuth 2.1 protection for muster server. This provides a battle-tested OAuth 2.1 implementation with all the security features we need.

mcp-oauth Library Capabilities

The library provides:

  • OAuth 2.1 Server Implementation: Full RFC-compliant OAuth 2.1 with mandatory PKCE
  • Multiple Provider Support: Dex OIDC (our primary choice) and Google OAuth
  • Token Storage Backends: In-memory (dev) and Valkey/Redis (production)
  • Security Features: Rate limiting, audit logging, AES-256-GCM token encryption
  • Client Registration: RFC 7591 Dynamic Client Registration with rate limiting
  • CIMD Support: Client ID Metadata Documents per MCP 2025-11-25 spec

Key Components from mcp-kubernetes

We will adapt the following components from mcp-kubernetes:

1. OAuth HTTP Server (internal/server/oauth_http.go)

The core OAuth integration that: - Creates an OAuth server using oauth.NewServer() with Dex provider - Exposes standard OAuth endpoints: /oauth/register, /oauth/authorize, /oauth/token, /oauth/callback - Exposes metadata endpoints: /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource - Wraps MCP endpoints with ValidateToken middleware for authentication

// Key integration pattern from mcp-kubernetes
oauthServer, tokenStore, err := createOAuthServer(config)
oauthHandler := oauth.NewHandler(oauthServer, oauthServer.Logger)

// MCP endpoint protected by OAuth
mux.Handle("/mcp", oauthHandler.ValidateToken(mcpHandler))

2. Token Provider (internal/mcp/oauth/token_provider.go)

Context helpers for passing OAuth tokens through the request chain:

// Store ID token in context for downstream use
ctx = oauth.ContextWithIDToken(ctx, idToken)

// Retrieve token in tool handlers
token, ok := oauth.GetIDTokenFromContext(ctx)

3. Access Token Injector Middleware

Middleware that retrieves the user's stored OAuth token and injects it into the request context for downstream authentication:

func (s *OAuthHTTPServer) createAccessTokenInjectorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userInfo, ok := oauth.UserInfoFromContext(ctx)
        if !ok {
            next.ServeHTTP(w, r)
            return
        }

        token, err := s.tokenStore.GetToken(ctx, userInfo.Email)
        // Extract ID token and inject into context
        idToken := GetIDToken(token)
        ctx = ContextWithIDToken(ctx, idToken)
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

muster-Specific Implementation

Server-Side (muster server)

  1. Create internal/server/oauth.go: OAuth server configuration and setup, adapting mcp-kubernetes's pattern
  2. Create internal/oauth/ package: Context helpers similar to mcp-kubernetes/internal/mcp/oauth/
  3. Modify Aggregator HTTP handler: Wrap with ValidateToken middleware
  4. Add CLI flags: --enable-oauth, --dex-issuer-url, --dex-client-id, --dex-client-secret, etc.

Agent-Side (muster agent)

The Agent handles the client side of OAuth authentication:

  1. Detect 401 from Server: When connecting to a protected muster server
  2. Parse WWW-Authenticate header: Extract issuer URL and realm information
  3. Expose synthetic authenticate_muster tool: While in pending auth state
  4. Implement Authorization Code Flow with PKCE: Using mcp-oauth's client utilities
  5. Store tokens locally: XDG-compliant secure file storage
  6. Retry connection with Bearer token: After successful authentication

Configuration Example

# Helm values for muster server with OAuth
muster:
  oauth:
    enabled: true
    baseURL: "https://muster.example.com"
    provider: "dex"
    dex:
      issuerURL: "https://dex.example.com"
      clientID: "muster-server"
      clientSecret: "${DEX_CLIENT_SECRET}"
    storage:
      type: "valkey"
      valkey:
        url: "valkey.muster.svc:6379"
        tls:
          enabled: true

Security Configuration

Aligned with mcp-kubernetes best practices:

Setting Default Production Recommendation
--allow-public-registration false Keep false, use registration token
--registration-token Required Use cryptographically random token
--oauth-encryption-key Optional Required for production (32 bytes, base64)
--enable-cimd true Keep true for MCP 2025-11-25 compliance
--trusted-public-registration-schemes [] Consider cursor,vscode for internal use

OAuth Endpoints on muster server

Endpoint Description RFC
/.well-known/oauth-authorization-server Authorization Server Metadata RFC 8414
/.well-known/oauth-protected-resource Protected Resource Metadata RFC 9728
/oauth/register Dynamic Client Registration RFC 7591
/oauth/authorize OAuth Authorization RFC 6749
/oauth/token Token Endpoint RFC 6749
/oauth/callback OAuth Callback (from Dex) RFC 6749
/oauth/revoke Token Revocation RFC 7009
/mcp Protected MCP endpoint (requires Bearer token) -

Implementation Order

  1. Server OAuth Protection (first priority):
  2. Add mcp-oauth dependency
  3. Create OAuth configuration types
  4. Implement OAuth HTTP server wrapper for aggregator
  5. Add CLI flags and Helm chart values
  6. Test with manual curl requests

  7. Agent OAuth Client (second priority):

  8. Implement 401 detection during SSE/HTTP connection
  9. Parse WWW-Authenticate header
  10. Implement synthetic authenticate_muster tool
  11. Implement PKCE flow with local callback listener
  12. Token storage (XDG-compliant)
  13. Retry logic with Bearer token

  14. Agent CIMD Configuration (third priority):

  15. Host muster-agent.json on GitHub Pages
  16. Document client registration process

Differences from mcp-kubernetes

Aspect mcp-kubernetes muster
Primary Use Case Direct K8s API access Aggregating remote MCPs
Downstream Auth ID token → K8s OIDC OAuth Proxy (ADR 004)
Token Storage Location Server-side (Valkey) Server + Agent (local)
CAPI Federation Kubeconfig discovery Remote MCP aggregation
Agent Component N/A (direct HTTP) Stdio MCP server (for Cursor)

Dependencies

Add to go.mod:

require (
    github.com/giantswarm/mcp-oauth v0.2.26
    golang.org/x/oauth2 v0.34.0
)

References