chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:57 +08:00
commit e30f8ba47c
533 changed files with 115926 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
package transport
import (
"net/http"
"strings"
ghcontext "github.com/github/github-mcp-server/pkg/context"
headers "github.com/github/github-mcp-server/pkg/http/headers"
)
type BearerAuthTransport struct {
Transport http.RoundTripper
Token string
// TokenProvider, when non-nil, supplies the bearer token for each request
// and takes precedence over Token. It backs OAuth, where the token is
// obtained after the client is built and is refreshed over the session's
// lifetime. It may return an empty string before authorization completes.
TokenProvider func() string
}
func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
token := t.Token
if t.TokenProvider != nil {
token = t.TokenProvider()
}
// Before OAuth authorization completes the token is empty; send an
// unauthenticated request rather than an empty "Bearer " header.
if token != "" {
req.Header.Set(headers.AuthorizationHeader, "Bearer "+token)
}
// Check for GraphQL-Features in context and add header if present
if features := ghcontext.GetGraphQLFeatures(req.Context()); len(features) > 0 {
req.Header.Set(headers.GraphQLFeaturesHeader, strings.Join(features, ", "))
}
return t.Transport.RoundTrip(req)
}
+164
View File
@@ -0,0 +1,164 @@
package transport
import (
"context"
"net/http"
"net/http/httptest"
"testing"
ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBearerAuthTransport(t *testing.T) {
t.Parallel()
tests := []struct {
name string
token string
tokenProvider func() string
wantAuth string
}{
{
name: "static token",
token: "static-token",
wantAuth: "Bearer static-token",
},
{
name: "token provider takes precedence over static token",
token: "static-token",
tokenProvider: func() string { return "provided-token" },
wantAuth: "Bearer provided-token",
},
{
name: "token provider with empty static token",
tokenProvider: func() string { return "provided-token" },
wantAuth: "Bearer provided-token",
},
{
name: "token provider may return empty before authorization",
tokenProvider: func() string { return "" },
wantAuth: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var gotAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get(headers.AuthorizationHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
rt := &BearerAuthTransport{
Transport: http.DefaultTransport,
Token: tc.token,
TokenProvider: tc.tokenProvider,
}
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
require.NoError(t, err)
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tc.wantAuth, gotAuth)
})
}
}
// TestBearerAuthTransport_TokenProviderResolvedPerRequest verifies that the
// token provider is consulted on every request, so a token that arrives (or is
// refreshed) after the transport is constructed takes effect without rebuilding
// the client. This is the property OAuth relies on.
func TestBearerAuthTransport_TokenProviderResolvedPerRequest(t *testing.T) {
t.Parallel()
var gotAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get(headers.AuthorizationHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
current := ""
rt := &BearerAuthTransport{
Transport: http.DefaultTransport,
TokenProvider: func() string { return current },
}
do := func() {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
require.NoError(t, err)
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
}
do()
assert.Equal(t, "", gotAuth, "no auth header before authorization")
current = "first-token"
do()
assert.Equal(t, "Bearer first-token", gotAuth, "token picked up once available")
current = "refreshed-token"
do()
assert.Equal(t, "Bearer refreshed-token", gotAuth, "refreshed token picked up")
}
func TestBearerAuthTransport_PassesGraphQLFeaturesHeader(t *testing.T) {
t.Parallel()
var gotFeatures string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotFeatures = r.Header.Get(headers.GraphQLFeaturesHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
rt := &BearerAuthTransport{
Transport: http.DefaultTransport,
Token: "token",
}
ctx := ghcontext.WithGraphQLFeatures(context.Background(), "feature1", "feature2")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, server.URL, nil)
require.NoError(t, err)
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, "feature1, feature2", gotFeatures)
}
func TestBearerAuthTransport_DoesNotMutateOriginalRequest(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
rt := &BearerAuthTransport{
Transport: http.DefaultTransport,
Token: "token",
}
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
require.NoError(t, err)
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Empty(t, req.Header.Get(headers.AuthorizationHeader), "original request must not be mutated")
}
+52
View File
@@ -0,0 +1,52 @@
package transport
import (
"net/http"
"strings"
ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/github/github-mcp-server/pkg/http/headers"
)
// GraphQLFeaturesTransport is an http.RoundTripper that adds GraphQL-Features
// header to requests based on context values. This is required for using
// non-GA GraphQL API features like the agent assignment API.
//
// This transport is used internally by the MCP server and is also exported
// for library consumers who need to build their own HTTP clients with
// GraphQL feature flag support.
//
// Usage:
//
// import "github.com/github/github-mcp-server/pkg/http/transport"
//
// httpClient := &http.Client{
// Transport: &transport.GraphQLFeaturesTransport{
// Transport: http.DefaultTransport,
// },
// }
// gqlClient := githubv4.NewClient(httpClient)
//
// Then use ghcontext.WithGraphQLFeatures(ctx, "feature_name") when calling GraphQL operations.
type GraphQLFeaturesTransport struct {
// Transport is the underlying HTTP transport. If nil, http.DefaultTransport is used.
Transport http.RoundTripper
}
// RoundTrip implements http.RoundTripper.
func (t *GraphQLFeaturesTransport) RoundTrip(req *http.Request) (*http.Response, error) {
transport := t.Transport
if transport == nil {
transport = http.DefaultTransport
}
// Clone the request to avoid mutating the original
req = req.Clone(req.Context())
// Check for GraphQL-Features in context and add header if present
if features := ghcontext.GetGraphQLFeatures(req.Context()); len(features) > 0 {
req.Header.Set(headers.GraphQLFeaturesHeader, strings.Join(features, ", "))
}
return transport.RoundTrip(req)
}
+154
View File
@@ -0,0 +1,154 @@
package transport
import (
"context"
"net/http"
"net/http/httptest"
"testing"
ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/github/github-mcp-server/pkg/http/headers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGraphQLFeaturesTransport(t *testing.T) {
t.Parallel()
tests := []struct {
name string
features []string
expectedHeader string
hasHeader bool
}{
{
name: "no features in context",
features: nil,
expectedHeader: "",
hasHeader: false,
},
{
name: "single feature in context",
features: []string{"issues_copilot_assignment_api_support"},
expectedHeader: "issues_copilot_assignment_api_support",
hasHeader: true,
},
{
name: "multiple features in context",
features: []string{"feature1", "feature2", "feature3"},
expectedHeader: "feature1, feature2, feature3",
hasHeader: true,
},
{
name: "empty features slice",
features: []string{},
expectedHeader: "",
hasHeader: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var capturedHeader string
var headerExists bool
// Create a test server that captures the request header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedHeader = r.Header.Get(headers.GraphQLFeaturesHeader)
headerExists = r.Header.Get(headers.GraphQLFeaturesHeader) != ""
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Create the transport
transport := &GraphQLFeaturesTransport{
Transport: http.DefaultTransport,
}
// Create a request
ctx := context.Background()
if tc.features != nil {
ctx = ghcontext.WithGraphQLFeatures(ctx, tc.features...)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, server.URL, nil)
require.NoError(t, err)
// Execute the request
resp, err := transport.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify the header
assert.Equal(t, tc.hasHeader, headerExists)
if tc.hasHeader {
assert.Equal(t, tc.expectedHeader, capturedHeader)
}
})
}
}
func TestGraphQLFeaturesTransport_NilTransport(t *testing.T) {
t.Parallel()
var capturedHeader string
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedHeader = r.Header.Get(headers.GraphQLFeaturesHeader)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Create the transport with nil Transport (should use DefaultTransport)
transport := &GraphQLFeaturesTransport{
Transport: nil,
}
// Create a request with features
ctx := ghcontext.WithGraphQLFeatures(context.Background(), "test_feature")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, server.URL, nil)
require.NoError(t, err)
// Execute the request
resp, err := transport.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify the header was added
assert.Equal(t, "test_feature", capturedHeader)
}
func TestGraphQLFeaturesTransport_DoesNotMutateOriginalRequest(t *testing.T) {
t.Parallel()
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Create the transport
transport := &GraphQLFeaturesTransport{
Transport: http.DefaultTransport,
}
// Create a request with features
ctx := ghcontext.WithGraphQLFeatures(context.Background(), "test_feature")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, server.URL, nil)
require.NoError(t, err)
// Store the original header value
originalHeader := req.Header.Get(headers.GraphQLFeaturesHeader)
// Execute the request
resp, err := transport.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify the original request was not mutated
assert.Equal(t, originalHeader, req.Header.Get(headers.GraphQLFeaturesHeader))
}
+18
View File
@@ -0,0 +1,18 @@
package transport
import (
"net/http"
"github.com/github/github-mcp-server/pkg/http/headers"
)
type UserAgentTransport struct {
Transport http.RoundTripper
Agent string
}
func (t *UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
req.Header.Set(headers.UserAgentHeader, t.Agent)
return t.Transport.RoundTrip(req)
}