Skip to content

Commit 54459fb

Browse files
authored
Add per-server service authentication (#19)
Previously, authentication was configured globally at the proxy level. This meant all servers had to use the same auth method. Now each MCP server can have its own authentication. This is useful when you have different services that need different auth - for example, one service uses API keys while another uses basic auth. Example config: ```json { "proxy": { "baseURL": "http://localhost:8080", "addr": ":8080" }, "mcpServers": { "postgres": { "transportType": "stdio", "command": "docker", "args": ["run", "-i", "mcp/postgres"], "serviceAuths": [ { "type": "bearer", "tokens": ["api-key-123", "api-key-456"] } ] }, "notion": { "transportType": "sse", "url": "https://notion-mcp.example.com/sse", "serviceAuths": [ { "type": "basic", "username": "admin", "password": {"$env": "NOTION_PASSWORD"} } ] } } } ``` When a server requires user tokens, each service auth can specify its own: ```json "serviceAuths": [ { "type": "bearer", "tokens": ["service-key"], "userToken": {"$env": "NOTION_USER_TOKEN"} } ] ```
1 parent 64d394f commit 54459fb

Some content is hidden

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

42 files changed

+1232
-533
lines changed

cmd/mcp-front/main.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,15 @@ import (
44
"encoding/json"
55
"flag"
66
"fmt"
7-
"log"
87
"os"
98

10-
"github.com/dgellow/mcp-front/internal"
119
"github.com/dgellow/mcp-front/internal/config"
10+
"github.com/dgellow/mcp-front/internal/log"
1211
"github.com/dgellow/mcp-front/internal/server"
1312
)
1413

1514
var BuildVersion = "dev"
1615

17-
func init() {
18-
log.SetFlags(0)
19-
log.SetOutput(os.Stderr)
20-
log.SetPrefix("")
21-
}
22-
2316
func generateDefaultConfig(path string) error {
2417
defaultConfig := map[string]any{
2518
"version": "v0.0.1-DEV_EDITION_EXPECT_CHANGES",
@@ -128,7 +121,7 @@ func main() {
128121
}
129122
if *configInit != "" {
130123
if err := generateDefaultConfig(*configInit); err != nil {
131-
internal.LogError("Failed to generate config: %v", err)
124+
log.LogError("Failed to generate config: %v", err)
132125
os.Exit(1)
133126
}
134127
fmt.Printf("Generated default config at: %s\n", *configInit)
@@ -154,12 +147,18 @@ func main() {
154147

155148
cfg, err := config.Load(*conf)
156149
if err != nil {
157-
internal.LogError("Failed to load config: %v", err)
150+
log.LogError("Failed to load config: %v", err)
158151
os.Exit(1)
159152
}
153+
154+
log.LogInfoWithFields("main", "Starting mcp-front", map[string]interface{}{
155+
"version": BuildVersion,
156+
"config": *conf,
157+
})
158+
160159
err = server.Run(cfg)
161160
if err != nil {
162-
internal.LogError("Failed to start server: %v", err)
161+
log.LogError("Failed to start server: %v", err)
163162
os.Exit(1)
164163
}
165164
}

config-token.example.json

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,7 @@
33
"proxy": {
44
"baseURL": "http://localhost:8080",
55
"addr": ":8080",
6-
"name": "mcp-front-dev",
7-
"auth": {
8-
"kind": "bearerToken",
9-
"tokens": {
10-
"postgres": ["dev-token-postgres-1", "dev-token-postgres-2"],
11-
"notion": ["dev-token-notion-1"],
12-
"git": ["dev-token-git-1", "dev-token-git-2", "dev-token-git-3"]
13-
}
14-
}
6+
"name": "mcp-front-dev"
157
},
168
"mcpServers": {
179
"postgres": {
@@ -21,6 +13,12 @@
2113
"run", "--rm", "-i", "--network", "host",
2214
"mcp/postgres",
2315
"postgresql://testuser:testpass@localhost:5432/testdb"
16+
],
17+
"serviceAuths": [
18+
{
19+
"type": "bearer",
20+
"tokens": ["dev-token-postgres-1", "dev-token-postgres-2"]
21+
}
2422
]
2523
},
2624
"notion": {
@@ -29,7 +27,13 @@
2927
"args": ["run", "--rm", "-i", "-e", "NOTION_TOKEN", "mcp/notion"],
3028
"env": {
3129
"NOTION_TOKEN": "test-notion-token"
32-
}
30+
},
31+
"serviceAuths": [
32+
{
33+
"type": "bearer",
34+
"tokens": ["dev-token-notion-1"]
35+
}
36+
]
3337
},
3438
"git": {
3539
"transportType": "stdio",
@@ -38,6 +42,12 @@
3842
"run", "--rm", "-i",
3943
"-v", "/tmp/test-repos:/repos:ro",
4044
"mcp/git"
45+
],
46+
"serviceAuths": [
47+
{
48+
"type": "bearer",
49+
"tokens": ["dev-token-git-1", "dev-token-git-2", "dev-token-git-3"]
50+
}
4151
]
4252
}
4353
}

integration/basic_auth_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package integration
2+
3+
import (
4+
"encoding/base64"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestBasicAuth(t *testing.T) {
13+
// Start mcp-front with basic auth config
14+
startMCPFront(t, "config/config.basic-auth-test.json",
15+
"ADMIN_PASSWORD=adminpass123",
16+
"USER_PASSWORD=userpass456",
17+
)
18+
19+
// Wait for startup
20+
waitForMCPFront(t)
21+
22+
t.Run("valid credentials", func(t *testing.T) {
23+
req, err := http.NewRequest("GET", "http://localhost:8080/postgres/sse", nil)
24+
require.NoError(t, err)
25+
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:adminpass123")))
26+
req.Header.Set("Accept", "text/event-stream")
27+
28+
client := &http.Client{}
29+
resp, err := client.Do(req)
30+
require.NoError(t, err)
31+
defer resp.Body.Close()
32+
33+
// Should get 200 OK with SSE stream when auth passes
34+
assert.Equal(t, http.StatusOK, resp.StatusCode)
35+
assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type"))
36+
})
37+
38+
t.Run("invalid password", func(t *testing.T) {
39+
req, err := http.NewRequest("GET", "http://localhost:8080/postgres/sse", nil)
40+
require.NoError(t, err)
41+
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:wrongpass")))
42+
43+
client := &http.Client{}
44+
resp, err := client.Do(req)
45+
require.NoError(t, err)
46+
defer resp.Body.Close()
47+
48+
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
49+
})
50+
51+
t.Run("unknown user", func(t *testing.T) {
52+
req, err := http.NewRequest("GET", "http://localhost:8080/postgres/sse", nil)
53+
require.NoError(t, err)
54+
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("unknown:adminpass123")))
55+
56+
client := &http.Client{}
57+
resp, err := client.Do(req)
58+
require.NoError(t, err)
59+
defer resp.Body.Close()
60+
61+
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
62+
})
63+
64+
t.Run("access MCP endpoint with basic auth", func(t *testing.T) {
65+
// Test accessing a protected MCP endpoint
66+
req, err := http.NewRequest("GET", "http://localhost:8080/postgres/sse", nil)
67+
require.NoError(t, err)
68+
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("user:userpass456")))
69+
req.Header.Set("Accept", "text/event-stream")
70+
71+
client := &http.Client{}
72+
resp, err := client.Do(req)
73+
require.NoError(t, err)
74+
defer resp.Body.Close()
75+
76+
// Should get 200 OK with SSE stream when auth passes
77+
assert.Equal(t, http.StatusOK, resp.StatusCode)
78+
assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type"))
79+
})
80+
81+
t.Run("bearer token with basic auth configured", func(t *testing.T) {
82+
// Server expects basic auth, bearer tokens should fail
83+
req, err := http.NewRequest("GET", "http://localhost:8080/postgres/sse", nil)
84+
require.NoError(t, err)
85+
req.Header.Set("Authorization", "Bearer sometoken")
86+
87+
client := &http.Client{}
88+
resp, err := client.Do(req)
89+
require.NoError(t, err)
90+
defer resp.Body.Close()
91+
92+
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
93+
})
94+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"version": "v0.0.1-DEV_EDITION_EXPECT_CHANGES",
3+
"proxy": {
4+
"baseURL": "http://localhost:8080",
5+
"addr": ":8080",
6+
"name": "mcp-front-basic-auth-test"
7+
},
8+
"mcpServers": {
9+
"postgres": {
10+
"transportType": "stdio",
11+
"command": "docker",
12+
"args": [
13+
"run",
14+
"-i",
15+
"--network",
16+
"host",
17+
"mcp/postgres",
18+
"postgresql://testuser:testpass@localhost:15432/testdb"
19+
],
20+
"serviceAuths": [
21+
{
22+
"type": "basic",
23+
"username": "admin",
24+
"password": {"$env": "ADMIN_PASSWORD"}
25+
},
26+
{
27+
"type": "basic",
28+
"username": "user",
29+
"password": {"$env": "USER_PASSWORD"}
30+
}
31+
]
32+
}
33+
}
34+
}

