008. Explicit Authentication State¶
Status¶
Accepted
Context¶
The Problem¶
When the muster agent connects to a muster server, some remote MCP servers may require OAuth authentication. Currently, the agent detects this by scanning tool names for _authenticate suffixes (e.g., looking for server_authenticate tools in the tool list).
This is fragile because: 1. Information Loss: The tool name doesn't carry issuer URL, scope, or SSO hints 2. Pattern Matching: Relies on naming conventions that could break 3. No SSO Intelligence: Can't detect that two servers use the same IdP 4. Requires Explicit Query: AI must know to look for auth tools 5. Pollutes Server Namespace: Synthetic auth tools are injected into each server's tool list
Current Flow (Fragile)¶
1. AI calls list_tools
2. Agent scans tool names for "_authenticate" suffix
3. Agent infers: "server_X needs auth" (loses issuer info)
4. AI must call x_server_X_authenticate without knowing if SSO is possible
Desired Flow (Proactive)¶
1. AI calls any tool (e.g., list_tools, call_tool)
2. Response includes _meta with auth status:
- server_X: needs auth, issuer=https://dex.example.com
- server_Y: needs auth, issuer=https://dex.example.com (same issuer = SSO!)
3. AI sees this in EVERY response and can act on it proactively
4. AI calls core_auth_login with server parameter to authenticate
Decision¶
Core Principle¶
Authentication is a muster platform concern, not an MCP server concern.
MCP servers should expose their actual tools unchanged. muster handles authentication orchestration through:
- The auth://status resource for status (data/context)
- Core tools for actions (core_auth_login, core_auth_logout)
This follows MCP philosophy:
- Resources are application-driven - the muster agent reads auth://status and decides how to present it to the model
- Tools are model-controlled - the AI can invoke core_auth_login when it decides authentication is needed
MCP _meta Specification¶
Per the MCP specification, _meta is reserved for additional metadata. We use a namespaced key following the format:
This follows the MCP key naming rules:
- Prefix: giantswarm.io/ (our namespace)
- Name: auth_required (alphanumeric with underscores)
Architecture¶
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ User → LLM (Cursor) → MCP Client (IDE) │
│ │ │
│ │ calls tools │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ muster agent (MCP Server to IDE) │ │
│ │ │ │
│ │ - Exposes core tools to IDE │ │
│ │ - Polls auth://status from aggregator │ │
│ │ - Adds _meta to every response │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ MCP Client connection │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ muster Aggregator (MCP Server) │ │
│ │ │ │
│ │ Resources: │ │
│ │ auth://status → Auth state for all servers │ │
│ │ │ │
│ │ Core Tools: │ │
│ │ core_auth_login → Initiate OAuth for a server │ │
│ │ core_auth_logout → Clear session for a server │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ gitlab │ │ jira │ │ github │ │
│ │ (OAuth) │ │ (OAuth) │ │ (no auth) │ │
│ │ │ │ │ │ │ │
│ │ Tools: │ │ Tools: │ │ Tools: │ │
│ │ - create_mr │ │ - create_tkt │ │ - create_pr │ │
│ │ - list_repos │ │ - search │ │ - list_repos │ │
│ │ (pure, no │ │ (pure, no │ │ │ │
│ │ synthetic) │ │ synthetic) │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1. Auth Status Resource (Aggregator)¶
The aggregator exposes auth://status as an MCP resource.
// internal/aggregator/auth_resource.go
const AuthStatusResourceURI = "auth://status"
type AuthStatusResponse struct {
Servers []ServerAuthStatus `json:"servers"`
}
type ServerAuthStatus struct {
Name string `json:"name"`
Status string `json:"status"` // "connected", "auth_required", "disconnected"
Issuer string `json:"issuer,omitempty"`
Scope string `json:"scope,omitempty"`
Error string `json:"error,omitempty"`
}
2. Core Auth Tools (Aggregator)¶
The aggregator exposes core tools for authentication actions.
| Tool | Description | Input Schema |
|---|---|---|
core_auth_login |
Initiate OAuth login flow for a specific MCP server | { "server": string } |
core_auth_logout |
Clear authentication session for a specific MCP server | { "server": string } |
// internal/api/core_auth.go
// core_auth_login initiates OAuth for the specified server
// Returns the OAuth URL for the user to open in their browser
func handleCoreAuthLogin(ctx context.Context, server string) (*mcp.CallToolResult, error) {
// Trigger OAuth flow for the server
// Return URL for user to complete authentication
}
// core_auth_logout clears the session for the specified server
func handleCoreAuthLogout(ctx context.Context, server string) (*mcp.CallToolResult, error) {
// Clear session tokens for the server
// Server tools will be hidden until re-authentication
}
3. Auth Status Polling and Caching (Agent)¶
The agent polls auth://status periodically and caches the result.
// internal/agent/auth_poller.go
const AuthPollInterval = 30 * time.Second
type AuthRequiredInfo struct {
Server string `json:"server"`
Issuer string `json:"issuer"`
Scope string `json:"scope,omitempty"`
}
func (s *MCPServer) pollAuthStatus(ctx context.Context) {
resource, err := s.musterClient.GetResource(ctx, "auth://status")
if err != nil {
return // Silently continue with cached data
}
var status AuthStatusResponse
if err := json.Unmarshal([]byte(resource.Contents), &status); err != nil {
return
}
var authRequired []AuthRequiredInfo
for _, srv := range status.Servers {
if srv.Status == "auth_required" {
authRequired = append(authRequired, AuthRequiredInfo{
Server: srv.Name,
Issuer: srv.Issuer,
Scope: srv.Scope,
})
}
}
s.authCacheMu.Lock()
s.authRequiredCache = authRequired
s.authCacheMu.Unlock()
}
4. Adding Auth Notification to Tool Responses (Agent)¶
Every tool response includes:
1. Human-readable notification in content (tells AI what to do)
2. Structured data in _meta (for programmatic use)
// internal/agent/server_mcp_handlers.go
func (s *MCPServer) wrapToolResult(result *mcp.CallToolResult) *mcp.CallToolResult {
s.authCacheMu.RLock()
authRequired := s.authRequiredCache
s.authCacheMu.RUnlock()
if len(authRequired) == 0 {
return result
}
// Add human-readable notification
notification := s.buildAuthNotification(authRequired)
result.Content = append(result.Content, mcp.TextContent{
Type: "text",
Text: notification,
})
// Add structured data in _meta
if result.Meta == nil {
result.Meta = make(map[string]interface{})
}
result.Meta["giantswarm.io/auth_required"] = authRequired
return result
}
func (s *MCPServer) buildAuthNotification(authRequired []AuthRequiredInfo) string {
var sb strings.Builder
sb.WriteString("\n---\n")
sb.WriteString("Authentication Required:\n")
for _, auth := range authRequired {
sb.WriteString(fmt.Sprintf("- %s: use core_auth_login tool with server='%s'\n",
auth.Server, auth.Server))
}
// Group by issuer for SSO hints
issuerServers := make(map[string][]string)
for _, auth := range authRequired {
issuerServers[auth.Issuer] = append(issuerServers[auth.Issuer], auth.Server)
}
for issuer, servers := range issuerServers {
if len(servers) > 1 {
sb.WriteString(fmt.Sprintf("\nNote: %s use the same identity provider (%s). ",
strings.Join(servers, " and "), issuer))
sb.WriteString("Signing in to one will authenticate all of them.\n")
}
}
return sb.String()
}
5. Tool Visibility¶
| Server State | What's Visible |
|---|---|
auth_required (session not authenticated) |
No tools from that server |
connected (session authenticated) |
All actual tools from that server, unchanged |
| Core tools | Always visible (core_auth_*, core_mcpserver_*, etc.) |
No synthetic x_<server>_authenticate tools are injected. Server tool lists remain pure.
Example Tool Response¶
When the AI calls any tool, the response includes clear instructions:
{
"content": [
{
"type": "text",
"text": "Available tools: github_create_issue, github_list_repos, ..."
},
{
"type": "text",
"text": "\n---\nAuthentication Required:\n- gitlab: use core_auth_login tool with server='gitlab'\n- jira: use core_auth_login tool with server='jira'\n\nNote: gitlab and jira use the same identity provider (dex.example.com). Signing in to one will authenticate all of them.\n"
}
],
"_meta": {
"giantswarm.io/auth_required": [
{
"server": "gitlab",
"issuer": "https://dex.example.com"
},
{
"server": "jira",
"issuer": "https://dex.example.com"
}
]
}
}
What the AI Sees and Does¶
The AI reads the human-readable notification and understands:
- What needs auth: "gitlab and jira require authentication"
- How to authenticate: "use core_auth_login tool with server parameter"
- SSO opportunity: "same identity provider, sign in once for both"
Typical AI response to user:
"I notice that gitlab and jira require authentication before I can use their tools. Good news: they use the same identity provider, so you only need to sign in once. Would you like me to start the authentication process?"
When the user agrees, the AI invokes the tool:
The tool returns an OAuth URL for the user to open in their browser.
What This ADR Does NOT Include¶
Previous iterations proposed complexity that is not needed:
- Synthetic per-server authenticate tools: Replaced by
core_auth_login - Shared pkg/oauth package: Code deduplication is a separate concern
- Issuer-keyed agent token storage: Nice-to-have but not required for explicit auth state
- Agent-initiated SSO: The AI can orchestrate SSO based on the issuer info
Implementation Steps¶
- Add
core_auth_logintool tointernal/api/ - Add
core_auth_logouttool tointernal/api/ - Remove synthetic
x_<server>_authenticatetool injection - Remove
IsSyntheticAuthTool()and related logic - Update
_metanotifications to referencecore_auth_login - Ensure tool visibility hides unauthenticated server tools (no synthetic replacement)
Consequences¶
Positive¶
- Clean Separation: MCP servers expose only their actual capabilities
- Stable Interface:
core_auth_*tools are always available, predictable - Discoverable: Auth is in the
core_*namespace where users expect muster functionality - Logout Support: Natural place for session management via
core_auth_logout - Consistent Patterns: Matches
core_mcpserver_*,core_config_* - Simpler Implementation: No synthetic tool injection/hiding logic
- SSO Awareness: AI can see which servers share an issuer and suggest SSO
- MCP Compliant: Uses standard
_metafield per specification
Negative¶
- Polling Overhead: Agent polls server every 30 seconds (lightweight, ~100 bytes)
- Response Size: Every response includes auth info (negligible for typical 0-3 servers)
Future Enhancements¶
If needed later: - Agent-side issuer-keyed token storage for automatic SSO - Server-side SSE notifications when auth state changes (avoid polling)
Related Decisions¶
- ADR-004: OAuth Proxy - Server-side OAuth implementation
- ADR-005: muster Auth - Agent-side OAuth implementation
- ADR-006: Session-Scoped Tool Visibility - Auth affects tool visibility