Skip to content

Commit 6ccc07c

Browse files
feat: auto-detect RFC 8707 resource parameter for OAuth flows (smart-mcp-proxy#188)
* Create spec * docs: add RFC 8707 resource auto-detection specification and plan Related smart-mcp-proxy#165 Add comprehensive specification and implementation plan for automatic detection of the RFC 8707 resource parameter in OAuth flows. This enables zero-config OAuth for providers like Runlayer that require the resource parameter. Artifacts: - spec.md: Feature specification with user stories and requirements - plan.md: Implementation plan with technical context - research.md: Technical research and implementation approach - data-model.md: Entity definitions and state transitions - quickstart.md: Usage guide and troubleshooting - checklists/requirements.md: Specification quality checklist Key Decisions: - Extract resource from RFC 9728 Protected Resource Metadata - Fallback to server URL if metadata unavailable - Inject params into auth URL after mcp-go constructs it - Manual extra_params override auto-detected values * docs: add implementation tasks for RFC 8707 resource auto-detection Related smart-mcp-proxy#165 Add structured task list with 39 tasks organized by user story: - Phase 1: Setup (1 task) - Phase 2: Foundational - discovery layer (5 tasks) - Phase 3: US1 - Zero-config auto-detection (15 tasks) - MVP - Phase 4: US2 - Manual override (6 tasks) - Phase 5: US3 - Token injection (4 tasks) - Phase 6: US4 - Diagnostic visibility (3 tasks) - Phase 7: Polish (5 tasks) Tasks follow TDD approach per constitution requirements. * feat: add DiscoverProtectedResourceMetadata for RFC 8707 resource detection Related smart-mcp-proxy#165 Add new function that returns the full RFC 9728 Protected Resource Metadata struct including the 'resource' field needed for RFC 8707 compliance. Refactor DiscoverScopesFromProtectedResource to delegate to new function. Changes: - Add DiscoverProtectedResourceMetadata() returning *ProtectedResourceMetadata - Refactor DiscoverScopesFromProtectedResource() as wrapper for backward compat - Add comprehensive unit tests for new function Testing: - All TestDiscoverProtectedResourceMetadata_* tests pass - Existing DiscoverScopesFromProtectedResource tests still pass * feat(oauth): implement RFC 8707 resource auto-detection (User Story 1) Add automatic detection of RFC 8707 resource parameter from Protected Resource Metadata (RFC 9728). This enables zero-config OAuth with providers like Runlayer that require the resource parameter. Key changes: - Add CreateOAuthConfigWithExtraParams() that returns both OAuth config and auto-detected extra params including resource - Add autoDetectResource() helper that: - Makes preflight HEAD request to get WWW-Authenticate header - Extracts resource_metadata URL - Fetches Protected Resource Metadata - Uses metadata.resource or falls back to server URL - Update handleOAuthAuthorization() to accept extraParams and inject them into authorization URL - Update all 6 call sites to use new function and pass extraParams Tests: - TestCreateOAuthConfig_AutoDetectsResource: verifies resource extraction - TestCreateOAuthConfig_FallsBackToServerURL: verifies fallback behavior - E2E tests in e2e_oauth_zero_config_test.go Part of smart-mcp-proxy#165 (RFC 8707 resource auto-detection for zero-config OAuth) * feat(oauth): add tests for manual extra_params override (US2) Tests verify that: - T022: Manual extra_params.resource overrides auto-detected value - T023: Manual extra_params are preserved while resource is auto-detected Implementation was already complete from US1: - T024-T026: Merge logic and logging in CreateOAuthConfigWithExtraParams All tests pass: go test ./internal/oauth/... -v -run TestCreateOAuthConfig * feat(oauth): pass auto-detected extraParams to transport wrapper (US3) Enable auto-detected resource parameter injection into token requests: - T029: OAuthTransportWrapper.injectFormParams() handles token exchange/refresh - T030: createOAuthConfigInternal() accepts extraParams for wrapper injection - T031: Existing TestInjectFormParams_TokenRequest covers token request injection Key changes: - CreateOAuthConfig() now delegates to createOAuthConfigInternal() - CreateOAuthConfigWithExtraParams() passes auto-detected params to internal fn - Transport wrapper uses passed extraParams instead of re-reading from config This ensures zero-config OAuth flows inject resource into all OAuth requests: - Authorization URL (via handleOAuthAuthorization) - Token exchange (via transport wrapper) - Token refresh (via transport wrapper) * feat(oauth): add RFC 8707 resource visibility to diagnostics (US4) * fix(oauth): use POST for resource auto-detection per MCP spec MCP spec only requires POST support for the main endpoint. Use POST directly for the preflight request to get WWW-Authenticate header with resource_metadata URL. Updated all tests to use POST method in mock handlers. * fix(oauth): address code review feedback - add timeout, clarify comments, clean tests * docs: add zero-config vs explicit OAuth examples, improve auth status output
1 parent f0ca376 commit 6ccc07c

18 files changed

Lines changed: 2116 additions & 55 deletions

File tree

CLAUDE.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,12 +565,51 @@ The `working_dir` field specifies the working directory for stdio MCP servers. U
565565
- If directory doesn't exist, server startup fails with detailed error
566566
- Compatible with Docker isolation (`isolation.working_dir` for container path)
567567

568+
### Zero-Config OAuth with Resource Auto-Detection (RFC 8707/9728)
569+
570+
MCPProxy automatically detects and injects the RFC 8707 `resource` parameter for OAuth providers like Runlayer. This enables zero-configuration OAuth for servers advertising Protected Resource Metadata (RFC 9728).
571+
572+
**How it works**:
573+
1. MCPProxy sends a preflight HEAD request to the MCP server URL
574+
2. If server returns 401 with `WWW-Authenticate` header containing `resource_metadata` URL, MCPProxy fetches the Protected Resource Metadata
575+
3. The `resource` field from metadata is automatically injected into OAuth authorization URL and token requests
576+
4. If metadata doesn't contain `resource`, MCPProxy falls back to using the server URL
577+
578+
**Zero-Config Example (Runlayer)**:
579+
```json
580+
{
581+
"mcpServers": [
582+
{
583+
"name": "runlayer-slack",
584+
"url": "https://oauth.runlayer.com/api/v1/proxy/abc123def/mcp",
585+
"protocol": "http",
586+
"enabled": true
587+
// No OAuth config needed! Resource parameter auto-detected from metadata
588+
}
589+
]
590+
}
591+
```
592+
593+
**Priority Order for Resource Parameter**:
594+
1. Manual `extra_params.resource` in config (highest priority - preserves backward compatibility)
595+
2. Auto-detected resource from RFC 9728 Protected Resource Metadata
596+
3. Fallback to server URL if metadata unavailable or lacks resource field
597+
598+
**Diagnostic Commands**:
599+
```bash
600+
# View auto-detected resource parameter
601+
mcpproxy auth status --server=runlayer-slack
602+
603+
# Check for OAuth issues including resource detection
604+
mcpproxy doctor
605+
```
606+
568607
### OAuth Extra Parameters Configuration
569608

570-
MCPProxy supports arbitrary OAuth 2.0/2.1 extra parameters via the `extra_params` field in OAuth configuration. This enables integration with OAuth providers requiring non-standard parameters like RFC 8707 resource indicators.
609+
MCPProxy also supports manual `extra_params` for OAuth providers requiring non-standard parameters. Manual params override auto-detected values.
571610

572611
**Use Cases**:
573-
- **RFC 8707 Resource Indicators**: Specify target resource servers for multi-tenant authorization
612+
- **RFC 8707 Resource Indicators**: Override auto-detected resource for multi-tenant authorization
574613
- **Audience-Restricted Tokens**: Request tokens for specific API audiences
575614
- **Tenant Identification**: Pass tenant/organization identifiers for multi-tenant OAuth
576615
- **Custom Provider Extensions**: Support proprietary OAuth extensions from specific providers
@@ -988,6 +1027,8 @@ When making changes to this codebase, ensure you understand the modular architec
9881027
- BBolt embedded database (`~/.mcpproxy/config.db`) - `oauth_tokens` bucke (009-proactive-oauth-refresh)
9891028
- Bash (GitHub Actions), Go 1.25 (existing project) + curl, jq, GitHub Actions, Anthropic Messages API (010-release-notes-generator)
9901029
- N/A (ephemeral workflow artifacts only) (010-release-notes-generator)
1030+
- Go 1.24.0 + mcp-go (OAuth transport), zap (logging), BBolt (storage) (011-resource-auto-detect)
1031+
- BBolt embedded database (`~/.mcpproxy/config.db`) for OAuth tokens (011-resource-auto-detect)
9911032

9921033
## Recent Changes
9931034
- 003-tool-annotations-webui: Added Go 1.21+, TypeScript/Vue 3

README.md

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -559,9 +559,11 @@ docker ps -a --filter label=com.mcpproxy.managed=true -q | xargs docker rm -f
559559

560560
## OAuth Authentication Support
561561

562-
MCPProxy provides **seamless OAuth 2.1 authentication** for MCP servers that require user authorization (like Cloudflare AutoRAG, GitHub, etc.):
562+
MCPProxy provides **seamless OAuth 2.1 authentication** for MCP servers that require user authorization (like Cloudflare AutoRAG, Runlayer, GitHub, etc.):
563563

564564
### **Key Features**
565+
- **Zero-Config OAuth**: Automatic detection and configuration for most OAuth servers
566+
- **RFC 8707 Resource Auto-Detection**: Automatically discovers resource parameters from server metadata
565567
- **RFC 8252 Compliant**: Dynamic port allocation for secure callback handling
566568
- **PKCE Security**: Proof Key for Code Exchange for enhanced security
567569
- **Auto Browser Launch**: Opens your default browser for authentication
@@ -570,38 +572,88 @@ MCPProxy provides **seamless OAuth 2.1 authentication** for MCP servers that req
570572

571573
### 🔄 **How It Works**
572574
1. **Add OAuth Server**: Configure an OAuth-enabled MCP server in your config
573-
2. **Auto Authentication**: MCPProxy detects when OAuth is required (401 response)
575+
2. **Auto Detection**: MCPProxy detects OAuth requirements and auto-configures parameters
574576
3. **Browser Opens**: Your default browser opens to the OAuth provider's login page
575577
4. **Dynamic Callback**: MCPProxy starts a local callback server on a random port
576578
5. **Token Exchange**: Authorization code is automatically exchanged for access tokens
577579
6. **Ready to Use**: Server becomes available for tool calls immediately
578580

579-
### 📝 **OAuth Server Configuration**
581+
### 📝 **OAuth Configuration Examples**
580582

581-
> **Note**: The `"oauth"` configuration is **optional**. MCPProxy will automatically detect when OAuth is required and use sensible defaults in most cases. You only need to specify OAuth settings if you want to customize scopes or have pre-registered client credentials.
583+
#### Zero-Config OAuth (Recommended)
582584

583-
```jsonc
585+
For most OAuth servers (including Runlayer, Cloudflare, etc.), **no OAuth configuration is needed**. MCPProxy automatically:
586+
- Detects OAuth requirements via 401 responses
587+
- Discovers Protected Resource Metadata (RFC 9728)
588+
- Injects the RFC 8707 `resource` parameter automatically
589+
590+
```json
584591
{
585592
"mcpServers": [
593+
{
594+
"name": "runlayer-slack",
595+
"url": "https://oauth.runlayer.com/api/v1/proxy/YOUR-UUID/mcp"
596+
},
586597
{
587598
"name": "cloudflare_autorag",
588599
"url": "https://autorag.mcp.cloudflare.com/mcp",
589-
"protocol": "streamable-http",
590-
"enabled": true,
600+
"protocol": "streamable-http"
601+
}
602+
]
603+
}
604+
```
605+
606+
That's it - **no `oauth` block needed**. MCPProxy handles everything automatically.
607+
608+
#### Explicit OAuth Configuration
609+
610+
Use explicit configuration when you need to:
611+
- Customize OAuth scopes
612+
- Override auto-detected parameters
613+
- Use pre-registered client credentials
614+
- Support providers with non-standard requirements
615+
616+
```json
617+
{
618+
"mcpServers": [
619+
{
620+
"name": "github-enterprise",
621+
"url": "https://github.example.com/mcp",
622+
"protocol": "http",
591623
"oauth": {
592-
"scopes": ["mcp.read", "mcp.write"],
593-
"pkce_enabled": true
624+
"scopes": ["repo", "user:email", "read:org"],
625+
"pkce_enabled": true,
626+
"client_id": "your-registered-client-id",
627+
"extra_params": {
628+
"resource": "https://api.github.example.com",
629+
"audience": "github-enterprise-api"
630+
}
594631
}
595632
}
596633
]
597634
}
598635
```
599636

600637
**OAuth Configuration Options** (all optional):
601-
- `scopes`: OAuth scopes to request (default: `["mcp.read", "mcp.write"]`)
638+
- `scopes`: OAuth scopes to request (auto-discovered if not specified)
602639
- `pkce_enabled`: Enable PKCE for security (default: `true`, recommended)
603-
- `client_id`: Pre-registered client ID (optional, uses Dynamic Client Registration if empty)
640+
- `client_id`: Pre-registered client ID (uses Dynamic Client Registration if empty)
604641
- `client_secret`: Client secret (optional, for confidential clients)
642+
- `extra_params`: Additional OAuth parameters (override auto-detected values)
643+
644+
### 🔍 **OAuth Diagnostics**
645+
646+
Check OAuth status and auto-detected parameters:
647+
```bash
648+
# View OAuth status for a specific server
649+
mcpproxy auth status --server=runlayer-slack
650+
651+
# View all OAuth-enabled servers
652+
mcpproxy auth status --all
653+
654+
# Run health diagnostics including OAuth issues
655+
mcpproxy doctor
656+
```
605657

606658
### 🔧 **OAuth Debugging**
607659

cmd/mcpproxy/auth_cmd.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -322,22 +322,52 @@ func runAuthStatusClientMode(ctx context.Context, dataDir, serverName string, al
322322
}
323323

324324
// Handle both map[string]string (from runtime) and map[string]interface{} (from conversions)
325+
var manualResource string
326+
var otherParams []string
325327
if extraParams, ok := oauth["extra_params"].(map[string]string); ok && len(extraParams) > 0 {
326-
paramParts := make([]string, 0, len(extraParams))
327328
for key, val := range extraParams {
328-
paramParts = append(paramParts, fmt.Sprintf("%s=%v", key, val))
329+
if key == "resource" {
330+
manualResource = val
331+
} else {
332+
otherParams = append(otherParams, fmt.Sprintf("%s=%v", key, val))
333+
}
329334
}
330-
fmt.Printf(" Extra Params: %s\n", strings.Join(paramParts, ", "))
331335
} else if extraParams, ok := oauth["extra_params"].(map[string]interface{}); ok && len(extraParams) > 0 {
332-
paramParts := make([]string, 0, len(extraParams))
333336
for key, val := range extraParams {
334-
paramParts = append(paramParts, fmt.Sprintf("%s=%v", key, val))
337+
if key == "resource" {
338+
manualResource = fmt.Sprintf("%v", val)
339+
} else {
340+
otherParams = append(otherParams, fmt.Sprintf("%s=%v", key, val))
341+
}
335342
}
336-
fmt.Printf(" Extra Params: %s\n", strings.Join(paramParts, ", "))
343+
}
344+
345+
// Show non-resource extra params
346+
if len(otherParams) > 0 {
347+
fmt.Printf(" Extra Params: %s\n", strings.Join(otherParams, ", "))
348+
}
349+
350+
// Show RFC 8707 resource parameter with actual URL
351+
serverURL, _ := srv["url"].(string)
352+
if manualResource != "" {
353+
fmt.Printf(" Resource: %s (manual)\n", manualResource)
354+
} else if serverURL != "" {
355+
// Resource will be auto-detected from RFC 9728 Protected Resource Metadata
356+
// Show server URL as the expected value (fallback if metadata unavailable)
357+
fmt.Printf(" Resource: Auto-detect → %s\n", serverURL)
358+
} else {
359+
fmt.Printf(" Resource: Auto-detect (RFC 9728)\n")
337360
}
338361
} else {
339362
// Server requires OAuth but has no explicit config (discovery/DCR)
340363
fmt.Printf(" OAuth: Discovered via Dynamic Client Registration\n")
364+
// RFC 8707 resource will be auto-detected for zero-config OAuth servers
365+
serverURL, _ := srv["url"].(string)
366+
if serverURL != "" {
367+
fmt.Printf(" Resource: Auto-detect → %s\n", serverURL)
368+
} else {
369+
fmt.Printf(" Resource: Auto-detect (RFC 9728)\n")
370+
}
341371
}
342372

343373
// Display token expiration if available
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Code Review: PR #188 - Auto-detect RFC 8707 Resource Parameter for OAuth
2+
3+
**Date**: 2025-12-10
4+
**PR**: https://github.com/smart-mcp-proxy/mcpproxy-go/pull/188
5+
**Reviewer**: Claude Code
6+
7+
## Overview
8+
9+
This PR implements automatic detection of the RFC 8707 `resource` parameter from RFC 9728 Protected Resource Metadata, enabling zero-configuration OAuth for providers like Runlayer/AnySource. This is a well-structured feature addition that improves user experience by eliminating manual `extra_params` configuration.
10+
11+
## Summary of Changes
12+
13+
| File | Type | Description |
14+
|------|------|-------------|
15+
| `internal/oauth/config.go` | New API | Added `CreateOAuthConfigWithExtraParams()` and `autoDetectResource()` |
16+
| `internal/oauth/discovery.go` | Enhancement | Added `DiscoverProtectedResourceMetadata()` |
17+
| `internal/upstream/core/connection.go` | Integration | Updated 4 call sites to use new API |
18+
| `cmd/mcpproxy/auth_cmd.go` | UX | Added resource status display |
19+
| `internal/management/diagnostics.go` | UX | Updated doctor resolution message |
20+
| `CLAUDE.md` | Docs | Comprehensive documentation |
21+
| Tests | Verification | Extensive unit and E2E tests |
22+
23+
---
24+
25+
## Positives
26+
27+
### 1. Clean API Design
28+
The new `CreateOAuthConfigWithExtraParams()` function returns both the config and extraParams, enabling the connection layer to inject params into the authorization URL. The backward-compatible `CreateOAuthConfig()` is preserved for existing callers.
29+
30+
### 2. Proper Priority Order
31+
```go
32+
// Resource auto-detection logic (in priority order):
33+
// 1. Manual extra_params.resource from config (highest priority)
34+
// 2. Auto-detected resource from RFC 9728 Protected Resource Metadata
35+
// 3. Fallback to server URL if metadata unavailable
36+
```
37+
38+
This correctly preserves backward compatibility - manual overrides always win.
39+
40+
### 3. Excellent Test Coverage
41+
- Unit tests for `DiscoverProtectedResourceMetadata()` with error cases
42+
- Unit tests for resource auto-detection and manual override
43+
- E2E test validating full metadata discovery flow
44+
- Tests for parameter merging behavior
45+
46+
### 4. Good Documentation
47+
- CLAUDE.md updated with clear explanation and examples
48+
- Inline code comments explain RFC references
49+
- Diagnostic commands documented for troubleshooting
50+
51+
---
52+
53+
## Issues and Suggestions
54+
55+
### 1. **Network Request in Config Creation** (Medium Risk) - FIXED
56+
57+
```go:internal/oauth/config.go
58+
func autoDetectResource(serverConfig *config.ServerConfig, logger *zap.Logger) string {
59+
resp, err := http.Post(serverConfig.URL, "application/json", strings.NewReader("{}"))
60+
```
61+
62+
**Concern**: The `CreateOAuthConfigWithExtraParams()` function makes a synchronous HTTP POST request during config creation. This can cause:
63+
- Slowdown during startup if the server is slow/unreachable
64+
- Potential timeout issues affecting connection initialization
65+
- No timeout specified on the `http.Post` call
66+
67+
**Fix**: Added 5-second timeout to preflight request client.
68+
69+
### 2. **Fallback Logic Inconsistency** (Low Risk) - FIXED
70+
71+
In `autoDetectResource()`:
72+
- On network error: returns `serverConfig.URL` (fallback)
73+
- On non-401 response: returns `""` (no resource)
74+
- On 401 without metadata URL: returns `serverConfig.URL` (fallback)
75+
76+
The non-401 case returning empty string seems correct (server doesn't require OAuth), but the behavior isn't immediately obvious from the code.
77+
78+
**Fix**: Added clarifying comment explaining why non-401 returns empty.
79+
80+
### 3. **Unused Variable in E2E Test** - FIXED
81+
82+
```go:internal/server/e2e_oauth_zero_config_test.go
83+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
84+
defer cancel()
85+
```
86+
87+
The `ctx` variable is created but never passed to the discovery function.
88+
89+
**Fix**: Removed unused context variable and related check.
90+
91+
### 4. **Minor: Unused import check** - FIXED
92+
93+
```go:internal/server/e2e_oauth_zero_config_test.go
94+
// compile-time check to silence unused import warning
95+
var _ = zap.NewNop
96+
```
97+
98+
**Fix**: Removed unused zap import and dummy reference.
99+
100+
---
101+
102+
## Security Considerations
103+
104+
**Positive**: The implementation correctly:
105+
- Respects manual configuration over auto-detected values
106+
- Masks sensitive params in logs (existing `maskExtraParams` function)
107+
- Uses standard HTTP methods for discovery
108+
109+
**Note**: The POST request with empty body `{}` to detect 401 is per MCP spec. Consider documenting this as it might appear suspicious in network logs.
110+
111+
---
112+
113+
## Performance Considerations
114+
115+
The preflight POST request adds latency to connection establishment. For servers that don't advertise RFC 9728 metadata, this is overhead with no benefit. Consider:
116+
- Caching metadata discovery results
117+
- Making discovery async (non-blocking)
118+
119+
However, this is likely acceptable for v1 since it only happens during initial connection.
120+
121+
---
122+
123+
## Verdict
124+
125+
**Approved with minor fixes applied.**
126+
127+
This is a well-implemented feature with good test coverage and documentation. The code follows project conventions and the API design is clean.
128+
129+
### Fixes Applied:
130+
1. Added timeout to `http.Post` call in `autoDetectResource()`
131+
2. Added clarifying comment for non-401 return behavior
132+
3. Removed unused `ctx` variable in E2E test
133+
4. Removed unused zap import workaround in E2E test

internal/management/diagnostics.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,9 @@ func (s *service) detectOAuthIssues(serversRaw []map[string]interface{}) []contr
211211
Issue: "OAuth provider parameter mismatch",
212212
Error: lastError,
213213
MissingParams: []string{paramName},
214-
Resolution: "This requires MCPProxy support for OAuth extra_params. " +
215-
"Track progress: https://github.com/smart-mcp-proxy/mcpproxy-go/issues",
214+
Resolution: "MCPProxy auto-detects RFC 8707 resource parameter from Protected Resource Metadata (RFC 9728). " +
215+
"Check detected values: mcpproxy auth status --server=" + serverName + ". " +
216+
"To override, add extra_params.resource to OAuth config.",
216217
DocumentationURL: "https://www.rfc-editor.org/rfc/rfc8707.html",
217218
})
218219
}

0 commit comments

Comments
 (0)