integration/config/config.demo-token.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@
33
"proxy": {
44
"baseURL": "http://localhost:8080",
55
"addr": ":8080",
6-
"name": "mcp-front-demo",
7-
"auth": {
8-
"kind": "bearerToken",
9-
"tokens": {
10-
"postgres": ["test-token", "demo-token"]
11-
}
12-
}
6+
"name": "mcp-front-demo"
137
},
148
"mcpServers": {
159
"postgres": {
@@ -22,7 +16,13 @@
2216
],
2317
"options": {
2418
"logEnabled": true
25-
}
19+
},
20+
"serviceAuths": [
21+
{
22+
"type": "bearer",
23+
"tokens": ["test-token", "demo-token"]
24+
}
25+
]
2626
}
2727
}
2828
}

integration/config/config.inline-test.json

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,7 @@
33
"proxy": {
44
"baseURL": "http://localhost:8080",
55
"addr": ":8080",
6-
"name": "mcp-front-inline-test",
7-
"auth": {
8-
"kind": "bearerToken",
9-
"tokens": {
10-
"test-inline": [
11-
"inline-test-token"
12-
]
13-
}
14-
}
6+
"name": "mcp-front-inline-test"
157
},
168
"mcpServers": {
179
"test-inline": {
@@ -95,7 +87,13 @@
9587
"timeout": "100ms"
9688
}
9789
]
98-
}
90+
},
91+
"serviceAuths": [
92+
{
93+
"type": "bearer",
94+
"tokens": ["inline-test-token"]
95+
}
96+
]
9997
}
10098
}
10199
}

