Skip to content

Commit c14be78

Browse files
committed
feat: allow HTTP default token fallback
1 parent cd39e42 commit c14be78

8 files changed

Lines changed: 91 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,8 @@ GITHUB_ALLOWED_PR_AUTHORS='renovate[bot],github-actions[bot]' ./github-mcp-serve
15121512

15131513
When set, tools such as `merge_pull_request`, `update_pull_request`, review-write tools, and PR branch updates fetch the target PR and reject the call unless `pr.User.Login` is in the allowlist. Read-only PR tools and `create_pull_request` are not restricted. `actions_run_trigger` is not gated by this setting because it targets a ref rather than a PR number.
15141514

1515+
In HTTP mode, `GITHUB_PERSONAL_ACCESS_TOKEN` can also be used as a server-side default token for trusted local deployments. Requests with an `Authorization` header still use the request token; requests without one fall back to the configured server token.
1516+
15151517
## i18n / Overriding Descriptions
15161518

15171519
The descriptions of the tools can be overridden by creating a

cmd/github-mcp-server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ var (
146146
httpConfig := ghhttp.ServerConfig{
147147
Version: version,
148148
Host: viper.GetString("host"),
149+
Token: viper.GetString("personal_access_token"),
149150
Port: viper.GetInt("port"),
150151
BaseURL: viper.GetString("base-url"),
151152
ResourcePath: viper.GetString("base-path"),

docs/streamable-http.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,13 @@ To provide PAT credentials, or to customize server behavior preferences, you can
9191
```
9292

9393
See [Remote Server](./remote-server.md) documentation for more details on client configuration options.
94+
95+
### Using a Server-Side Default Token
96+
97+
For trusted local deployments, HTTP mode can use `GITHUB_PERSONAL_ACCESS_TOKEN` as a fallback when a request does not include an `Authorization` header:
98+
99+
```bash
100+
GITHUB_PERSONAL_ACCESS_TOKEN=ghp_yourtokenhere github-mcp-server http
101+
```
102+
103+
If a request includes `Authorization: Bearer ...`, that request token takes precedence. If no request token is provided and no server-side token is configured, the server returns `401 Unauthorized`.

pkg/http/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func NewHTTPMcpHandler(
127127

128128
func (h *Handler) RegisterMiddleware(r chi.Router) {
129129
r.Use(
130-
middleware.ExtractUserToken(h.oauthCfg),
130+
middleware.ExtractUserToken(h.oauthCfg, h.config.Token),
131131
middleware.WithRequestConfig,
132132
middleware.WithMCPParse(),
133133
middleware.WithPATScopes(h.logger, h.scopeFetcher),

pkg/http/middleware/token.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/github/github-mcp-server/pkg/utils"
1111
)
1212

13-
func ExtractUserToken(oauthCfg *oauth.Config) func(next http.Handler) http.Handler {
13+
func ExtractUserToken(oauthCfg *oauth.Config, defaultToken ...string) func(next http.Handler) http.Handler {
1414
return func(next http.Handler) http.Handler {
1515
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1616
ctx := r.Context()
@@ -27,6 +27,20 @@ func ExtractUserToken(oauthCfg *oauth.Config) func(next http.Handler) http.Handl
2727
if err != nil {
2828
// For missing Authorization header, return 401 with WWW-Authenticate header per MCP spec
2929
if errors.Is(err, utils.ErrMissingAuthorizationHeader) {
30+
if len(defaultToken) > 0 && defaultToken[0] != "" {
31+
tokenType, err := utils.ParseToken(defaultToken[0])
32+
if err != nil {
33+
http.Error(w, fmt.Sprintf("default token is invalid: %v", err), http.StatusInternalServerError)
34+
return
35+
}
36+
37+
ctx = ghcontext.WithTokenInfo(ctx, &ghcontext.TokenInfo{
38+
Token: defaultToken[0],
39+
TokenType: tokenType,
40+
})
41+
next.ServeHTTP(w, r.WithContext(ctx))
42+
return
43+
}
3044
sendAuthChallenge(w, r, oauthCfg)
3145
return
3246
}

pkg/http/middleware/token_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,55 @@ func TestExtractUserToken_NilOAuthConfig(t *testing.T) {
232232
assert.Equal(t, utils.TokenTypePersonalAccessToken, capturedTokenInfo.TokenType)
233233
}
234234

235+
func TestExtractUserToken_DefaultTokenFallback(t *testing.T) {
236+
var capturedTokenInfo *ghcontext.TokenInfo
237+
var tokenInfoCaptured bool
238+
239+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
240+
capturedTokenInfo, tokenInfoCaptured = ghcontext.GetTokenInfo(r.Context())
241+
w.WriteHeader(http.StatusOK)
242+
})
243+
244+
middleware := ExtractUserToken(nil, "ghp_defaulttokenxxxxxxxxxxxxxxxxxxxxxxxx")
245+
handler := middleware(nextHandler)
246+
247+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
248+
rr := httptest.NewRecorder()
249+
250+
handler.ServeHTTP(rr, req)
251+
252+
assert.Equal(t, http.StatusOK, rr.Code)
253+
require.True(t, tokenInfoCaptured)
254+
require.NotNil(t, capturedTokenInfo)
255+
assert.Equal(t, utils.TokenTypePersonalAccessToken, capturedTokenInfo.TokenType)
256+
assert.Equal(t, "ghp_defaulttokenxxxxxxxxxxxxxxxxxxxxxxxx", capturedTokenInfo.Token)
257+
}
258+
259+
func TestExtractUserToken_RequestTokenOverridesDefaultToken(t *testing.T) {
260+
var capturedTokenInfo *ghcontext.TokenInfo
261+
var tokenInfoCaptured bool
262+
263+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
264+
capturedTokenInfo, tokenInfoCaptured = ghcontext.GetTokenInfo(r.Context())
265+
w.WriteHeader(http.StatusOK)
266+
})
267+
268+
middleware := ExtractUserToken(nil, "ghp_defaulttokenxxxxxxxxxxxxxxxxxxxxxxxx")
269+
handler := middleware(nextHandler)
270+
271+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
272+
req.Header.Set(headers.AuthorizationHeader, "Bearer gho_requesttokenxxxxxxxxxxxxxxxxxxxxxxxx")
273+
rr := httptest.NewRecorder()
274+
275+
handler.ServeHTTP(rr, req)
276+
277+
assert.Equal(t, http.StatusOK, rr.Code)
278+
require.True(t, tokenInfoCaptured)
279+
require.NotNil(t, capturedTokenInfo)
280+
assert.Equal(t, utils.TokenTypeOAuthAccessToken, capturedTokenInfo.TokenType)
281+
assert.Equal(t, "gho_requesttokenxxxxxxxxxxxxxxxxxxxxxxxx", capturedTokenInfo.Token)
282+
}
283+
235284
func TestExtractUserToken_MissingAuthHeader_WWWAuthenticateFormat(t *testing.T) {
236285
oauthCfg := &oauth.Config{
237286
BaseURL: "https://api.example.com",

pkg/http/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ type ServerConfig struct {
3232
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
3333
Host string
3434

35+
// GitHub Token to use for requests that do not provide Authorization.
36+
// If empty, HTTP requests must provide their own Authorization header.
37+
Token string
38+
3539
// Port to listen on (default: 8082)
3640
Port int
3741

pkg/utils/token.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,22 @@ func ParseAuthorizationHeader(req *http.Request) (tokenType TokenType, token str
6060
}
6161
}
6262

63+
tokenType, err := ParseToken(token)
64+
return tokenType, token, err
65+
}
66+
67+
// ParseToken identifies a GitHub API token type from the token value.
68+
func ParseToken(token string) (TokenType, error) {
6369
for prefix, tokenType := range supportedGitHubPrefixes {
6470
if strings.HasPrefix(token, prefix) {
65-
return tokenType, token, nil
71+
return tokenType, nil
6672
}
6773
}
6874

6975
matchesOldTokenPattern := oldPatternRegexp.MatchString(token)
7076
if matchesOldTokenPattern {
71-
return TokenTypePersonalAccessToken, token, nil
77+
return TokenTypePersonalAccessToken, nil
7278
}
7379

74-
return 0, "", ErrBadAuthorizationHeader
80+
return 0, ErrBadAuthorizationHeader
7581
}

0 commit comments

Comments
 (0)