Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions internal/ghmcp/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,10 @@ func TestCreateGitHubClientsTokenProvider(t *testing.T) {
t.Parallel()

var gotAuth string
var gotAPIVersion string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get(headers.AuthorizationHeader)
gotAPIVersion = r.Header.Get(headers.GitHubAPIVersionHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
Expand All @@ -600,6 +602,7 @@ func TestCreateGitHubClientsTokenProvider(t *testing.T) {

do()
assert.Equal(t, "", gotAuth, "no auth header before authorization")
assert.Equal(t, headers.GitHubAPIVersion, gotAPIVersion)

current = "oauth-token"
do()
Expand Down
2 changes: 1 addition & 1 deletion internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
// the latter installs its own round tripper that would pin the static token
// and shadow the dynamic one.
restUATransport := &transport.UserAgentTransport{
Transport: http.DefaultTransport,
Transport: &transport.APIVersionTransport{Transport: http.DefaultTransport},
Agent: fmt.Sprintf("github-mcp-server/%s", cfg.Version),
}
var restClient *gogithub.Client
Expand Down
7 changes: 4 additions & 3 deletions internal/githubapp/githubapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"sync"
"time"

"github.com/github/github-mcp-server/pkg/http/headers"
"golang.org/x/oauth2"
)

Expand Down Expand Up @@ -140,9 +141,9 @@ func (s *installationTokenSource) Token() (*oauth2.Token, error) {
if err != nil {
return nil, fmt.Errorf("creating installation token request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+jwt)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
req.Header.Set(headers.AuthorizationHeader, "Bearer "+jwt)
req.Header.Set(headers.AcceptHeader, "application/vnd.github+json")
req.Header.Set(headers.GitHubAPIVersionHeader, headers.GitHubAPIVersion)

resp, err := s.httpClient.Do(req)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/githubapp/githubapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"
"time"

"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -155,6 +156,7 @@ func installationServer(t *testing.T, pub *rsa.PublicKey, token string, expiresA
calls.Add(1)
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/app/installations/456/access_tokens", r.URL.Path)
assert.Equal(t, headers.GitHubAPIVersion, r.Header.Get(headers.GitHubAPIVersionHeader))

authz := r.Header.Get("Authorization")
require.True(t, strings.HasPrefix(authz, "Bearer "), "must send the app JWT as a bearer token")
Expand Down
3 changes: 3 additions & 0 deletions pkg/github/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ func (d *RequestDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {

// Construct REST client
restClient, err := gogithub.NewClient(
gogithub.WithHTTPClient(&http.Client{
Transport: &transport.APIVersionTransport{Transport: http.DefaultTransport},
}),
gogithub.WithAuthToken(token),
gogithub.WithUserAgent(fmt.Sprintf("github-mcp-server/%s", d.version)),
gogithub.WithEnterpriseURLs(baseRestURL.String(), uploadURL.String()),
Expand Down
45 changes: 45 additions & 0 deletions pkg/github/dependencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,65 @@ import (
"context"
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"testing"

ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/github/github-mcp-server/pkg/observability"
"github.com/github/github-mcp-server/pkg/observability/metrics"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type requestDepsAPIHost struct {
url *url.URL
}

func (h requestDepsAPIHost) BaseRESTURL(context.Context) (*url.URL, error) { return h.url, nil }
func (h requestDepsAPIHost) GraphqlURL(context.Context) (*url.URL, error) { return h.url, nil }
func (h requestDepsAPIHost) UploadURL(context.Context) (*url.URL, error) { return h.url, nil }
func (h requestDepsAPIHost) RawURL(context.Context) (*url.URL, error) { return h.url, nil }
func (h requestDepsAPIHost) AuthorizationServerURL(context.Context) (*url.URL, error) {
return h.url, nil
}

func testExporters() observability.Exporters {
obs, _ := observability.NewExporters(slog.New(slog.DiscardHandler), metrics.NewNoopMetrics())
return obs
}

func TestRequestDepsGetClientSetsAPIVersion(t *testing.T) {
t.Parallel()

var gotVersion string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotVersion = r.Header.Get(headers.GitHubAPIVersionHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

serverURL, err := url.Parse(server.URL)
require.NoError(t, err)
apiHost := requestDepsAPIHost{url: serverURL}
deps := github.NewRequestDeps(apiHost, "test", false, nil, nil, 0, nil, testExporters())
ctx := ghcontext.WithTokenInfo(context.Background(), &ghcontext.TokenInfo{Token: "test-token"})
client, err := deps.GetClient(ctx)
require.NoError(t, err)

req, err := client.NewRequest(ctx, http.MethodGet, "rate_limit", nil)
require.NoError(t, err)
resp, err := client.Do(req, nil)
require.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, headers.GitHubAPIVersion, gotVersion)
}

func TestIsFeatureEnabled_WithEnabledFlag(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 2 additions & 0 deletions pkg/http/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ const (
GraphQLFeaturesHeader = "GraphQL-Features"
// GitHubAPIVersionHeader is the header used to specify the GitHub API version.
GitHubAPIVersionHeader = "X-GitHub-Api-Version"
// GitHubAPIVersion is the GitHub REST API version used by this server.
GitHubAPIVersion = "2026-03-10"
)
24 changes: 24 additions & 0 deletions pkg/http/transport/api_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package transport

import (
"net/http"

"github.com/github/github-mcp-server/pkg/http/headers"
)

// APIVersionTransport sets the GitHub REST API version on every request.
type APIVersionTransport struct {
Transport http.RoundTripper
}

// RoundTrip implements http.RoundTripper.
func (t *APIVersionTransport) RoundTrip(req *http.Request) (*http.Response, error) {
underlying := t.Transport
if underlying == nil {
underlying = http.DefaultTransport
}

req = req.Clone(req.Context())
req.Header.Set(headers.GitHubAPIVersionHeader, headers.GitHubAPIVersion)
return underlying.RoundTrip(req)
}
33 changes: 33 additions & 0 deletions pkg/http/transport/api_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package transport

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAPIVersionTransport(t *testing.T) {
t.Parallel()

var gotVersion string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotVersion = r.Header.Get(headers.GitHubAPIVersionHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

req, err := http.NewRequest(http.MethodGet, server.URL, nil)
require.NoError(t, err)
req.Header.Set(headers.GitHubAPIVersionHeader, "2022-11-28")

resp, err := (&APIVersionTransport{}).RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, headers.GitHubAPIVersion, gotVersion)
assert.Equal(t, "2022-11-28", req.Header.Get(headers.GitHubAPIVersionHeader))
}
2 changes: 1 addition & 1 deletion pkg/scopes/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (f *Fetcher) FetchTokenScopes(ctx context.Context, token string) ([]string,

req.Header.Set(headers.AuthorizationHeader, "Bearer "+token)
req.Header.Set(headers.AcceptHeader, "application/vnd.github+json")
req.Header.Set(headers.GitHubAPIVersionHeader, "2022-11-28")
req.Header.Set(headers.GitHubAPIVersionHeader, headers.GitHubAPIVersion)

resp, err := f.client.Do(req)
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions pkg/scopes/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"
"time"

"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -148,6 +149,19 @@ func TestFetcher_FetchTokenScopes(t *testing.T) {
expectedScopes: []string{"repo"},
expectError: false,
},
{
name: "verifies API version header is set",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(headers.GitHubAPIVersionHeader) != headers.GitHubAPIVersion {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("X-OAuth-Scopes", "repo")
w.WriteHeader(http.StatusOK)
},
expectedScopes: []string{"repo"},
expectError: false,
},
{
name: "verifies request method is HEAD",
handler: func(w http.ResponseWriter, r *http.Request) {
Expand Down