Files
Adam Holt 3956a97e7c WIP
2026-01-12 13:11:09 +01:00

167 lines
4.7 KiB
Go

//go:build ignore
package http
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-chi/chi"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type HTTPHandlerConfig struct {
Version string
}
type MCPHTTPHandler struct {
config HTTPHandlerConfig
serverDependencies serverDependencies
}
// serverDependencies holds the dependencies required by the MCP server.
type serverDependencies struct {
getClient github.GetClientFn
getGQClient github.GetGQLClientFn
getRawClient raw.GetRawClientFn
t translations.TranslationHelperFunc
}
func NewMCPHTTPHandler(ctx context.Context, router chi.Router) (*MCPHTTPHandler, error) {
return &MCPHTTPHandler{}, nil
}
type reqOptions struct {
readonly bool
toolsetRoute bool
}
func (h *MCPHTTPHandler) RegisterRoutes(ctx context.Context, r chi.Router) error {
spanNameFormatter := otelhttp.WithSpanNameFormatter(
func(operation string, r *http.Request) string {
return r.Method + " " + routePattern(r)
},
)
mount := func(pattern string, opts reqOptions) {
r.Mount(pattern, otelhttp.NewHandler(h.perRequestHTTPHandler(opts), pattern, spanNameFormatter))
}
// Always mount the root route as before
mount("/", reqOptions{})
// Use chi params for toolset and readonly
mount("/x/{toolset}", reqOptions{toolsetRoute: true})
mount("/x/{toolset}/readonly", reqOptions{toolsetRoute: true, readonly: true})
mount("/readonly", reqOptions{readonly: true})
return nil
}
func (h *MCPHTTPHandler) perRequestHTTPHandler(opts reqOptions) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Initialize GitHub errors context early in the request lifecycle
ctx := r.Context()
ctx = ghErrors.ContextWithGitHubErrors(ctx)
r = r.WithContext(ctx)
// Set the toolset and readonly params in the context
if opts.readonly {
r = setReadOnly(r)
}
if opts.toolsetRoute {
r = setToolset(r)
}
//nolint:contextcheck // ctx returned from newMCPServerForRequest contains injected deps via ContextWithDeps
ctx, svr, _, err := m.newMCPServerForRequest(r)
if err != nil {
message := fmt.Sprintf("failed to create MCP server, %s", err.Error())
status := http.StatusInternalServerError
http.Error(w, message, status)
return
}
// Then pass to the main handler logic
h.serveHTTP(w, r)
})
}
func (h *MCPHTTPHandler) newMCPServerForRequest(r *http.Request) (context.Context, *mcp.Server, *inventory.Inventory, error) {
ctx := r.Context()
getClient := h.serverDependencies.getClient
getGQClient := h.serverDependencies.getGQClient
getRawClient := h.serverDependencies.getRawClient
t := h.serverDependencies.t
deps := github.NewBaseDeps(
clients.rest,
clients.gql,
clients.raw,
clients.repoAccess,
cfg.Translator,
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
cfg.ContentWindowSize,
)
// Build the registry with all configuration applied
builder := inventory.NewBuilder().
SetTools(github.AllTools(t)).
SetResources(github.AllResources(t)).
SetPrompts(github.AllPrompts(t))
inv := builder.Build()
enabledToolsetIDs := inv.EnabledToolsetIDs()
enabledToolsets := make([]string, len(enabledToolsetIDs))
for i, id := range enabledToolsetIDs {
enabledToolsets[i] = string(id)
}
svrOpts := mcp.ServerOptions{}
svr := github.NewServer(h.config.Version, &svrOpts)
return ctx, svr, inv, nil
}
// routePattern returns the route pattern for the given HTTP request.
// It retrieves the route pattern from the chi RouteContext, falling back to the request's URL path
// if the route context or pattern is unavailable. If the pattern is "/*", it is normalized to "/".
// Additionally, it replaces curly braces in the pattern with a colon prefix to ensure compatibility
// with Datadog, which does not accept curly braces in route patterns.
func routePattern(r *http.Request) string {
rc := chi.RouteContext(r.Context())
if rc == nil || rc.RoutePattern() == "" {
return r.URL.Path
}
routePattern := rc.RoutePattern()
// Normalise /* to / as even without an explicit catch all route,
// mounting at / can result in /* as the route pattern for unmatched paths.
if routePattern == "/*" {
routePattern = "/"
}
// Replace { and } with : because Datadog doesn't like them.
// Before: /x/{toolset}
// After: /x/:toolset
routePattern = strings.ReplaceAll(routePattern, "{", ":")
routePattern = strings.ReplaceAll(routePattern, "}", "")
return routePattern
}