Skip to content

006. Session-Scoped Tool Visibility for OAuth-Protected MCP Servers

Context

With the introduction of OAuth authentication in ADR 004 and ADR 005, remote MCP servers can now require authentication before exposing their tools. However, the current aggregator architecture maintains a global tool registry that is shared across all users.

This creates a fundamental problem: tool visibility depends on who has authenticated with which servers, but the current implementation exposes the same tool list to everyone.

Current Behavior (Problem)

User A authenticates with mcp-kubernetes → muster connects
User B authenticates with mcp-github → muster connects
User C has not authenticated with anything

Current: All three users see tools from BOTH mcp-kubernetes AND mcp-github
Expected: Each user should only see tools from servers THEY have authenticated with

Scenarios Requiring Per-User Tool Visibility

  1. Different access levels: User A has access to production Kubernetes, User B only has access to staging.

  2. Different tool permissions: User A authenticated with full permissions, User B with read-only. The MCP server might expose different tools based on the user's authorization.

  3. Private servers: Some MCP servers are only accessible to specific users based on their IdP group membership.

  4. Multi-tenant environments: Each user may have access to a different set of backend systems.

Decision

We will implement session-scoped tool visibility, where each user session maintains its own view of available tools based on their authentication status with each remote MCP server.

1. Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                           muster server                                      │
│                                                                              │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                     Session Manager                                     │ │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐                  │ │
│  │  │ Session A    │  │ Session B    │  │ Session C    │                  │ │
│  │  │ (User A)     │  │ (User B)     │  │ (User C)     │                  │ │
│  │  │              │  │              │  │              │                  │ │
│  │  │ Token: K8s   │  │ Token: GH    │  │ Tokens: ∅    │                  │ │
│  │  │ Tool View:   │  │ Tool View:   │  │ Tool View:   │                  │ │
│  │  │  - K8s tools │  │  - GH tools  │  │  - auth_k8s  │                  │ │
│  │  │              │  │  - auth_k8s  │  │  - auth_gh   │                  │ │
│  │  └──────────────┘  └──────────────┘  └──────────────┘                  │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                    │                                         │
│                                    ▼                                         │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                 Session-Aware Registry                                  │ │
│  │                                                                         │ │
│  │  Global Servers:                 Per-Session Connections:               │ │
│  │  ┌─────────────────────────┐    ┌────────────────────────────────┐     │ │
│  │  │ mcp-kubernetes (OAuth)  │ ←──│ Session A: Connected (token)   │     │ │
│  │  │ mcp-github (OAuth)      │ ←──│ Session B: Connected (token)   │     │ │
│  │  │ mcp-local (no auth)     │    │ Session C: Pending auth        │     │ │
│  │  └─────────────────────────┘    └────────────────────────────────┘     │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

2. Key Concepts

2.1 Server Registration Types

Servers are registered with the aggregator in two categories:

Type Authentication Tool Visibility Example
Global None required Visible to all sessions mcp-local, core tools
Session-Scoped OAuth required Visible only after user authenticates mcp-kubernetes, mcp-github

2.2 Session Tool View

Each session maintains its own "tool view" that is computed dynamically:

SessionToolView = GlobalTools + AuthenticatedServerTools + SyntheticAuthTools

Where: - GlobalTools: Tools from servers that don't require authentication - AuthenticatedServerTools: Tools from OAuth-protected servers where the user has a valid token - SyntheticAuthTools: authenticate_<server> tools for OAuth-protected servers the user hasn't authenticated with yet

2.3 Per-Session MCP Clients

For OAuth-protected servers, each session needs its own MCP client connection because:

  1. OAuth tokens are user-specific and may grant different permissions
  2. The remote MCP server may expose different tools based on the user's identity
  3. Connection state (ping, health) is per-user
Session A → MCP Client A (token: user-a-token) → mcp-kubernetes
Session B → MCP Client B (token: user-b-token) → mcp-kubernetes

3. Implementation Architecture

3.1 Session Registry

// SessionRegistry manages per-session state for OAuth-protected servers.
type SessionRegistry struct {
    mu       sync.RWMutex
    sessions map[string]*SessionState  // sessionID -> state
}

// SessionState holds per-session connection state.
type SessionState struct {
    SessionID      string
    CreatedAt      time.Time
    LastActivity   time.Time

    // Per-server connection state for this session
    Connections map[string]*SessionConnection  // serverName -> connection
}

// SessionConnection represents a session's connection to a specific server.
type SessionConnection struct {
    ServerName  string
    Status      ConnectionStatus  // connected, pending_auth, failed
    Client      MCPClient         // Session-specific MCP client (with user's token)
    Token       *oauth.Token      // User's OAuth token for this server
    Tools       []mcp.Tool        // Tools available to THIS user (may differ by permissions)
    Resources   []mcp.Resource
    Prompts     []mcp.Prompt
}

3.2 Tool Resolution Flow

