Skip to content

Commit ae80f2a

Browse files
Merge branch 'http-stack-2' into mcp-ui-apps-3-http-stack
2 parents 3096894 + 4c1ad6b commit ae80f2a

29 files changed

+1727
-174
lines changed

cmd/github-mcp-server/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ var (
109109
ContentWindowSize: viper.GetInt("content-window-size"),
110110
LockdownMode: viper.GetBool("lockdown-mode"),
111111
RepoAccessCacheTTL: &ttl,
112+
ScopeChallenge: viper.GetBool("scope-challenge"),
112113
}
113114

114115
return ghhttp.RunHTTPServer(httpConfig)
@@ -141,6 +142,7 @@ func init() {
141142
httpCmd.Flags().Int("port", 8082, "HTTP server port")
142143
httpCmd.Flags().String("base-url", "", "Base URL where this server is publicly accessible (for OAuth resource metadata)")
143144
httpCmd.Flags().String("base-path", "", "Externally visible base path for the HTTP server (for OAuth resource metadata)")
145+
httpCmd.Flags().Bool("scope-challenge", false, "Enable OAuth scope challenge responses and tool filtering based on token scopes")
144146

145147
// Bind flag to viper
146148
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
@@ -159,7 +161,7 @@ func init() {
159161
_ = viper.BindPFlag("port", httpCmd.Flags().Lookup("port"))
160162
_ = viper.BindPFlag("base-url", httpCmd.Flags().Lookup("base-url"))
161163
_ = viper.BindPFlag("base-path", httpCmd.Flags().Lookup("base-path"))
162-
164+
_ = viper.BindPFlag("scope-challenge", httpCmd.Flags().Lookup("scope-challenge"))
163165
// Add subcommands
164166
rootCmd.AddCommand(stdioCmd)
165167
rootCmd.AddCommand(httpCmd)

internal/ghmcp/server.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -374,14 +374,7 @@ func fetchTokenScopesForHost(ctx context.Context, token, host string) ([]string,
374374
return nil, fmt.Errorf("failed to parse API host: %w", err)
375375
}
376376

377-
baseRestURL, err := apiHost.BaseRESTURL(ctx)
378-
if err != nil {
379-
return nil, fmt.Errorf("failed to get base REST URL: %w", err)
380-
}
381-
382-
fetcher := scopes.NewFetcher(scopes.FetcherOptions{
383-
APIHost: baseRestURL.String(),
384-
})
377+
fetcher := scopes.NewFetcher(apiHost, scopes.FetcherOptions{})
385378

386379
return fetcher.FetchTokenScopes(ctx, token)
387380
}

pkg/context/mcp_info.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package context
2+
3+
import "context"
4+
5+
type mcpMethodInfoCtx string
6+
7+
var mcpMethodInfoCtxKey mcpMethodInfoCtx = "mcpmethodinfo"
8+
9+
// MCPMethodInfo contains pre-parsed MCP method information extracted from the JSON-RPC request.
10+
// This is populated early in the request lifecycle to enable:
11+
// - Inventory filtering via ForMCPRequest (only register needed tools/resources/prompts)
12+
// - Avoiding duplicate JSON parsing in middlewares (secret-scanning, scope-challenge)
13+
// - Performance optimization for per-request server creation
14+
type MCPMethodInfo struct {
15+
// Method is the MCP method being called (e.g., "tools/call", "tools/list", "initialize")
16+
Method string
17+
// ItemName is the name of the specific item being accessed (tool name, resource URI, prompt name)
18+
// Only populated for call/get methods (tools/call, prompts/get, resources/read)
19+
ItemName string
20+
// Owner is the repository owner from tool call arguments, if present
21+
Owner string
22+
// Repo is the repository name from tool call arguments, if present
23+
Repo string
24+
// Arguments contains the raw tool arguments for tools/call requests
25+
Arguments map[string]any
26+
}
27+
28+
// WithMCPMethodInfo stores the MCPMethodInfo in the context.
29+
func WithMCPMethodInfo(ctx context.Context, info *MCPMethodInfo) context.Context {
30+
return context.WithValue(ctx, mcpMethodInfoCtxKey, info)
31+
}
32+
33+
// MCPMethod retrieves the MCPMethodInfo from the context.
34+
func MCPMethod(ctx context.Context) (*MCPMethodInfo, bool) {
35+
if info, ok := ctx.Value(mcpMethodInfoCtxKey).(*MCPMethodInfo); ok {
36+
return info, true
37+
}
38+
return nil, false
39+
}

pkg/context/token.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
package context
22

3-
import "context"
3+
import (
4+
"context"
5+
6+
"github.com/github/github-mcp-server/pkg/utils"
7+
)
48

59
// tokenCtxKey is a context key for authentication token information
6-
type tokenCtxKey struct{}
10+
type tokenCtx string
11+
12+
var tokenCtxKey tokenCtx = "tokenctx"
13+
14+
type TokenInfo struct {
15+
Token string
16+
TokenType utils.TokenType
17+
ScopesFetched bool
18+
Scopes []string
19+
}
720

821
// WithTokenInfo adds TokenInfo to the context
9-
func WithTokenInfo(ctx context.Context, token string) context.Context {
10-
return context.WithValue(ctx, tokenCtxKey{}, token)
22+
func WithTokenInfo(ctx context.Context, tokenInfo *TokenInfo) context.Context {
23+
return context.WithValue(ctx, tokenCtxKey, tokenInfo)
1124
}
1225

1326
// GetTokenInfo retrieves the authentication token from the context
14-
func GetTokenInfo(ctx context.Context) (string, bool) {
15-
if token, ok := ctx.Value(tokenCtxKey{}).(string); ok {
16-
return token, true
27+
func GetTokenInfo(ctx context.Context) (*TokenInfo, bool) {
28+
if tokenInfo, ok := ctx.Value(tokenCtxKey).(*TokenInfo); ok {
29+
return tokenInfo, true
1730
}
18-
return "", false
31+
return nil, false
1932
}

pkg/github/dependencies.go

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,6 @@ func NewToolFromHandler(
237237
}
238238

239239
type RequestDeps struct {
240-
Client *gogithub.Client
241-
GQLClient *githubv4.Client
242-
RawClient *raw.Client
243-
RepoAccessCache *lockdown.RepoAccessCache
244-
245240
// Static dependencies
246241
apiHosts utils.APIHostResolver
247242
version string
@@ -277,12 +272,12 @@ func NewRequestDeps(
277272

278273
// GetClient implements ToolDependencies.
279274
func (d *RequestDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {
280-
if d.Client != nil {
281-
return d.Client, nil
282-
}
283-
284275
// extract the token from the context
285-
token, _ := ghcontext.GetTokenInfo(ctx)
276+
tokenInfo, ok := ghcontext.GetTokenInfo(ctx)
277+
if !ok {
278+
return nil, fmt.Errorf("no token info in context")
279+
}
280+
token := tokenInfo.Token
286281

287282
baseRestURL, err := d.apiHosts.BaseRESTURL(ctx)
288283
if err != nil {
@@ -303,12 +298,12 @@ func (d *RequestDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {
303298

304299
// GetGQLClient implements ToolDependencies.
305300
func (d *RequestDeps) GetGQLClient(ctx context.Context) (*githubv4.Client, error) {
306-
if d.GQLClient != nil {
307-
return d.GQLClient, nil
308-
}
309-
310301
// extract the token from the context
311-
token, _ := ghcontext.GetTokenInfo(ctx)
302+
tokenInfo, ok := ghcontext.GetTokenInfo(ctx)
303+
if !ok {
304+
return nil, fmt.Errorf("no token info in context")
305+
}
306+
token := tokenInfo.Token
312307

313308
// Construct GraphQL client
314309
// We use NewEnterpriseClient unconditionally since we already parsed the API host
@@ -329,16 +324,11 @@ func (d *RequestDeps) GetGQLClient(ctx context.Context) (*githubv4.Client, error
329324
}
330325

331326
gqlClient := githubv4.NewEnterpriseClient(graphqlURL.String(), gqlHTTPClient)
332-
d.GQLClient = gqlClient
333327
return gqlClient, nil
334328
}
335329

336330
// GetRawClient implements ToolDependencies.
337331
func (d *RequestDeps) GetRawClient(ctx context.Context) (*raw.Client, error) {
338-
if d.RawClient != nil {
339-
return d.RawClient, nil
340-
}
341-
342332
client, err := d.GetClient(ctx)
343333
if err != nil {
344334
return nil, err
@@ -350,7 +340,6 @@ func (d *RequestDeps) GetRawClient(ctx context.Context) (*raw.Client, error) {
350340
}
351341

352342
rawClient := raw.NewClient(client, rawURL)
353-
d.RawClient = rawClient
354343

355344
return rawClient, nil
356345
}
@@ -361,18 +350,13 @@ func (d *RequestDeps) GetRepoAccessCache(ctx context.Context) (*lockdown.RepoAcc
361350
return nil, nil
362351
}
363352

364-
if d.RepoAccessCache != nil {
365-
return d.RepoAccessCache, nil
366-
}
367-
368353
gqlClient, err := d.GetGQLClient(ctx)
369354
if err != nil {
370355
return nil, err
371356
}
372357

373358
// Create repo access cache
374359
instance := lockdown.GetInstance(gqlClient, d.RepoAccessOpts...)
375-
d.RepoAccessCache = instance
376360
return instance, nil
377361
}
378362

pkg/github/pullrequests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,7 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
944944
})
945945
}
946946

947-
// AddReplyToPullRequestComment creates a tool to add a reply to an existing PR comment.
947+
// AddReplyToPullRequestComment creates a tool to add a reply to an existing pull request comment.
948948
func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventory.ServerTool {
949949
schema := &jsonschema.Schema{
950950
Type: "object",

pkg/github/server.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ type MCPServerConfig struct {
7373

7474
type MCPServerOption func(*mcp.ServerOptions)
7575

76-
func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependencies, inventory *inventory.Inventory) (*mcp.Server, error) {
76+
func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependencies, inv *inventory.Inventory) (*mcp.Server, error) {
7777
// Create the MCP server
7878
serverOpts := &mcp.ServerOptions{
79-
Instructions: inventory.Instructions(),
79+
Instructions: inv.Instructions(),
8080
Logger: cfg.Logger,
8181
CompletionHandler: CompletionsHandler(deps.GetClient),
8282
}
@@ -102,20 +102,20 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
102102
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
103103
ghServer.AddReceivingMiddleware(InjectDepsMiddleware(deps))
104104

105-
if unrecognized := inventory.UnrecognizedToolsets(); len(unrecognized) > 0 {
105+
if unrecognized := inv.UnrecognizedToolsets(); len(unrecognized) > 0 {
106106
cfg.Logger.Warn("Warning: unrecognized toolsets ignored", "toolsets", strings.Join(unrecognized, ", "))
107107
}
108108

109109
// Register GitHub tools/resources/prompts from the inventory.
110110
// In dynamic mode with no explicit toolsets, this is a no-op since enabledToolsets
111111
// is empty - users enable toolsets at runtime via the dynamic tools below (but can
112112
// enable toolsets or tools explicitly that do need registration).
113-
inventory.RegisterAll(ctx, ghServer, deps)
113+
inv.RegisterAll(ctx, ghServer, deps)
114114

115115
// Register dynamic toolset management tools (enable/disable) - these are separate
116116
// meta-tools that control the inventory, not part of the inventory itself
117117
if cfg.DynamicToolsets {
118-
registerDynamicTools(ghServer, inventory, deps, cfg.Translator)
118+
registerDynamicTools(ghServer, inv, deps, cfg.Translator)
119119
}
120120

121121
return ghServer, nil

pkg/github/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func TestNewMCPServer_CreatesSuccessfully(t *testing.T) {
135135
require.NoError(t, err, "expected inventory build to succeed")
136136

137137
// Create the server
138-
server, err := NewMCPServer(t.Context(), &cfg, deps, inv)
138+
server, err := NewMCPServer(context.Background(), &cfg, deps, inv)
139139
require.NoError(t, err, "expected server creation to succeed")
140140
require.NotNil(t, server, "expected server to be non-nil")
141141

0 commit comments

Comments
 (0)