Skip to content

Commit ed55729

Browse files
authored
Feat/generic sdk (#4)
* chore(docs): Add initial implementation and refactoring plan Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * ci(oauth): add OAuth server with token validation and caching support Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * chore: update imports to use mark3labs for OAuth setup Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * refactor(core): extract cache and context utilities for SDK-agnostic support Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * fix(advancd/main.go): Rename file Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * feat(oauth): add token validation and error handling in WrapHandler Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * style: Add explanatory comments about PORT and SERVER_URL usage Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * chore(github workflow): update example build to find all main.go files Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> * refactor(advanced/main.go): Enhance TLS detection and Logging for server startup Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com> --------- Signed-off-by: Tommy Nguyen <tuannvm@hotmail.com>
1 parent 41ddb4c commit ed55729

File tree

27 files changed

+2528
-573
lines changed

27 files changed

+2528
-573
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ jobs:
4848

4949
- name: Build examples
5050
run: |
51-
go build ./examples/simple/main.go
52-
go build ./examples/advanced/main.go
51+
for example in $(find examples -name main.go); do
52+
echo "Building $example..."
53+
go build "$example"
54+
done
5355
5456
- name: Run go vet
5557
run: go vet ./...

CLAUDE.md

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
**oauth-mcp-proxy** is an OAuth 2.1 authentication library for Go MCP servers. It provides server-side OAuth integration with minimal code (3-line integration via `WithOAuth()`), supporting multiple providers (HMAC, Okta, Google, Azure AD).
88

9+
**Version**: v2.0.0 (Supports both `mark3labs/mcp-go` and official `modelcontextprotocol/go-sdk`)
10+
911
## Build Commands
1012

1113
```bash
@@ -36,34 +38,78 @@ make vuln
3638

3739
## Architecture
3840

41+
### Package Structure (v2.0.0)
42+
43+
```
44+
oauth-mcp-proxy/
45+
├── [core package - SDK-agnostic]
46+
│ ├── oauth.go - Server type, NewServer, ValidateTokenCached
47+
│ ├── config.go - Configuration validation and provider setup
48+
│ ├── cache.go - Token cache with 5-minute TTL
49+
│ ├── context.go - Context utilities (WithOAuthToken, GetUserFromContext, etc.)
50+
│ ├── handlers.go - OAuth HTTP endpoints (/.well-known/*, /oauth/*)
51+
│ ├── middleware.go - CreateHTTPContextFunc for token extraction
52+
│ ├── logger.go - Logger interface
53+
│ ├── metadata.go - OAuth metadata structures
54+
│ └── provider/ - Token validators (HMAC, OIDC)
55+
56+
├── mark3labs/ - Adapter for mark3labs/mcp-go SDK
57+
│ ├── oauth.go - WithOAuth → ServerOption
58+
│ └── middleware.go - Middleware for mark3labs types
59+
60+
└── mcp/ - Adapter for official modelcontextprotocol/go-sdk
61+
└── oauth.go - WithOAuth → http.Handler
62+
```
63+
3964
### Core Components
4065

41-
1. **oauth.go** - Main entry point, provides `WithOAuth()` function that creates OAuth server and returns MCP server option
66+
**Core Package** (SDK-agnostic):
67+
1. **oauth.go** - `Server` type, `NewServer()`, `ValidateTokenCached()` (used by adapters)
4268
2. **config.go** - Configuration validation and provider setup
43-
3. **middleware.go** - Token validation middleware with 5-minute caching
44-
4. **handlers.go** - OAuth HTTP endpoints (/.well-known/*, /oauth/*)
45-
5. **provider/provider.go** - Token validators (HMACValidator, OIDCValidator)
69+
3. **cache.go** - Token caching logic (`TokenCache`, `CachedToken`)
70+
4. **context.go** - Context utilities (`WithOAuthToken`, `GetOAuthToken`, `WithUser`, `GetUserFromContext`)
71+
5. **handlers.go** - OAuth HTTP endpoints
72+
6. **provider/provider.go** - Token validators (HMACValidator, OIDCValidator)
73+
74+
**Adapters** (SDK-specific):
75+
- **mark3labs/** - Middleware adapter for `mark3labs/mcp-go`
76+
- **mcp/** - HTTP handler wrapper for official SDK
4677

4778
### Key Design Patterns
4879

80+
- **OpenTelemetry Pattern**: Core logic is SDK-agnostic; adapters provide SDK-specific integration
4981
- **Instance-scoped**: Each `Server` instance has its own token cache and validator (no globals)
5082
- **Provider abstraction**: `TokenValidator` interface supports multiple OAuth providers
5183
- **Caching strategy**: Tokens cached for 5 minutes using SHA-256 hash as key
52-
- **Context propagation**: OAuth token extracted from HTTP header → stored in context → validated by middleware → user added to context
84+
- **Context propagation**: OAuth token extracted from HTTP header → stored in context → validated → user added to context
5385

5486
### Integration Flow
5587

88+
**mark3labs SDK:**
5689
```text
5790
1. HTTP request with "Authorization: Bearer <token>" header
5891
2. CreateHTTPContextFunc() extracts token → adds to context via WithOAuthToken()
59-
3. OAuth middleware (Server.Middleware()) validates token:
60-
- Checks cache first (5-minute TTL)
92+
3. mark3labs middleware validates token:
93+
- Calls Server.ValidateTokenCached() (checks cache first)
6194
- If not cached, validates via provider (HMAC or OIDC)
62-
- Caches result
63-
4. Adds authenticated User to context via userContextKey
95+
- Caches result (5-minute TTL)
96+
4. Adds authenticated User to context via WithUser()
6497
5. Tool handler accesses user via GetUserFromContext(ctx)
6598
```
6699

100+
**Official SDK:**
101+
```text
102+
1. HTTP request with "Authorization: Bearer <token>" header
103+
2. mcp adapter's HTTP handler intercepts request
104+
3. Validates token via Server.ValidateTokenCached():
105+
- Checks cache first (5-minute TTL)
106+
- If not cached, validates via provider
107+
- Caches result
108+
4. Adds token and user to context (WithOAuthToken, WithUser)
109+
5. Passes request to official SDK's StreamableHTTPHandler
110+
6. Tool handler accesses user via GetUserFromContext(ctx)
111+
```
112+
67113
### Provider System
68114

69115
- **HMAC**: Validates JWT tokens with shared secret (testing/dev)
@@ -96,3 +142,29 @@ go test -v -run TestName ./...
96142
3. **Logging**: Config.Logger is optional. If nil, uses default logger (log.Printf with level prefixes)
97143
4. **Modes**: Library supports "native" (token validation only) and "proxy" (OAuth flow proxy) modes
98144
5. **Security**: All redirect URIs validated, state parameters HMAC-signed, tokens never logged (only hash previews)
145+
6. **v2.0.0 Breaking Change**: `WithOAuth()` moved to adapter packages (`mark3labs.WithOAuth()` or `mcp.WithOAuth()`). See `MIGRATION-V2.md`.
146+
147+
## Using the Library
148+
149+
### With mark3labs/mcp-go
150+
```go
151+
import (
152+
oauth "github.com/tuannvm/oauth-mcp-proxy"
153+
"github.com/tuannvm/oauth-mcp-proxy/mark3labs"
154+
)
155+
156+
_, oauthOption, _ := mark3labs.WithOAuth(mux, &oauth.Config{...})
157+
mcpServer := server.NewMCPServer("name", "1.0.0", oauthOption)
158+
```
159+
160+
### With Official SDK
161+
```go
162+
import (
163+
oauth "github.com/tuannvm/oauth-mcp-proxy"
164+
mcpoauth "github.com/tuannvm/oauth-mcp-proxy/mcp"
165+
)
166+
167+
mcpServer := mcp.NewServer(&mcp.Implementation{...}, nil)
168+
_, handler, _ := mcpoauth.WithOAuth(mux, &oauth.Config{...}, mcpServer)
169+
http.ListenAndServe(":8080", handler)
170+
```

README.md

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,37 @@
22

33
OAuth 2.1 authentication library for Go MCP servers.
44

5+
**Supports both MCP SDKs:**
6+
-`mark3labs/mcp-go`
7+
-`modelcontextprotocol/go-sdk` (official)
8+
59
**One-time setup:** Configure provider + add `WithOAuth()` to your server.
610
**Result:** All tools automatically protected with token validation and caching.
711

12+
### mark3labs/mcp-go
813
```go
9-
// Enable OAuth authentication
10-
_, oauthOption, _ := oauth.WithOAuth(mux, &oauth.Config{
14+
import "github.com/tuannvm/oauth-mcp-proxy/mark3labs"
15+
16+
_, oauthOption, _ := mark3labs.WithOAuth(mux, &oauth.Config{
1117
Provider: "okta",
1218
Issuer: "https://your-company.okta.com",
1319
Audience: "api://your-mcp-server",
1420
})
1521

16-
// All tools now require authentication
1722
mcpServer := server.NewMCPServer("Server", "1.0.0", oauthOption)
1823
```
1924

25+
### Official SDK
26+
```go
27+
import mcpoauth "github.com/tuannvm/oauth-mcp-proxy/mcp"
28+
29+
mcpServer := mcp.NewServer(&mcp.Implementation{...}, nil)
30+
_, handler, _ := mcpoauth.WithOAuth(mux, cfg, mcpServer)
31+
http.ListenAndServe(":8080", handler)
32+
```
33+
34+
> **📢 Migrating from v1.x?** See [MIGRATION-V2.md](./MIGRATION-V2.md) (2 line change, ~5 min)
35+
2036
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tuannvm/oauth-mcp-proxy/test.yml?branch=main&label=Tests&logo=github)](https://github.com/tuannvm/oauth-mcp-proxy/actions/workflows/test.yml)
2137
[![Go Version](https://img.shields.io/github/go-mod/go-version/tuannvm/oauth-mcp-proxy?logo=go)](https://github.com/tuannvm/oauth-mcp-proxy/blob/main/go.mod)
2238
[![Go Report Card](https://goreportcard.com/badge/github.com/tuannvm/oauth-mcp-proxy)](https://goreportcard.com/report/github.com/tuannvm/oauth-mcp-proxy)
@@ -28,6 +44,7 @@ mcpServer := server.NewMCPServer("Server", "1.0.0", oauthOption)
2844

2945
## Why Use This Library?
3046

47+
- **Dual SDK support** - Works with both mark3labs and official SDKs
3148
- **Simple integration** - One `WithOAuth()` call protects all tools
3249
- **Zero per-tool config** - All tools automatically protected
3350
- **Fast token caching** - 5-min cache, <5ms validation
@@ -64,21 +81,26 @@ sequenceDiagram
6481

6582
## Quick Start
6683

67-
### 1. Install
84+
### Using mark3labs/mcp-go
85+
86+
#### 1. Install
6887

6988
```bash
7089
go get github.com/tuannvm/oauth-mcp-proxy
7190
```
7291

73-
### 2. Add to Your Server
92+
#### 2. Add to Your Server
7493

7594
```go
76-
import oauth "github.com/tuannvm/oauth-mcp-proxy"
95+
import (
96+
oauth "github.com/tuannvm/oauth-mcp-proxy"
97+
"github.com/tuannvm/oauth-mcp-proxy/mark3labs"
98+
)
7799

78100
mux := http.NewServeMux()
79101

80102
// Enable OAuth (one time setup)
81-
_, oauthOption, _ := oauth.WithOAuth(mux, &oauth.Config{
103+
_, oauthOption, _ := mark3labs.WithOAuth(mux, &oauth.Config{
82104
Provider: "okta", // or "hmac", "google", "azure"
83105
Issuer: "https://your-company.okta.com",
84106
Audience: "api://your-mcp-server",
@@ -99,7 +121,7 @@ streamable := mcpserver.NewStreamableHTTPServer(
99121
mux.Handle("/mcp", streamable)
100122
```
101123

102-
### 3. Access Authenticated User
124+
#### 3. Access Authenticated User
103125

104126
```go
105127
func myHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -111,16 +133,71 @@ func myHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResul
111133
}
112134
```
113135

136+
---
137+
138+
### Using Official SDK
139+
140+
#### 1. Install
141+
142+
```bash
143+
go get github.com/modelcontextprotocol/go-sdk
144+
go get github.com/tuannvm/oauth-mcp-proxy
145+
```
146+
147+
#### 2. Add to Your Server
148+
149+
```go
150+
import (
151+
"github.com/modelcontextprotocol/go-sdk/mcp"
152+
oauth "github.com/tuannvm/oauth-mcp-proxy"
153+
mcpoauth "github.com/tuannvm/oauth-mcp-proxy/mcp"
154+
)
155+
156+
mux := http.NewServeMux()
157+
158+
// Create MCP server
159+
mcpServer := mcp.NewServer(&mcp.Implementation{
160+
Name: "my-server",
161+
Version: "1.0.0",
162+
}, nil)
163+
164+
// Add tools
165+
mcp.AddTool(mcpServer, &mcp.Tool{
166+
Name: "greet",
167+
Description: "Greet user",
168+
}, func(ctx context.Context, req *mcp.CallToolRequest, params *struct{}) (*mcp.CallToolResult, any, error) {
169+
user, _ := oauth.GetUserFromContext(ctx)
170+
return &mcp.CallToolResult{
171+
Content: []mcp.Content{
172+
&mcp.TextContent{Text: "Hello, " + user.Username},
173+
},
174+
}, nil, nil
175+
})
176+
177+
// Add OAuth protection
178+
_, handler, _ := mcpoauth.WithOAuth(mux, &oauth.Config{
179+
Provider: "okta",
180+
Issuer: "https://your-company.okta.com",
181+
Audience: "api://your-mcp-server",
182+
}, mcpServer)
183+
184+
http.ListenAndServe(":8080", handler)
185+
```
186+
114187
Your MCP server now requires OAuth authentication.
115188

116189
---
117190

118191
## Examples
119192

120-
| Example | Description |
121-
|---------|-------------|
122-
| **[Simple](examples/simple/)** | Minimal setup - copy/paste ready |
123-
| **[Advanced](examples/advanced/)** | All features - ConfigBuilder, WrapHandler, LogStartup |
193+
See [examples/README.md](examples/README.md) for detailed setup guide including Okta configuration.
194+
195+
| SDK | Example | Description |
196+
|-----|---------|-------------|
197+
| **mark3labs** | [Simple](examples/mark3labs/simple/) | Minimal setup - copy/paste ready |
198+
| **mark3labs** | [Advanced](examples/mark3labs/advanced/) | ConfigBuilder, multiple tools, logging |
199+
| **Official** | [Simple](examples/official/simple/) | Minimal setup - copy/paste ready |
200+
| **Official** | [Advanced](examples/official/advanced/) | ConfigBuilder, multiple tools, logging |
124201

125202
---
126203

cache.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package oauth
2+
3+
import (
4+
"sync"
5+
"time"
6+
7+
"github.com/tuannvm/oauth-mcp-proxy/provider"
8+
)
9+
10+
// Re-export User from provider for backwards compatibility
11+
type User = provider.User
12+
13+
// TokenCache stores validated tokens to avoid re-validation
14+
type TokenCache struct {
15+
mu sync.RWMutex
16+
cache map[string]*CachedToken
17+
}
18+
19+
// CachedToken represents a cached token validation result
20+
type CachedToken struct {
21+
User *User
22+
ExpiresAt time.Time
23+
}
24+
25+
// getCachedToken retrieves a cached token validation result
26+
func (tc *TokenCache) getCachedToken(tokenHash string) (*CachedToken, bool) {
27+
tc.mu.RLock()
28+
29+
cached, exists := tc.cache[tokenHash]
30+
if !exists {
31+
tc.mu.RUnlock()
32+
return nil, false
33+
}
34+
35+
if time.Now().After(cached.ExpiresAt) {
36+
tc.mu.RUnlock()
37+
go tc.deleteExpiredToken(tokenHash)
38+
return nil, false
39+
}
40+
41+
tc.mu.RUnlock()
42+
return cached, true
43+
}
44+
45+
// deleteExpiredToken safely deletes an expired token from the cache
46+
func (tc *TokenCache) deleteExpiredToken(tokenHash string) {
47+
tc.mu.Lock()
48+
defer tc.mu.Unlock()
49+
50+
if cached, exists := tc.cache[tokenHash]; exists && time.Now().After(cached.ExpiresAt) {
51+
delete(tc.cache, tokenHash)
52+
}
53+
}
54+
55+
// setCachedToken stores a token validation result
56+
func (tc *TokenCache) setCachedToken(tokenHash string, user *User, expiresAt time.Time) {
57+
tc.mu.Lock()
58+
defer tc.mu.Unlock()
59+
60+
tc.cache[tokenHash] = &CachedToken{
61+
User: user,
62+
ExpiresAt: expiresAt,
63+
}
64+
}

0 commit comments

Comments
 (0)