When a session requests the tool list (tools/list):

1. Start with global tools (no auth required)
   └── Core tools, local MCP servers, etc.

2. For each OAuth-protected server:
   ├── If session has valid token AND connection:
   │   └── Include server's tools (from session-specific client)
   └── Else:
       └── Include synthetic authenticate_<server> tool

3. Return aggregated tool list specific to this session

3.3 Tool Execution Flow

When a session executes a tool:

1. Identify the target server from the tool name

2. If server requires OAuth:
   ├── Get session-specific connection
   ├── Verify token is valid
   └── Execute via session's MCP client (with user's token)

3. If server is global:
   └── Execute via shared MCP client (no auth needed)

4. MCP Protocol Considerations

4.1 Session Identification

The mcp-go library provides session IDs for HTTP-based transports:

session := mcpserver.ClientSessionFromContext(ctx)
sessionID := session.SessionID()  // UUID per connection

Each MCP protocol message handler MUST use the session context to determine which user's view to present.

4.2 Tool List Changed Notifications

When a user completes OAuth authentication:

1. Create session-specific MCP client with the new token
2. Fetch tools from the remote server (may be user-specific!)
3. Update session's tool view
4. Send tools/list_changed notification to THAT session only

This requires extending the aggregator to send targeted notifications:

// Current: Broadcast to all sessions
a.mcpServer.NotifyToolsListChanged()

// New: Notify specific session
a.NotifySessionToolsChanged(sessionID)

4.3 Lazy Tool Discovery

Since we can't know what tools a user has access to until they authenticate, the flow becomes:

Initial connection:
└── User sees: core_tools + authenticate_mcp_kubernetes + authenticate_mcp_github

After authenticating with mcp-kubernetes:
└── User sees: core_tools + kubernetes_* tools + authenticate_mcp_github

After authenticating with mcp-github:
└── User sees: core_tools + kubernetes_* tools + github_* tools

5. Token Injection for Tool Calls

When forwarding tool calls to remote OAuth-protected servers, the aggregator must inject the user's token from the session:

func (a *AggregatorServer) forwardToolCall(ctx context.Context, serverName, toolName string, args map[string]any) (*mcp.CallToolResult, error) {
    sessionID := getSessionIDFromContext(ctx)

    // Get session-specific connection
    conn := a.sessionRegistry.GetConnection(sessionID, serverName)
    if conn == nil || conn.Status != StatusConnected {
        return nil, fmt.Errorf("not authenticated with %s", serverName)
    }

    // The session's client already has the token configured
    return conn.Client.CallTool(ctx, toolName, args)
}

6. Resource Sharing vs Isolation

Different resources have different sharing models:

Resource Sharing Model Reason
Global server connections Shared No auth, same for all users
OAuth tokens Per-session User-specific credentials
MCP client connections Per-session (OAuth servers) Token embedded, may have different permissions
Tool lists Per-session May differ based on user permissions
OAuth metadata cache Shared Same IdP metadata for all users

7. Session Lifecycle

7.1 Session Creation

Sessions are created implicitly when a new MCP connection is established:

func (a *AggregatorServer) handleInitialize(ctx context.Context, req mcp.InitializeRequest) {
    sessionID := getSessionIDFromContext(ctx)

    // Create session state
    a.sessionRegistry.CreateSession(sessionID)

    // Register unauthenticated session connections for OAuth servers
    for _, server := range a.getOAuthServers() {
        a.sessionRegistry.SetPendingAuth(sessionID, server.Name)
    }
}

7.2 Session Cleanup

Sessions should be cleaned up when: - MCP connection is closed - Session has been idle for a configurable timeout (e.g., 30 minutes) - Server is shutting down

func (a *AggregatorServer) cleanupSession(sessionID string) {
    state := a.sessionRegistry.GetSession(sessionID)
    if state == nil {
        return
    }

    // Close session-specific MCP client connections
    for _, conn := range state.Connections {
        if conn.Client != nil {
            conn.Client.Close()
        }
    }

    // Remove session state
    a.sessionRegistry.DeleteSession(sessionID)

    // Optionally: Revoke OAuth tokens (if supported by IdP)
}

8. Backward Compatibility

To maintain backward compatibility with existing deployments:

  1. No OAuth servers configured: Behavior unchanged; all tools visible to all users.

  2. OAuth disabled for a server: Server's tools are globally visible (existing behavior).

  3. Mixed mode: Global servers work as before; only OAuth-protected servers get session-scoped visibility.

9. SSO Mechanisms

The SSO mechanisms (ADR 004) work with session-scoped connections:

Token Forwarding: When muster is protected by OAuth and forwardToken: true is set, muster forwards its ID token to downstream servers that trust muster's client ID.

Token Exchange: For cross-cluster SSO where clusters have separate IdPs, muster exchanges its local token for one valid on the remote cluster's IdP using RFC 8693 Token Exchange.

