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¶
-
Different access levels: User A has access to production Kubernetes, User B only has access to staging.
-
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.
-
Private servers: Some MCP servers are only accessible to specific users based on their IdP group membership.
-
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:
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:
- OAuth tokens are user-specific and may grant different permissions
- The remote MCP server may expose different tools based on the user's identity
- 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:
-
No OAuth servers configured: Behavior unchanged; all tools visible to all users.
-
OAuth disabled for a server: Server's tools are globally visible (existing behavior).
-
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¶
-
True multi-user support: Each user sees only the tools they have access to.
-
Per-user permissions: Remote MCP servers can expose different tools based on user identity.
-
Security isolation: Users cannot see or call tools from servers they haven't authenticated with.
-
Incremental authentication: Users can authenticate with servers as needed, building up their tool set.
-
Audit trail: Tool access is traceable to specific authenticated users.
Costs¶
-
Increased complexity: The aggregator now manages per-session state, not just global state.
-
Memory overhead: Each session maintains its own MCP client connections for OAuth servers.
-
Connection scaling: N users × M OAuth servers = N×M potential connections.
-
Protocol extension: Need to target notifications to specific sessions.
-
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¶
-
Create SessionRegistry: Implement session-scoped state management alongside the existing global registry.
-
Extend token storage: Link tokens to session connections, not just session IDs.
-
Session-aware tool listing: Modify
GetAllTools()to accept a session context and compute per-session views. -
Per-session MCP clients: Create and manage MCP client instances per session for OAuth servers.
-
Targeted notifications: Extend the MCP server wrapper to send notifications to specific sessions.
-
Session cleanup: Implement idle session detection and cleanup with connection pooling.
-
Testing: Add comprehensive tests for multi-session scenarios with different auth states.
Open Questions¶
-
Connection pooling: Can we share MCP client connections when multiple users have the same token (e.g., service accounts)?
-
Token refresh coordination: When a token is refreshed, how do we update all session connections using it?
-
Stdio transport: Stdio is inherently single-user; should we skip session management entirely for stdio?
-
Tool list caching: How long can we cache a session's tool list before re-checking server connections?