Skip to content

Commit 199e62c

Browse files
committed
WIP
1 parent 37c32c5 commit 199e62c

File tree

9 files changed

+470
-91
lines changed

9 files changed

+470
-91
lines changed

cmd/github-mcp-server/main.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ func init() {
136136
rootCmd.PersistentFlags().Bool("insiders", false, "Enable insiders features")
137137
rootCmd.PersistentFlags().Duration("repo-access-cache-ttl", 5*time.Minute, "Override the repo access cache TTL (e.g. 1m, 0s to disable)")
138138
rootCmd.PersistentFlags().Int("port", 8082, "HTTP server port")
139-
rootCmd.PersistentFlags().String("base-url", "", "Base URL where this server is publicly accessible (for OAuth resource metadata)")
139+
140+
// Add port flag to http command
141+
httpCmd.PersistentFlags().Int("port", 8082, "HTTP server port")
142+
httpCmd.PersistentFlags().String("base-url", "", "Base URL where this server is publicly accessible (for OAuth resource metadata)")
140143

141144
// Bind flag to viper
142145
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
@@ -152,8 +155,8 @@ func init() {
152155
_ = viper.BindPFlag("lockdown-mode", rootCmd.PersistentFlags().Lookup("lockdown-mode"))
153156
_ = viper.BindPFlag("insiders", rootCmd.PersistentFlags().Lookup("insiders"))
154157
_ = viper.BindPFlag("repo-access-cache-ttl", rootCmd.PersistentFlags().Lookup("repo-access-cache-ttl"))
155-
_ = viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
156-
_ = viper.BindPFlag("base-url", rootCmd.PersistentFlags().Lookup("base-url"))
158+
_ = viper.BindPFlag("port", httpCmd.PersistentFlags().Lookup("port"))
159+
_ = viper.BindPFlag("base-url", httpCmd.PersistentFlags().Lookup("base-url"))
157160

158161
// Add subcommands
159162
rootCmd.AddCommand(stdioCmd)

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+
// ContextWithMCPMethodInfo stores the MCPMethodInfo in the context.
29+
func ContextWithMCPMethodInfo(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: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
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+
}
718

819
// WithTokenInfo adds TokenInfo to the context
9-
func WithTokenInfo(ctx context.Context, token string) context.Context {
10-
return context.WithValue(ctx, tokenCtxKey{}, token)
20+
func WithTokenInfo(ctx context.Context, token string, tokenType utils.TokenType) context.Context {
21+
return context.WithValue(ctx, tokenCtxKey, TokenInfo{Token: token, TokenType: tokenType})
1122
}
1223

1324
// 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
25+
func GetTokenInfo(ctx context.Context) (TokenInfo, bool) {
26+
if tokenInfo, ok := ctx.Value(tokenCtxKey).(TokenInfo); ok {
27+
return tokenInfo, true
1728
}
18-
return "", false
29+
return TokenInfo{}, false
1930
}

pkg/github/dependencies.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,11 @@ func (d *RequestDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {
283283
}
284284

285285
// extract the token from the context
286-
token, _ := ghcontext.GetTokenInfo(ctx)
286+
tokenInfo, ok := ghcontext.GetTokenInfo(ctx)
287+
if !ok {
288+
return nil, fmt.Errorf("no token info in context")
289+
}
290+
token := tokenInfo.Token
287291

288292
baseRestURL, err := d.apiHosts.BaseRESTURL(ctx)
289293
if err != nil {
@@ -309,7 +313,11 @@ func (d *RequestDeps) GetGQLClient(ctx context.Context) (*githubv4.Client, error
309313
}
310314

311315
// extract the token from the context
312-
token, _ := ghcontext.GetTokenInfo(ctx)
316+
tokenInfo, ok := ghcontext.GetTokenInfo(ctx)
317+
if !ok {
318+
return nil, fmt.Errorf("no token info in context")
319+
}
320+
token := tokenInfo.Token
313321

314322
// Construct GraphQL client
315323
// We use NewEnterpriseClient unconditionally since we already parsed the API host

pkg/github/server.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"time"
1010

11+
ghcontext "github.com/github/github-mcp-server/pkg/context"
1112
gherrors "github.com/github/github-mcp-server/pkg/errors"
1213
"github.com/github/github-mcp-server/pkg/inventory"
1314
"github.com/github/github-mcp-server/pkg/octicons"
@@ -73,10 +74,10 @@ type MCPServerConfig struct {
7374

7475
type MCPServerOption func(*mcp.ServerOptions)
7576

76-
func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependencies, inventory *inventory.Inventory) (*mcp.Server, error) {
77+
func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependencies, inv *inventory.Inventory) (*mcp.Server, error) {
7778
// Create the MCP server
7879
serverOpts := &mcp.ServerOptions{
79-
Instructions: inventory.Instructions(),
80+
Instructions: inv.Instructions(),
8081
Logger: cfg.Logger,
8182
CompletionHandler: CompletionsHandler(deps.GetClient),
8283
}
@@ -102,20 +103,25 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
102103
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
103104
ghServer.AddReceivingMiddleware(InjectDepsMiddleware(deps))
104105

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

110+
invToUse := inv
111+
if methodInfo, ok := ghcontext.MCPMethod(ctx); ok && methodInfo != nil {
112+
invToUse = inv.ForMCPRequest(methodInfo.Method, methodInfo.ItemName)
113+
}
114+
109115
// Register GitHub tools/resources/prompts from the inventory.
110116
// In dynamic mode with no explicit toolsets, this is a no-op since enabledToolsets
111117
// is empty - users enable toolsets at runtime via the dynamic tools below (but can
112118
// enable toolsets or tools explicitly that do need registration).
113-
inventory.RegisterAll(ctx, ghServer, deps)
119+
invToUse.RegisterAll(ctx, ghServer, deps)
114120

115121
// Register dynamic toolset management tools (enable/disable) - these are separate
116122
// meta-tools that control the inventory, not part of the inventory itself
117123
if cfg.DynamicToolsets {
118-
registerDynamicTools(ghServer, inventory, deps, cfg.Translator)
124+
registerDynamicTools(ghServer, invToUse, deps, cfg.Translator)
119125
}
120126

121127
return ghServer, nil

pkg/http/middleware/mcp_parse.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
9+
ghcontext "github.com/github/github-mcp-server/pkg/context"
10+
)
11+
12+
// mcpJSONRPCRequest represents the structure of an MCP JSON-RPC request.
13+
// We only parse the fields needed for routing and optimization.
14+
type mcpJSONRPCRequest struct {
15+
JSONRPC string `json:"jsonrpc"`
16+
Method string `json:"method"`
17+
Params struct {
18+
// For tools/call
19+
Name string `json:"name,omitempty"`
20+
Arguments json.RawMessage `json:"arguments,omitempty"`
21+
// For prompts/get
22+
// Name is shared with tools/call
23+
// For resources/read
24+
URI string `json:"uri,omitempty"`
25+
} `json:"params"`
26+
}
27+
28+
// WithMCPParse creates a middleware that parses MCP JSON-RPC requests early in the
29+
// request lifecycle and stores the parsed information in the request context.
30+
// This enables:
31+
// - Registry filtering via ForMCPRequest (only register needed tools/resources/prompts)
32+
// - Avoiding duplicate JSON parsing in downstream middlewares
33+
// - Access to owner/repo for secret-scanning middleware
34+
//
35+
// The middleware reads the request body, parses it, restores the body for downstream
36+
// handlers, and stores the parsed MCPMethodInfo in the request context.
37+
func WithMCPParse() func(http.Handler) http.Handler {
38+
return func(next http.Handler) http.Handler {
39+
fn := func(w http.ResponseWriter, r *http.Request) {
40+
ctx := r.Context()
41+
42+
// Skip health check endpoints
43+
if r.URL.Path == "/_ping" {
44+
next.ServeHTTP(w, r)
45+
return
46+
}
47+
48+
// Only parse POST requests (MCP uses JSON-RPC over POST)
49+
if r.Method != http.MethodPost {
50+
next.ServeHTTP(w, r)
51+
return
52+
}
53+
54+
// Read the request body
55+
body, err := io.ReadAll(r.Body)
56+
if err != nil {
57+
// Log but continue - don't block requests on parse errors
58+
next.ServeHTTP(w, r)
59+
return
60+
}
61+
62+
// Restore the body for downstream handlers
63+
r.Body = io.NopCloser(bytes.NewReader(body))
64+
65+
// Skip empty bodies
66+
if len(body) == 0 {
67+
next.ServeHTTP(w, r)
68+
return
69+
}
70+
71+
// Parse the JSON-RPC request
72+
var mcpReq mcpJSONRPCRequest
73+
err = json.Unmarshal(body, &mcpReq)
74+
if err != nil {
75+
// Log but continue - could be a non-MCP request or malformed JSON
76+
next.ServeHTTP(w, r)
77+
return
78+
}
79+
80+
// Skip if not a valid JSON-RPC 2.0 request
81+
if mcpReq.JSONRPC != "2.0" || mcpReq.Method == "" {
82+
next.ServeHTTP(w, r)
83+
return
84+
}
85+
86+
// Build the MCPMethodInfo
87+
methodInfo := &ghcontext.MCPMethodInfo{
88+
Method: mcpReq.Method,
89+
}
90+
91+
// Extract item name based on method type
92+
93+
switch mcpReq.Method {
94+
case "tools/call":
95+
methodInfo.ItemName = mcpReq.Params.Name
96+
// Parse arguments if present
97+
if len(mcpReq.Params.Arguments) > 0 {
98+
var args map[string]any
99+
err := json.Unmarshal(mcpReq.Params.Arguments, &args)
100+
if err == nil {
101+
methodInfo.Arguments = args
102+
// Extract owner and repo if present
103+
if owner, ok := args["owner"].(string); ok {
104+
methodInfo.Owner = owner
105+
}
106+
if repo, ok := args["repo"].(string); ok {
107+
methodInfo.Repo = repo
108+
}
109+
}
110+
}
111+
case "prompts/get":
112+
methodInfo.ItemName = mcpReq.Params.Name
113+
case "resources/read":
114+
methodInfo.ItemName = mcpReq.Params.URI
115+
default:
116+
// Whatever
117+
}
118+
119+
// Store the parsed info in context
120+
ctx = ghcontext.ContextWithMCPMethodInfo(ctx, methodInfo)
121+
122+
next.ServeHTTP(w, r.WithContext(ctx))
123+
}
124+
return http.HandlerFunc(fn)
125+
}
126+
}

0 commit comments

Comments
 (0)