docs/oauthserver_testing.md
This document describes how to use the OAuth test server for manual testing and E2E test development.
# Basic start on port 9000
go run ./tests/oauthserver/cmd/server -port 9000
# With longer token expiration (recommended for manual testing)
go run ./tests/oauthserver/cmd/server -port 9000 -access-token-ttl=1h
Add the OAuth test server to your ~/.mcpproxy/mcp_config.json:
{
"mcpServers": [
{
"name": "oauth-test-server",
"url": "http://127.0.0.1:9000/mcp",
"protocol": "streamable-http",
"enabled": true
}
]
}
# Via CLI (opens browser automatically)
mcpproxy auth login --server=oauth-test-server
# Or restart mcpproxy to trigger connection
mcpproxy upstream restart oauth-test-server
# Check server status
mcpproxy upstream list
# List available tools
mcpproxy tools list --server=oauth-test-server
When started, the server displays all configuration:
========================================
OAuth Test Server
========================================
Listening on: http://localhost:9000
Issuer: http://127.0.0.1:9000
Endpoints:
Authorization: http://127.0.0.1:9000/authorize
Token: http://127.0.0.1:9000/token
JWKS: http://127.0.0.1:9000/jwks.json
Discovery: http://127.0.0.1:9000/.well-known/oauth-authorization-server
Protected: http://127.0.0.1:9000/protected
DCR: http://127.0.0.1:9000/registration
Device Auth: http://127.0.0.1:9000/device_authorization
Features:
Auth Code: enabled
Device Code: enabled (RFC 8628)
DCR: enabled (RFC 7591)
Client Creds: enabled
Refresh Token: enabled
PKCE Required: enabled (RFC 7636)
Resource Req: disabled (RFC 8707)
Detection Mode: both
Test Credentials: testuser / testpass
========================================
-port 9000 # Listen port (default: 9000)
-no-auth-code # Disable authorization code flow
-no-device-code # Disable device code flow (RFC 8628)
-no-dcr # Disable dynamic client registration (RFC 7591)
-no-client-credentials # Disable client credentials flow
-no-refresh-token # Disable refresh tokens
-require-pkce=true # Require PKCE for auth code flow (default: true)
-require-resource=false # Require RFC 8707 resource indicator (default: false)
-runlayer-mode=false # Mimic Runlayer strict validation with Pydantic 422 errors
-access-token-ttl=1h # Access token expiry (default: 3m for testing)
-refresh-token-ttl=24h # Refresh token expiry (default: 24h)
Controls how OAuth discovery works:
-detection=discovery # Only via /.well-known/oauth-authorization-server
-detection=www-authenticate # Only via WWW-Authenticate header on /protected
-detection=explicit # Requires explicit OAuth config (no auto-detection)
-detection=both # Both discovery and WWW-Authenticate (default)
-token-error=invalid_client # Inject error on token endpoint
-token-error=invalid_grant
-token-error=invalid_scope
-token-error=server_error
-auth-error=access_denied # Inject error on authorization endpoint
-auth-error=invalid_request
go run ./tests/oauthserver/cmd/server -port 9000 \
-no-dcr \
-no-device-code \
-no-client-credentials
go run ./tests/oauthserver/cmd/server -port 9000 \
-access-token-ttl=24h \
-refresh-token-ttl=168h
go run ./tests/oauthserver/cmd/server -port 9000 \
-require-resource=true
Mimics Runlayer's strict OAuth validation with Pydantic-style 422 errors:
go run ./tests/oauthserver/cmd/server -port 9000 -runlayer-mode
When the resource parameter is missing, returns:
{
"detail": [
{
"type": "missing",
"loc": ["query", "resource"],
"msg": "Field required"
}
]
}
This mode implies -require-resource=true.
# Test invalid_grant error on token exchange
go run ./tests/oauthserver/cmd/server -port 9000 \
-token-error=invalid_grant
The login page features automatic submission for E2E testing:
testuser / testpassThis enables fully automated OAuth E2E tests without manual interaction.
The OAuth E2E tests use Playwright to automate browser-based OAuth flows:
# Run OAuth E2E test suite
./scripts/run-oauth-e2e.sh
# Or run directly with Playwright
OAUTH_SERVER_URL="http://127.0.0.1:9000" \
OAUTH_CLIENT_ID="test-client" \
MCPPROXY_URL="http://127.0.0.1:8085" \
MCPPROXY_API_KEY="test-api-key" \
npx playwright test tests/e2e/oauth.spec.ts
# Run OAuth server unit tests
go test ./tests/oauthserver/... -v
# Run integration tests (starts test server automatically)
OAUTH_INTEGRATION_TESTS=1 go test ./tests/oauthserver/... -run TestIntegration -v
curl -s http://127.0.0.1:9000/.well-known/oauth-authorization-server | jq .
curl -s -X POST http://127.0.0.1:9000/registration \
-H "Content-Type: application/json" \
-d '{
"client_name": "Test Client",
"redirect_uris": ["http://127.0.0.1/callback"],
"grant_types": ["authorization_code", "refresh_token"]
}' | jq .
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
curl -s -X POST http://127.0.0.1:9000/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials&scope=read write" | jq .
ACCESS_TOKEN="your-access-token"
# Should return 401 without token
curl -s -X POST http://127.0.0.1:9000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# Should succeed with token
curl -s -X POST http://127.0.0.1:9000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | jq .
# List tools
curl -s -X POST http://127.0.0.1:9000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | jq .
# Call echo tool
curl -s -X POST http://127.0.0.1:9000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"Hello"}}}' | jq .
# Call get_time tool
curl -s -X POST http://127.0.0.1:9000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_time","arguments":{}}}' | jq .
The OAuth test server exposes two simple tools for testing:
| Tool | Description | Arguments |
|---|---|---|
echo | Echoes back the input message | message (string, required) |
get_time | Returns current server time | none |
If the server remains disconnected after successful OAuth login:
-access-token-ttl=1hCheck these environment variables:
HEADLESS=true prevents browser openingNO_BROWSER=true prevents browser openingCI=true prevents browser openingThe server requires PKCE by default. Ensure your client:
curl http://127.0.0.1:9000/jwks.jsonFor lightweight testing of issue #271 (RFC 8707 resource parameter), there's also a Python/FastAPI mock server:
test/integration/oauth_runlayer_mock.py
# Using uv (recommended)
PORT=9000 uv run --with fastapi --with uvicorn python test/integration/oauth_runlayer_mock.py
# Using pip
pip install fastapi uvicorn
PORT=9000 python test/integration/oauth_runlayer_mock.py
| Scenario | Recommended |
|---|---|
| Full E2E tests with Playwright | Go server |
| Testing OAuth discovery modes | Go server |
| Token refresh/expiry testing | Go server |
| Error injection scenarios | Go server |
| JWKS/key rotation testing | Go server |
| Quick RFC 8707 resource tests | Python or Go with -runlayer-mode |
| Issue #271 regression testing | Python (lighter) or Go |
| CI/CD integration tests | Go (embeddable) |
./test/integration/test_oauth_resource_injection.sh
This script:
resource parameter correctly