integration/config/config.sse-test.json

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33
"proxy": {
44
"baseURL": "http://localhost:8080",
55
"addr": ":8080",
6-
"name": "mcp-front-sse-test",
7-
"auth": {
8-
"kind": "bearerToken",
9-
"tokens": {
10-
"test-sse": [
11-
"sse-test-token"
12-
]
13-
}
14-
}
6+
"name": "mcp-front-sse-test"
157
},
168
"mcpServers": {
179
"test-sse": {
1810
"transportType": "sse",
19-
"url": "http://localhost:3001/sse"
11+
"url": "http://localhost:3001/sse",
12+
"serviceAuths": [
13+
{
14+
"type": "bearer",
15+
"tokens": ["sse-test-token"]
16+
}
17+
]
2018
}
2119
}
2220
}

integration/config/config.streamable-test.json

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33
"proxy": {
44
"baseURL": "http://localhost:8080",
55
"addr": ":8080",
6-
"name": "mcp-front-streamable-test",
7-
"auth": {
8-
"kind": "bearerToken",
9-
"tokens": {
10-
"test-streamable": [
11-
"streamable-test-token"
12-
]
13-
}
14-
}
6+
"name": "mcp-front-streamable-test"
157
},
168
"mcpServers": {
179
"test-streamable": {
1810
"transportType": "streamable-http",
19-
"url": "http://localhost:3002"
11+
"url": "http://localhost:3002",
12+
"serviceAuths": [
13+
{
14+
"type": "bearer",
15+
"tokens": ["streamable-test-token"]
16+
}
17+
]
2018
}
2119
}
2220
}

0 commit comments

Comments
 (0)