-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Scope challenge http #1925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scope challenge http #1925
Changes from all commits
370ebca
0a1b701
afda19b
97859a1
f8f109c
9f308b3
9b5c2fb
50227bf
a3135d9
1ce01df
4fc6c3a
b0bddbf
6c5102a
3daa5c3
e3c565a
f768eda
68e1f50
67b821c
7c90050
78f1a82
e2699c8
9c21eed
49191a9
03a5082
37c32c5
97092a0
cfea762
199e62c
840b41e
203ebb3
3990325
4b690f5
7801dc5
4d679fd
9a338d7
7f6e0e8
45ff914
2b016e5
f6d4337
2b1b9bb
dad1e71
ce05b87
ab6a5ca
cb957d0
2871761
50e33a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package context | ||
|
|
||
| import "context" | ||
|
|
||
| type mcpMethodInfoCtx string | ||
|
|
||
| var mcpMethodInfoCtxKey mcpMethodInfoCtx = "mcpmethodinfo" | ||
|
|
||
| // MCPMethodInfo contains pre-parsed MCP method information extracted from the JSON-RPC request. | ||
| // This is populated early in the request lifecycle to enable: | ||
| // - Inventory filtering via ForMCPRequest (only register needed tools/resources/prompts) | ||
| // - Avoiding duplicate JSON parsing in middlewares (secret-scanning, scope-challenge) | ||
| // - Performance optimization for per-request server creation | ||
| type MCPMethodInfo struct { | ||
| // Method is the MCP method being called (e.g., "tools/call", "tools/list", "initialize") | ||
| Method string | ||
| // ItemName is the name of the specific item being accessed (tool name, resource URI, prompt name) | ||
| // Only populated for call/get methods (tools/call, prompts/get, resources/read) | ||
| ItemName string | ||
| // Owner is the repository owner from tool call arguments, if present | ||
| Owner string | ||
| // Repo is the repository name from tool call arguments, if present | ||
| Repo string | ||
| // Arguments contains the raw tool arguments for tools/call requests | ||
| Arguments map[string]any | ||
| } | ||
|
|
||
| // WithMCPMethodInfo stores the MCPMethodInfo in the context. | ||
| func WithMCPMethodInfo(ctx context.Context, info *MCPMethodInfo) context.Context { | ||
| return context.WithValue(ctx, mcpMethodInfoCtxKey, info) | ||
| } | ||
|
|
||
| // MCPMethod retrieves the MCPMethodInfo from the context. | ||
| func MCPMethod(ctx context.Context) (*MCPMethodInfo, bool) { | ||
| if info, ok := ctx.Value(mcpMethodInfoCtxKey).(*MCPMethodInfo); ok { | ||
| return info, true | ||
| } | ||
| return nil, false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,39 @@ | ||
| package context | ||
|
|
||
| import "context" | ||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/github/github-mcp-server/pkg/utils" | ||
| ) | ||
|
|
||
| // tokenCtxKey is a context key for authentication token information | ||
| type tokenCtxKey struct{} | ||
| type tokenCtx string | ||
|
|
||
| var tokenCtxKey tokenCtx = "tokenctx" | ||
|
|
||
| type TokenInfo struct { | ||
| Token string | ||
| TokenType utils.TokenType | ||
| ScopesFetched bool | ||
| Scopes []string | ||
| } | ||
|
|
||
| // WithTokenInfo adds TokenInfo to the context | ||
| func WithTokenInfo(ctx context.Context, token string) context.Context { | ||
| return context.WithValue(ctx, tokenCtxKey{}, token) | ||
| func WithTokenInfo(ctx context.Context, tokenInfo *TokenInfo) context.Context { | ||
| return context.WithValue(ctx, tokenCtxKey, tokenInfo) | ||
| } | ||
|
|
||
| func SetTokenScopes(ctx context.Context, scopes []string) { | ||
| if tokenInfo, ok := GetTokenInfo(ctx); ok { | ||
| tokenInfo.Scopes = scopes | ||
| tokenInfo.ScopesFetched = true | ||
| } | ||
| } | ||
|
|
||
| // GetTokenInfo retrieves the authentication token from the context | ||
| func GetTokenInfo(ctx context.Context) (string, bool) { | ||
| if token, ok := ctx.Value(tokenCtxKey{}).(string); ok { | ||
| return token, true | ||
| func GetTokenInfo(ctx context.Context) (*TokenInfo, bool) { | ||
| if tokenInfo, ok := ctx.Value(tokenCtxKey).(*TokenInfo); ok { | ||
| return tokenInfo, true | ||
| } | ||
| return "", false | ||
| return nil, false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,9 @@ import ( | |
| "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/scopes" | ||
| "github.com/github/github-mcp-server/pkg/translations" | ||
| "github.com/github/github-mcp-server/pkg/utils" | ||
| "github.com/go-chi/chi/v5" | ||
| "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| ) | ||
|
|
@@ -23,21 +25,30 @@ type Handler struct { | |
| config *ServerConfig | ||
| deps github.ToolDependencies | ||
| logger *slog.Logger | ||
| apiHosts utils.APIHostResolver | ||
| t translations.TranslationHelperFunc | ||
| githubMcpServerFactory GitHubMCPServerFactoryFunc | ||
| inventoryFactoryFunc InventoryFactoryFunc | ||
| oauthCfg *oauth.Config | ||
| scopeFetcher scopes.FetcherInterface | ||
| } | ||
|
|
||
| type HandlerOptions struct { | ||
| GitHubMcpServerFactory GitHubMCPServerFactoryFunc | ||
| InventoryFactory InventoryFactoryFunc | ||
| OAuthConfig *oauth.Config | ||
| ScopeFetcher scopes.FetcherInterface | ||
| FeatureChecker inventory.FeatureFlagChecker | ||
| } | ||
|
|
||
| type HandlerOption func(*HandlerOptions) | ||
|
|
||
| func WithScopeFetcher(f scopes.FetcherInterface) HandlerOption { | ||
| return func(o *HandlerOptions) { | ||
| o.ScopeFetcher = f | ||
| } | ||
| } | ||
|
|
||
| func WithGitHubMCPServerFactory(f GitHubMCPServerFactoryFunc) HandlerOption { | ||
| return func(o *HandlerOptions) { | ||
| o.GitHubMcpServerFactory = f | ||
|
|
@@ -68,6 +79,7 @@ func NewHTTPMcpHandler( | |
| deps github.ToolDependencies, | ||
| t translations.TranslationHelperFunc, | ||
| logger *slog.Logger, | ||
| apiHost utils.APIHostResolver, | ||
| options ...HandlerOption) *Handler { | ||
| opts := &HandlerOptions{} | ||
| for _, o := range options { | ||
|
|
@@ -79,28 +91,40 @@ func NewHTTPMcpHandler( | |
| githubMcpServerFactory = DefaultGitHubMCPServerFactory | ||
| } | ||
|
|
||
| scopeFetcher := opts.ScopeFetcher | ||
| if scopeFetcher == nil { | ||
| scopeFetcher = scopes.NewFetcher(apiHost, scopes.FetcherOptions{}) | ||
| } | ||
|
|
||
| inventoryFactory := opts.InventoryFactory | ||
| if inventoryFactory == nil { | ||
| inventoryFactory = DefaultInventoryFactory(cfg, t, opts.FeatureChecker) | ||
| inventoryFactory = DefaultInventoryFactory(cfg, t, opts.FeatureChecker, scopeFetcher) | ||
| } | ||
|
|
||
| return &Handler{ | ||
| ctx: ctx, | ||
| config: cfg, | ||
| deps: deps, | ||
| logger: logger, | ||
| apiHosts: apiHost, | ||
| t: t, | ||
| githubMcpServerFactory: githubMcpServerFactory, | ||
| inventoryFactoryFunc: inventoryFactory, | ||
| oauthCfg: opts.OAuthConfig, | ||
| scopeFetcher: scopeFetcher, | ||
| } | ||
| } | ||
|
|
||
| func (h *Handler) RegisterMiddleware(r chi.Router) { | ||
| r.Use( | ||
| middleware.ExtractUserToken(h.oauthCfg), | ||
| middleware.WithRequestConfig, | ||
| middleware.WithMCPParse(), | ||
| ) | ||
|
|
||
| if h.config.ScopeChallenge { | ||
| r.Use(middleware.WithScopeChallenge(h.oauthCfg, h.scopeFetcher)) | ||
| } | ||
| } | ||
|
|
||
| // RegisterRoutes registers the routes for the MCP server | ||
|
|
@@ -145,22 +169,38 @@ func withInsiders(next http.Handler) http.Handler { | |
| } | ||
|
|
||
| func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
| inventory, err := h.inventoryFactoryFunc(r) | ||
| inv, err := h.inventoryFactoryFunc(r) | ||
| if err != nil { | ||
| w.WriteHeader(http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| ghServer, err := h.githubMcpServerFactory(r, h.deps, inventory, &github.MCPServerConfig{ | ||
| invToUse := inv | ||
| if methodInfo, ok := ghcontext.MCPMethod(r.Context()); ok && methodInfo != nil { | ||
| invToUse = inv.ForMCPRequest(methodInfo.Method, methodInfo.ItemName) | ||
| } | ||
|
|
||
| ghServer, err := h.githubMcpServerFactory(r, h.deps, invToUse, &github.MCPServerConfig{ | ||
| Version: h.config.Version, | ||
| Translator: h.t, | ||
| ContentWindowSize: h.config.ContentWindowSize, | ||
| Logger: h.logger, | ||
| RepoAccessTTL: h.config.RepoAccessCacheTTL, | ||
| // Explicitly set empty capabilities. inv.ForMCPRequest currently returns nothing for Initialize. | ||
| ServerOptions: []github.MCPServerOption{ | ||
| func(so *mcp.ServerOptions) { | ||
| so.Capabilities = &mcp.ServerCapabilities{ | ||
| Tools: &mcp.ToolCapabilities{}, | ||
| Resources: &mcp.ResourceCapabilities{}, | ||
| Prompts: &mcp.PromptCapabilities{}, | ||
| } | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| if err != nil { | ||
| w.WriteHeader(http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| mcpHandler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server { | ||
|
|
@@ -177,13 +217,15 @@ func DefaultGitHubMCPServerFactory(r *http.Request, deps github.ToolDependencies | |
| } | ||
|
|
||
| // DefaultInventoryFactory creates the default inventory factory for HTTP mode | ||
| func DefaultInventoryFactory(_ *ServerConfig, t translations.TranslationHelperFunc, featureChecker inventory.FeatureFlagChecker) InventoryFactoryFunc { | ||
| func DefaultInventoryFactory(_ *ServerConfig, t translations.TranslationHelperFunc, featureChecker inventory.FeatureFlagChecker, scopeFetcher scopes.FetcherInterface) InventoryFactoryFunc { | ||
| return func(r *http.Request) (*inventory.Inventory, error) { | ||
| b := github.NewInventory(t). | ||
| WithDeprecatedAliases(github.DeprecatedToolAliases). | ||
| WithFeatureChecker(featureChecker) | ||
|
|
||
| b = InventoryFiltersForRequest(r, b) | ||
| b = PATScopeFilter(b, r, scopeFetcher) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanity check, this is still done in stdio server too right?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this runs slightly differently in STDIO, since you get the scopes at server startup: https://github.com/github/github-mcp-server/blob/scope-challenge-http/internal/ghmcp/server.go#L142-L144 |
||
|
|
||
| b.WithServerInstructions() | ||
|
|
||
| return b.Build() | ||
|
|
@@ -215,3 +257,29 @@ func InventoryFiltersForRequest(r *http.Request, builder *inventory.Builder) *in | |
|
|
||
| return builder | ||
| } | ||
|
|
||
| func PATScopeFilter(b *inventory.Builder, r *http.Request, fetcher scopes.FetcherInterface) *inventory.Builder { | ||
| ctx := r.Context() | ||
|
|
||
| tokenInfo, ok := ghcontext.GetTokenInfo(ctx) | ||
| if !ok || tokenInfo == nil { | ||
| return b | ||
| } | ||
|
|
||
| // Fetch token scopes for scope-based tool filtering (PAT tokens only) | ||
| // Only classic PATs (ghp_ prefix) return OAuth scopes via X-OAuth-Scopes header. | ||
| // Fine-grained PATs and other token types don't support this, so we skip filtering. | ||
| if tokenInfo.TokenType == utils.TokenTypePersonalAccessToken { | ||
| scopesList, err := fetcher.FetchTokenScopes(ctx, tokenInfo.Token) | ||
| if err != nil { | ||
| return b | ||
| } | ||
|
|
||
| // Store fetched scopes in context for downstream use | ||
| ghcontext.SetTokenScopes(ctx, scopesList) | ||
|
|
||
| return b.WithFilter(github.CreateToolScopeFilter(scopesList)) | ||
| } | ||
|
|
||
| return b | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.