167 lines
4.7 KiB
Go
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
|
|
}
|