10. Diagram: Complete OAuth + Session Flow

┌─────────────────────────────────────────────────────────────────────────────────┐
│                                muster agent                                      │
│                         (Cursor -> stdio -> Agent)                               │
└────────────────────────────────────────┬────────────────────────────────────────┘
                                         │ HTTP/SSE
┌─────────────────────────────────────────────────────────────────────────────────┐
│                                muster server                                     │
│                                                                                  │
│  1. Agent connects → SessionID assigned (abc-123)                               │
│                                                                                  │
│  2. tools/list request from session abc-123:                                    │
│     ┌─────────────────────────────────────────────────────────────────────────┐ │
│     │ Tool View for Session abc-123:                                          │ │
│     │ ├── core_workflow_list          (global)                                │ │
│     │ ├── core_service_list           (global)                                │ │
│     │ ├── authenticate_mcp_kubernetes (synthetic - not authenticated yet)     │ │
│     │ └── authenticate_mcp_github     (synthetic - not authenticated yet)     │ │
│     └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                                  │
│  3. User calls authenticate_mcp_kubernetes:                                     │
│     → Returns OAuth URL: https://dex.example.com/authorize?...                  │
│     → User completes browser auth flow                                          │
│     → Callback stores token for session abc-123                                 │
│                                                                                  │
│  4. Session abc-123 upgraded:                                                   │
│     → Create MCP client with user's token                                       │
│     → Fetch tools from mcp-kubernetes                                           │
│     → Update session tool view                                                  │
│     → Send tools/list_changed to session abc-123                                │
│     ┌─────────────────────────────────────────────────────────────────────────┐ │
│     │ Tool View for Session abc-123 (updated):                                │ │
│     │ ├── core_workflow_list          (global)                                │ │
│     │ ├── core_service_list           (global)                                │ │
│     │ ├── kubernetes_list_pods        (session-specific - authenticated!)     │ │
│     │ ├── kubernetes_get_deployment   (session-specific)                      │ │
│     │ ├── kubernetes_logs             (session-specific)                      │ │
│     │ └── authenticate_mcp_github     (synthetic - still not authenticated)   │ │
│     └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                                  │
│  5. Meanwhile, Session xyz-789 (different user) connects:                        │
│     ┌─────────────────────────────────────────────────────────────────────────┐ │
│     │ Tool View for Session xyz-789:                                          │ │
│     │ ├── core_workflow_list          (global)                                │ │
│     │ ├── core_service_list           (global)                                │ │
│     │ ├── authenticate_mcp_kubernetes (synthetic - this user hasn't authed)   │ │
│     │ └── authenticate_mcp_github     (synthetic)                             │ │
│     └─────────────────────────────────────────────────────────────────────────┘ │
│     Note: Session xyz-789 does NOT see kubernetes tools!                        │
└─────────────────────────────────────────────────────────────────────────────────┘

Consequences

Benefits

  1. True multi-user support: Each user sees only the tools they have access to.

  2. Per-user permissions: Remote MCP servers can expose different tools based on user identity.

  3. Security isolation: Users cannot see or call tools from servers they haven't authenticated with.

  4. Incremental authentication: Users can authenticate with servers as needed, building up their tool set.

  5. Audit trail: Tool access is traceable to specific authenticated users.

Costs

  1. Increased complexity: The aggregator now manages per-session state, not just global state.

  2. Memory overhead: Each session maintains its own MCP client connections for OAuth servers.

  3. Connection scaling: N users × M OAuth servers = N×M potential connections.

  4. Protocol extension: Need to target notifications to specific sessions.

  5. Tool list latency: First tool list for a new session requires checking all server auth states.

Mitigations

Cost Mitigation
Memory overhead Session idle timeout, lazy connection establishment
Connection scaling Connection pooling, shared connections for same-token users
Tool list latency Cache global tools, async session connection establishment
Protocol complexity Abstract behind SessionAwareRegistry interface

Implementation Steps

  1. Create SessionRegistry: Implement session-scoped state management alongside the existing global registry.

  2. Extend token storage: Link tokens to session connections, not just session IDs.

  3. Session-aware tool listing: Modify GetAllTools() to accept a session context and compute per-session views.

  4. Per-session MCP clients: Create and manage MCP client instances per session for OAuth servers.

  5. Targeted notifications: Extend the MCP server wrapper to send notifications to specific sessions.

  6. Session cleanup: Implement idle session detection and cleanup with connection pooling.

  7. Testing: Add comprehensive tests for multi-session scenarios with different auth states.

Open Questions

  1. Connection pooling: Can we share MCP client connections when multiple users have the same token (e.g., service accounts)?

  2. Token refresh coordination: When a token is refreshed, how do we update all session connections using it?

  3. Stdio transport: Stdio is inherently single-user; should we skip session management entirely for stdio?

  4. Tool list caching: How long can we cache a session's tool list before re-checking server connections?