Skip to content

Commit 70cbcba

Browse files
authored
Per-server oauth flow and massive refactoring (#20)
Adds OAuth for external MCP services and cleans up the auth code. After Google login, users see a page listing services that need OAuth (Linear, GitHub, etc). They can connect now or skip and do it later at /my/tokens. Tokens get refreshed 5 min before expiry so they don't break mid-session when users are in Claude. Refactored the monolithic auth/oauth code into separate packages - oauth for provider setup, googleauth for Google-specific stuff, browserauth for session types, auth for the service OAuth client. Handlers split up too. Everything uses dependency injection now.
1 parent 00a2309 commit 70cbcba

File tree

94 files changed

+6882
-3281
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+6882
-3281
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -63,40 +63,8 @@ jobs:
6363
- name: Run unit tests
6464
run: go test -v ./internal/... ./cmd/...
6565

66-
- name: Build mcp-front CLI
67-
run: go build -o mcp-front ./cmd/mcp-front
68-
6966
- name: Validate config files
70-
run: |
71-
echo "Validating all config files in the repository..."
72-
failed=0
73-
74-
# Check root level config files
75-
for config in *.json; do
76-
if [[ -f "$config" ]]; then
77-
echo "Checking $config..."
78-
if ! ./mcp-front -validate -config "$config"; then
79-
failed=1
80-
fi
81-
fi
82-
done
83-
84-
# Check integration test config files
85-
for config in integration/config/*.json; do
86-
if [[ -f "$config" ]]; then
87-
echo "Checking $config..."
88-
if ! ./mcp-front -validate -config "$config"; then
89-
failed=1
90-
fi
91-
fi
92-
done
93-
94-
if [[ $failed -eq 1 ]]; then
95-
echo "❌ Config validation failed for one or more files"
96-
exit 1
97-
else
98-
echo "✅ All config files validated successfully"
99-
fi
67+
run: make lint-config
10068

10169
- name: Set up Docker Buildx
10270
uses: docker/setup-buildx-action@v3

CLAUDE.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ mcp-front is a Go-based OAuth 2.1 proxy server for MCP (Model Context Protocol)
5656
- **Define interfaces where they are used** - Not in the package that implements them
5757
- **Avoid circular imports** - Use interface segregation in separate packages when needed
5858
- **Dependency injection over getter methods** - Pass dependencies to constructors
59+
- **Functional core, imperative shell** - Prefer pure functions for business logic, keep side effects (I/O, state mutations) at the boundaries. Makes code more testable and reasoning easier.
60+
- **Upstream lifecycle control** - Manage goroutines, servers, and background processes from the application root. Library code should expose Start/Stop methods, not start things autonomously.
5961

6062
### 🎯 Core Development Principles (from Zig Zen)
6163

@@ -115,7 +117,7 @@ LOG_FORMAT="text" # json or text
115117

116118
#### Updating OAuth scopes
117119

118-
1. Check `internal/oauth/auth.go` for current scopes
120+
1. Check `internal/googleauth/google.go` for current scopes
119121
2. Use standard OpenID Connect scopes (not Google-specific URLs)
120122
3. Update tests to verify new scopes work
121123

@@ -129,14 +131,24 @@ LOG_FORMAT="text" # json or text
129131

130132
```
131133
internal/
132-
├── config/ # Configuration parsing and validation
133-
├── oauth/ # OAuth 2.1 implementation with fosite
134-
├── server/ # HTTP server and middleware
135-
├── client/ # MCP client management
136-
└── logging.go # Structured logging setup
137-
138-
integration/ # Integration tests (OAuth, security, scenarios)
139-
cmd/mcp-front/ # Main application entry point
134+
├── config/ # Configuration parsing and validation
135+
├── oauth/ # OAuth 2.1 provider, JWT, middleware
136+
├── googleauth/ # Google OAuth integration (pure functions)
137+
├── adminauth/ # Admin authorization logic
138+
├── browserauth/ # Browser session types (SessionCookie, AuthorizationState)
139+
├── oauthsession/ # OAuth session types for fosite
140+
├── servicecontext/ # Service authentication context utilities
141+
├── server/ # HTTP server, handlers, and middleware
142+
├── client/ # MCP client management and session handling
143+
├── auth/ # Service OAuth client for upstream authentication
144+
├── crypto/ # Encryption, HMAC, token signing utilities
145+
├── storage/ # Storage abstraction (memory, Firestore)
146+
├── inline/ # Inline MCP server implementation
147+
├── mcpfront.go # Main application orchestration (imperative shell)
148+
└── [utility packages: cookie, email, envutil, json, jsonrpc, log, sse, testutil]
149+
150+
integration/ # Integration tests (OAuth, security, scenarios)
151+
cmd/mcp-front/ # Main application entry point
140152
```
141153

142154
### Testing Guidance
@@ -153,6 +165,7 @@ cmd/mcp-front/ # Main application entry point
153165
3. Don't create new auth patterns - use existing OAuth or bearer token auth
154166
4. Don't modify git configuration
155167
5. Don't create README files proactively
168+
6. **Variable shadowing package names** - `config.MCPClientConfig is not a type` means a variable named `config` is shadowing the package. Always check for variables that shadow imported package names
156169

157170
### When Working on Features
158171

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ doc:
33

44
format:
55
go fmt ./...
6+
# go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...
7+
modernize -fix -test ./...
68
cd docs-site && npm run format
79

810
lint:
911
staticcheck ./...
1012
golangci-lint run ./...
1113

14+
lint-config:
15+
go build -o cmd/mcp-front/mcp-front ./cmd/mcp-front
16+
./scripts/validate-configs.sh
17+
1218
build:
1319
go build -o mcp-front ./cmd/mcp-front
1420
cd docs-site && npm run build
1521

16-
.PHONY: doc format build lint
22+
.PHONY: doc format build lint lint-config

README.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ mcp-front is an authentication proxy that sits between Claude.ai and your MCP se
3333

3434
- **Single sign-on** via Google OAuth for all MCP tools
3535
- **Domain validation** to restrict access to your organization
36-
- **Per-user tokens** for services like Notion
36+
- **Per-user authentication** for services like Notion and Stainless (via OAuth or manual tokens)
3737
- **Session isolation** so multiple users can share infrastructure
3838

3939
## Why use mcp-front?
@@ -52,7 +52,7 @@ With mcp-front:
5252

5353
1. Claude.ai connects to `https://your-domain.com/<service>/sse`
5454
2. mcp-front validates the user's OAuth token
55-
3. If the server needs a user token, prompts via `/my/tokens`
55+
3. If a service requires user authentication, `mcp-front` will guide the user through a one-time setup (via an OAuth consent screen or a manual token entry page).
5656
4. Proxies requests to the configured MCP server
5757
5. For stdio servers, each user gets an isolated process
5858

@@ -150,16 +150,24 @@ docker run -d -p 8080:8080 \
150150

151151
mcp-front uses explicit JSON syntax `{"$env": "VAR_NAME"}` for environment variables throughout its configuration. This deliberate choice eliminates the ambiguity and risks inherent in shell-style variable substitution. When configs pass through multiple layers of tooling and scripts, traditional `$VAR` syntax can expand unexpectedly, causing security issues and debugging nightmares. The JSON format ensures your configuration remains exactly as written until mcp-front processes it, providing predictable behavior across all deployment environments. For per-user authentication, `{"$userToken": "{{token}}"}` follows the same principle, keeping user credentials cleanly separated from system configuration.
152152

153-
### Per-user tokens
153+
## Service Authentication (Per-User Tokens)
154154

155-
Some MCP servers are better to use with each users having their own integration token (e.g., Notion). Configure with:
155+
Some MCP servers, like Notion or Stainless, require each user to provide their own individual API key or grant access via OAuth. `mcp-front` automates this process.
156156

157+
When a service is configured with `requiresUserToken: true`, `mcp-front` will guide the user through a one-time setup for that service after their initial Google login. This is handled in one of two ways, depending on the service's configuration.
158+
159+
### Manual Token Entry
160+
161+
For services that require a manually generated API key, you can configure `mcp-front` to prompt the user for it on a secure web page.
162+
163+
**Configuration:**
157164
```json
158165
{
159166
"notion": {
160167
"transportType": "stdio",
161168
"requiresUserToken": true,
162-
"tokenSetup": {
169+
"userAuthentication": {
170+
"type": "manual",
163171
"displayName": "Notion Integration Token",
164172
"instructions": "Create an integration and copy the token",
165173
"helpUrl": "https://www.notion.so/my-integrations"
@@ -174,9 +182,33 @@ Some MCP servers are better to use with each users having their own integration
174182
}
175183
}
176184
```
185+
After authenticating, the user will be directed to the `/my/tokens` page to enter their token.
186+
187+
### Service OAuth Flow
177188

178-
After the initial OAuth authentication, when users try to use Claude.ai with the integration they will be prompted to
179-
provide a token via the `/my/tokens` web UI.
189+
For services that support OAuth, `mcp-front` can handle the entire flow automatically. After the user logs in with Google, they will be shown an interstitial page where they can connect to each service.
190+
191+
**Configuration:**
192+
```json
193+
{
194+
"stainless": {
195+
"transportType": "stdio",
196+
"command": "stainless",
197+
"args": ["mcp"],
198+
"requiresUserToken": true,
199+
"userAuthentication": {
200+
"type": "oauth",
201+
"displayName": "Stainless",
202+
"clientId": {"$env": "STAINLESS_OAUTH_CLIENT_ID"},
203+
"clientSecret": {"$env": "STAINLESS_OAUTH_CLIENT_SECRET"},
204+
"authorizationUrl": "https://api.stainless.com/oauth/authorize",
205+
"tokenUrl": "https://api.stainless.com/oauth/token",
206+
"scopes": ["mcp:read", "mcp:write"]
207+
}
208+
}
209+
}
210+
```
211+
This provides a seamless experience for the user and enables automatic token refreshes.
180212

181213
### Full configuration example
182214

cmd/mcp-front/main.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/json"
56
"flag"
67
"fmt"
78
"os"
89

10+
"github.com/dgellow/mcp-front/internal"
911
"github.com/dgellow/mcp-front/internal/config"
1012
"github.com/dgellow/mcp-front/internal/log"
11-
"github.com/dgellow/mcp-front/internal/server"
1213
)
1314

1415
var BuildVersion = "dev"
@@ -151,12 +152,19 @@ func main() {
151152
os.Exit(1)
152153
}
153154

154-
log.LogInfoWithFields("main", "Starting mcp-front", map[string]interface{}{
155+
log.LogInfoWithFields("main", "Starting mcp-front", map[string]any{
155156
"version": BuildVersion,
156157
"config": *conf,
157158
})
158159

159-
err = server.Run(cfg)
160+
ctx := context.Background()
161+
mcpFront, err := internal.NewMCPFront(ctx, cfg)
162+
if err != nil {
163+
log.LogError("Failed to create MCP proxy: %v", err)
164+
os.Exit(1)
165+
}
166+
167+
err = mcpFront.Run()
160168
if err != nil {
161169
log.LogError("Failed to start server: %v", err)
162170
os.Exit(1)

config-oauth.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"notion": {
3333
"transportType": "stdio",
3434
"requiresUserToken": true,
35-
"tokenSetup": {
35+
"userAuthentication": {
36+
"type": "manual",
3637
"displayName": "Notion Integration Token",
3738
"instructions": "Create an integration at https://www.notion.so/my-integrations and copy the token",
3839
"helpUrl": "https://developers.notion.com/docs/create-a-notion-integration"
@@ -55,6 +56,24 @@
5556
"-v", "/repos:/repos:ro",
5657
"mcp/git:latest"
5758
]
59+
},
60+
"stainless": {
61+
"transportType": "stdio",
62+
"command": "stainless",
63+
"args": ["mcp"],
64+
"requiresUserToken": true,
65+
"userAuthentication": {
66+
"type": "oauth",
67+
"displayName": "Stainless",
68+
"clientId": {"$env": "STAINLESS_OAUTH_CLIENT_ID"},
69+
"clientSecret": {"$env": "STAINLESS_OAUTH_CLIENT_SECRET"},
70+
"authorizationUrl": "https://api.stainless.com/oauth/authorize",
71+
"tokenUrl": "https://api.stainless.com/oauth/token",
72+
"scopes": ["mcp:read", "mcp:write"]
73+
},
74+
"env": {
75+
"STAINLESS_API_TOKEN": {"$userToken": "{{token}}"}
76+
}
5877
}
5978
}
6079
}

config-user-tokens-example.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"OPENAPI_MCP_HEADERS": {"$userToken": "{\"Authorization\": \"Bearer {{token}}\"}"}
2929
},
3030
"requiresUserToken": true,
31-
"tokenSetup": {
31+
"userAuthentication": {
32+
"type": "manual",
3233
"displayName": "Notion API Token",
3334
"instructions": "Enter your Notion API token. You can find this at https://www.notion.so/my-integrations",
3435
"helpUrl": "https://developers.notion.com/docs/authorization",
@@ -43,7 +44,8 @@
4344
"GITHUB_TOKEN": {"$userToken": "{{token}}"}
4445
},
4546
"requiresUserToken": true,
46-
"tokenSetup": {
47+
"userAuthentication": {
48+
"type": "manual",
4749
"displayName": "GitHub Personal Access Token",
4850
"instructions": "Create a personal access token at https://github.com/settings/tokens",
4951
"helpUrl": "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token",

docs-site/src/content/docs/api-reference.md

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,34 @@ Returns:
138138
}
139139
```
140140

141+
## User Service Endpoints
142+
143+
Browser-only endpoints for service connections. Require browser SSO session.
144+
145+
### `GET /oauth/services`
146+
147+
Service connection page. Lists OAuth-enabled services, shown after Google login if services need auth.
148+
149+
### `GET /my/tokens`
150+
151+
Token management. Connect OAuth services, add manual tokens.
152+
153+
### `GET /oauth/connect?service={service_name}`
154+
155+
Initiate OAuth flow. Redirects to service.
156+
157+
### `POST /oauth/disconnect`
158+
159+
Revoke OAuth connection. Form: `service={service_name}`.
160+
161+
### `POST /my/tokens/set`
162+
163+
Save manual token. Form: `service={service_name}&token={user_token}`.
164+
165+
### `GET /oauth/callback/{service_name}`
166+
167+
OAuth callback. Set as redirect URI in service OAuth config.
168+
141169
## Authentication
142170

143171
### Bearer token
@@ -159,19 +187,4 @@ PKCE is required for all OAuth flows.
159187

160188
## Errors
161189

162-
All errors follow OAuth 2.1 format:
163-
164-
```json
165-
{
166-
"error": "invalid_request",
167-
"error_description": "Missing required parameter"
168-
}
169-
```
170-
171-
Common errors:
172-
173-
- `invalid_request` - Bad parameters
174-
- `invalid_client` - Unknown client
175-
- `invalid_grant` - Bad auth code
176-
- `unauthorized_client` - Client can't use grant type
177-
- `server_error` - Internal error
190+
OAuth 2.1 format with `error` and `error_description` fields. Common codes: `invalid_request` (bad parameters), `invalid_client` (unknown client), `invalid_grant` (bad auth code), `unauthorized_client` (client can't use grant type), `server_error` (internal error).

0 commit comments

Comments
 (0)