Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
370ebca
initial oauth metadata implementation
mattdholloway Jan 22, 2026
0a1b701
add nolint for GetEffectiveHostAndScheme
mattdholloway Jan 22, 2026
afda19b
remove CAPI reference
mattdholloway Jan 23, 2026
97859a1
remove nonsensical example URL
mattdholloway Jan 23, 2026
f8f109c
anonymize
mattdholloway Jan 23, 2026
9f308b3
add oauth tests
mattdholloway Jan 23, 2026
9b5c2fb
Merge branch 'http-stack-2' into oauth-handler-implementation
mattdholloway Jan 23, 2026
50227bf
replace custom protected resource metadata handler with our own
mattdholloway Jan 26, 2026
a3135d9
remove unused header
mattdholloway Jan 26, 2026
1ce01df
Update pkg/http/oauth/oauth.go
mattdholloway Jan 26, 2026
4fc6c3a
pass oauth config to mcp handler for token extraction
mattdholloway Jan 28, 2026
b0bddbf
chore: retrigger ci
mattdholloway Jan 28, 2026
6c5102a
align types with base branch
mattdholloway Jan 28, 2026
3daa5c3
update more types
mattdholloway Jan 28, 2026
e3c565a
initial oauth metadata implementation
mattdholloway Jan 22, 2026
f768eda
add nolint for GetEffectiveHostAndScheme
mattdholloway Jan 22, 2026
68e1f50
remove CAPI reference
mattdholloway Jan 23, 2026
67b821c
remove nonsensical example URL
mattdholloway Jan 23, 2026
7c90050
anonymize
mattdholloway Jan 23, 2026
78f1a82
add oauth tests
mattdholloway Jan 23, 2026
e2699c8
replace custom protected resource metadata handler with our own
mattdholloway Jan 26, 2026
9c21eed
Update pkg/http/oauth/oauth.go
mattdholloway Jan 26, 2026
49191a9
chore: retrigger ci
mattdholloway Jan 28, 2026
03a5082
update more types
mattdholloway Jan 28, 2026
37c32c5
Merge branch 'oauth-handler-implementation' of https://github.com/git…
mattdholloway Jan 28, 2026
97092a0
remove CAPI specific header
mattdholloway Jan 28, 2026
cfea762
restore mcp path specific logic
mattdholloway Jan 28, 2026
840b41e
implement better resource path handling for OAuth server
mattdholloway Jan 29, 2026
203ebb3
return auth handler to lib version
mattdholloway Jan 29, 2026
3990325
rename to base-path flag
mattdholloway Jan 29, 2026
7abbc53
switch to chi group
mattdholloway Jan 29, 2026
4d0bdbc
make viper commands http only
mattdholloway Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ var (
Version: version,
Host: viper.GetString("host"),
Port: viper.GetInt("port"),
BaseURL: viper.GetString("base-url"),
ResourcePath: viper.GetString("base-path"),
ExportTranslations: viper.GetBool("export-translations"),
EnableCommandLogging: viper.GetBool("enable-command-logging"),
LogFilePath: viper.GetString("log-file"),
Expand Down Expand Up @@ -134,7 +136,11 @@ func init() {
rootCmd.PersistentFlags().Bool("lockdown-mode", false, "Enable lockdown mode")
rootCmd.PersistentFlags().Bool("insiders", false, "Enable insiders features")
rootCmd.PersistentFlags().Duration("repo-access-cache-ttl", 5*time.Minute, "Override the repo access cache TTL (e.g. 1m, 0s to disable)")
rootCmd.PersistentFlags().Int("port", 8082, "HTTP server port")

// HTTP-specific flags
httpCmd.Flags().Int("port", 8082, "HTTP server port")
httpCmd.Flags().String("base-url", "", "Base URL where this server is publicly accessible (for OAuth resource metadata)")
httpCmd.Flags().String("base-path", "", "Externally visible base path for the HTTP server (for OAuth resource metadata)")

// Bind flag to viper
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
Expand All @@ -150,7 +156,9 @@ func init() {
_ = viper.BindPFlag("lockdown-mode", rootCmd.PersistentFlags().Lookup("lockdown-mode"))
_ = viper.BindPFlag("insiders", rootCmd.PersistentFlags().Lookup("insiders"))
_ = viper.BindPFlag("repo-access-cache-ttl", rootCmd.PersistentFlags().Lookup("repo-access-cache-ttl"))
_ = viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
_ = viper.BindPFlag("port", httpCmd.Flags().Lookup("port"))
_ = viper.BindPFlag("base-url", httpCmd.Flags().Lookup("base-url"))
_ = viper.BindPFlag("base-path", httpCmd.Flags().Lookup("base-path"))

// Add subcommands
rootCmd.AddCommand(stdioCmd)
Expand Down
14 changes: 11 additions & 3 deletions pkg/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/github/github-mcp-server/pkg/http/middleware"
"github.com/github/github-mcp-server/pkg/http/oauth"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/go-chi/chi/v5"
Expand All @@ -26,11 +27,13 @@ type Handler struct {
t translations.TranslationHelperFunc
githubMcpServerFactory GitHubMCPServerFactoryFunc
inventoryFactoryFunc InventoryFactoryFunc
oauthCfg *oauth.Config
}

type HandlerOptions struct {
GitHubMcpServerFactory GitHubMCPServerFactoryFunc
InventoryFactory InventoryFactoryFunc
OAuthConfig *oauth.Config
}

type HandlerOption func(*HandlerOptions)
Expand All @@ -47,6 +50,12 @@ func WithInventoryFactory(f InventoryFactoryFunc) HandlerOption {
}
}

func WithOAuthConfig(cfg *oauth.Config) HandlerOption {
return func(o *HandlerOptions) {
o.OAuthConfig = cfg
}
}

func NewHTTPMcpHandler(
ctx context.Context,
cfg *ServerConfig,
Expand Down Expand Up @@ -77,14 +86,13 @@ func NewHTTPMcpHandler(
t: t,
githubMcpServerFactory: githubMcpServerFactory,
inventoryFactoryFunc: inventoryFactory,
oauthCfg: opts.OAuthConfig,
}
}

// RegisterRoutes registers the routes for the MCP server
// URL-based values take precedence over header-based values
func (h *Handler) RegisterRoutes(r chi.Router) {
r.Use(middleware.WithRequestConfig)

r.Mount("/", h)
// Mount readonly and toolset routes
r.With(withToolset).Mount("/x/{toolset}", h)
Expand Down Expand Up @@ -134,7 +142,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Stateless: true,
})

middleware.ExtractUserToken()(mcpHandler).ServeHTTP(w, r)
middleware.ExtractUserToken(h.oauthCfg)(mcpHandler).ServeHTTP(w, r)
}

