Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,23 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
// authenticate via BearerAuthTransport and skip go-github's WithAuthToken:
// the latter installs its own round tripper that would pin the static token
// and shadow the dynamic one.
//
// ETagTransport sits below the user-agent (and auth) layers so that, by the
// time it runs, the Authorization header is set and can scope the
// conditional-request cache per token. It adds ETag/If-None-Match handling
// so unchanged resources are revalidated with a 304 instead of being
// re-downloaded in full.
//
// The conditional-request cache is enabled only for the REST API client on
// this long-lived local (stdio) server. The raw-content client below uses a
// separate transport without it, so large file bodies are never buffered
// into the cache. The hosted, horizontally-scaled server builds a fresh REST
// client per request (see pkg/github RequestDeps) and does not use this path.
restUATransport := &transport.UserAgentTransport{
Transport: http.DefaultTransport,
Transport: &transport.ETagTransport{Transport: http.DefaultTransport},
Agent: fmt.Sprintf("github-mcp-server/%s", cfg.Version),
}
var restClient *gogithub.Client
if cfg.TokenProvider != nil {
restClient, err = gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{Transport: &transport.BearerAuthTransport{
Transport: restUATransport,
TokenProvider: cfg.TokenProvider,
}}),
gogithub.WithEnterpriseURLs(restURL.String(), uploadURL.String()),
)
} else {
restClient, err = gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{Transport: restUATransport}),
gogithub.WithAuthToken(cfg.Token),
gogithub.WithEnterpriseURLs(restURL.String(), uploadURL.String()),
)
}
restClient, err := newRESTClient(cfg, restUATransport, restURL.String(), uploadURL.String())
if err != nil {
return nil, fmt.Errorf("failed to create REST client: %w", err)
}
Expand All @@ -104,8 +101,18 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv

gqlClient := githubv4.NewEnterpriseClient(graphQLURL.String(), gqlHTTPClient)

// Create raw content client (shares REST client's HTTP transport)
rawClient, err := raw.NewClient(restClient, rawURL)
// Create raw content client. It shares the REST client's authentication but
// uses a transport without the conditional-request cache: raw file bodies can
// be large and are streamed rather than retained in memory.
rawUATransport := &transport.UserAgentTransport{
Transport: http.DefaultTransport,
Agent: fmt.Sprintf("github-mcp-server/%s", cfg.Version),
}
rawRESTClient, err := newRESTClient(cfg, rawUATransport, restURL.String(), uploadURL.String())
if err != nil {
return nil, fmt.Errorf("failed to create raw REST client: %w", err)
}
rawClient, err := raw.NewClient(rawRESTClient, rawURL)
if err != nil {
return nil, fmt.Errorf("failed to create raw client: %w", err)
}
Expand All @@ -132,6 +139,27 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
}, nil
}

// newRESTClient builds a go-github REST client that sends requests through the
// supplied user-agent transport, wiring authentication to match the server
// configuration: a dynamic TokenProvider via BearerAuthTransport, or a static
// token via go-github's WithAuthToken.
func newRESTClient(cfg github.MCPServerConfig, uaTransport *transport.UserAgentTransport, restURL, uploadURL string) (*gogithub.Client, error) {
if cfg.TokenProvider != nil {
return gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{Transport: &transport.BearerAuthTransport{
Transport: uaTransport,
TokenProvider: cfg.TokenProvider,
}}),
gogithub.WithEnterpriseURLs(restURL, uploadURL),
)
}
return gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{Transport: uaTransport}),
gogithub.WithAuthToken(cfg.Token),
gogithub.WithEnterpriseURLs(restURL, uploadURL),
)
}

func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Server, error) {
apiHost, err := utils.NewAPIHost(cfg.Host)
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions pkg/http/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const (
AcceptHeader = "Accept"
// UserAgentHeader is a standard HTTP Header.
UserAgentHeader = "User-Agent"
// ETagHeader is a standard HTTP Header carrying a response entity tag.
ETagHeader = "ETag"
// IfNoneMatchHeader is a standard HTTP Header used to make a request conditional on an entity tag.
IfNoneMatchHeader = "If-None-Match"
// CacheControlHeader is a standard HTTP Header carrying caching directives.
CacheControlHeader = "Cache-Control"
// VaryHeader is a standard HTTP Header describing which request headers a response varies on.
VaryHeader = "Vary"

// ContentTypeJSON is the standard MIME type for JSON.
ContentTypeJSON = "application/json"
Expand Down
Loading