127 lines
3.4 KiB
Go
127 lines
3.4 KiB
Go
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)
|
|
}
|
|
}
|