func DefaultGitHubMCPServerFactory(r *http.Request, deps github.ToolDependencies, inventory *inventory.Inventory, cfg *github.MCPServerConfig) (*mcp.Server, error) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/github/github-mcp-server/pkg/http/middleware"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -262,6 +263,7 @@ func TestHTTPHandlerRoutes(t *testing.T) {

// Create router and register routes
r := chi.NewRouter()
r.Use(middleware.WithRequestConfig)
handler.RegisterRoutes(r)

// Create request
Expand Down
5 changes: 5 additions & 0 deletions pkg/http/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const (
// RealIPHeader is a standard HTTP Header used to indicate the real IP address of the client.
RealIPHeader = "X-Real-IP"

// ForwardedHostHeader is a standard HTTP Header for preserving the original Host header when proxying.
ForwardedHostHeader = "X-Forwarded-Host"
// ForwardedProtoHeader is a standard HTTP Header for preserving the original protocol when proxying.
ForwardedProtoHeader = "X-Forwarded-Proto"

// RequestHmacHeader is used to authenticate requests to the Raw API.
RequestHmacHeader = "Request-Hmac"

Expand Down
15 changes: 13 additions & 2 deletions pkg/http/middleware/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
ghcontext "github.com/github/github-mcp-server/pkg/context"
httpheaders "github.com/github/github-mcp-server/pkg/http/headers"
"github.com/github/github-mcp-server/pkg/http/mark"
"github.com/github/github-mcp-server/pkg/http/oauth"
)

type authType int
Expand Down Expand Up @@ -39,14 +40,14 @@ var supportedThirdPartyTokenPrefixes = []string{
// were 40 characters long and only contained the characters a-f and 0-9.
var oldPatternRegexp = regexp.MustCompile(`\A[a-f0-9]{40}\z`)

func ExtractUserToken() func(next http.Handler) http.Handler {
func ExtractUserToken(oauthCfg *oauth.Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, token, err := parseAuthorizationHeader(r)
if err != nil {
// For missing Authorization header, return 401 with WWW-Authenticate header per MCP spec
if errors.Is(err, errMissingAuthorizationHeader) {
// sendAuthChallenge(w, r, cfg, obsv)
sendAuthChallenge(w, r, oauthCfg)
return
}
// For other auth errors (bad format, unsupported), return 400
Expand All @@ -62,6 +63,16 @@ func ExtractUserToken() func(next http.Handler) http.Handler {
})
}
}

// sendAuthChallenge sends a 401 Unauthorized response with WWW-Authenticate header
// containing the OAuth protected resource metadata URL as per RFC 6750 and MCP spec.
func sendAuthChallenge(w http.ResponseWriter, r *http.Request, oauthCfg *oauth.Config) {
resourcePath := oauth.ResolveResourcePath(r, oauthCfg)
resourceMetadataURL := oauth.BuildResourceMetadataURL(r, oauthCfg, resourcePath)
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer resource_metadata=%q`, resourceMetadataURL))
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}

func parseAuthorizationHeader(req *http.Request) (authType authType, token string, _ error) {
authHeader := req.Header.Get(httpheaders.AuthorizationHeader)
if authHeader == "" {
Expand Down
Loading