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
54 changes: 42 additions & 12 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,23 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
// Construct REST client. BearerAuthTransport handles both static and
// provider-backed tokens so every authentication mode uses the same host
// restrictions.
//
// 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),
}
restClient, err := gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{Transport: &transport.BearerAuthTransport{
Transport: restUATransport,
Token: cfg.Token,
TokenProvider: cfg.TokenProvider,
AllowedHosts: allowedHosts,
}}),
gogithub.WithEnterpriseURLs(restURL.String(), uploadURL.String()),
)
restClient, err := newRESTClient(cfg, restUATransport, restURL.String(), uploadURL.String(), allowedHosts)
if err != nil {
return nil, fmt.Errorf("failed to create REST client: %w", err)
}
Expand All @@ -108,8 +112,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(), allowedHosts)
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 @@ -136,6 +150,22 @@ 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. Authentication uses BearerAuthTransport for
// both static and provider-backed tokens, and allowedHosts scopes the token to
// the configured GitHub hosts so it is never leaked to off-host redirects.
func newRESTClient(cfg github.MCPServerConfig, uaTransport *transport.UserAgentTransport, restURL, uploadURL string, allowedHosts []string) (*gogithub.Client, error) {
return gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{Transport: &transport.BearerAuthTransport{
Transport: uaTransport,
Token: cfg.Token,
TokenProvider: cfg.TokenProvider,
AllowedHosts: allowedHosts,
}}),
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
Loading