package middleware
import (
"bytes"
"encoding/json"
"io"
"net/http"
ghcontext "github.com/github/github-mcp-server/pkg/context"
)
// mcpJSONRPCRequest represents the structure of an MCP JSON-RPC request.
// We only parse the fields needed for routing and optimization.
type mcpJSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params struct {
// For tools/call
Name string `json:"name,omitempty"`
Arguments json.RawMessage `json:"arguments,omitempty"`
// For prompts/get
// Name is shared with tools/call
// For resources/read
URI string `json:"uri,omitempty"`
} `json:"params"`
}
// WithMCPParse creates a middleware that parses MCP JSON-RPC requests early in the
// request lifecycle and stores the parsed information in the request context.
// This enables:
// - Registry filtering via ForMCPRequest (only register needed tools/resources/prompts)
// - Avoiding duplicate JSON parsing in downstream middlewares
// - Access to owner/repo for secret-scanning middleware
//
// The middleware reads the request body, parses it, restores the body for downstream
// handlers, and stores the parsed MCPMethodInfo in the request context.
func WithMCPParse() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Skip health check endpoints
if r.URL.Path == "/_ping" {
next.ServeHTTP(w, r)
return
}
// Only parse POST requests (MCP uses JSON-RPC over POST)
if r.Method != http.MethodPost {
next.ServeHTTP(w, r)
return
}
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
// Log but continue - don't block requests on parse errors
next.ServeHTTP(w, r)
return
}
// Restore the body for downstream handlers
r.Body = io.NopCloser(bytes.NewReader(body))
// Skip empty bodies
if len(body) == 0 {
next.ServeHTTP(w, r)
return
}
// Parse the JSON-RPC request
var mcpReq mcpJSONRPCRequest
err = json.Unmarshal(body, &mcpReq)
if err != nil {
// Log but continue - could be a non-MCP request or malformed JSON
next.ServeHTTP(w, r)
return
}
// Skip if not a valid JSON-RPC 2.0 request
if mcpReq.JSONRPC != "2.0" || mcpReq.Method == "" {
next.ServeHTTP(w, r)
return
}
// Build the MCPMethodInfo
methodInfo := &ghcontext.MCPMethodInfo{
Method: mcpReq.Method,
}
// Extract item name based on method type
switch mcpReq.Method {
case "tools/call":
methodInfo.ItemName = mcpReq.Params.Name
// Parse arguments if present
if len(mcpReq.Params.Arguments) > 0 {
var args map[string]any
err := json.Unmarshal(mcpReq.Params.Arguments, &args)
if err == nil {
methodInfo.Arguments = args
// Extract owner and repo if present
if owner, ok := args["owner"].(string); ok {
methodInfo.Owner = owner
}
if repo, ok := args["repo"].(string); ok {
methodInfo.Repo = repo
}
}
}
case "prompts/get":
methodInfo.ItemName = mcpReq.Params.Name
case "resources/read":
methodInfo.ItemName = mcpReq.Params.URI
default:
// Whatever
}
// Store the parsed info in context
ctx = ghcontext.WithMCPMethodInfo(ctx, methodInfo)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
}