Skip to content

Expose ghmcp package #437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 1 addition & 1 deletion cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import (
"fmt"
"os"

"github.com/github/github-mcp-server/internal/ghmcp"
"github.com/github/github-mcp-server/pkg/ghmcp"
"github.com/github/github-mcp-server/pkg/github"
"github.com/spf13/cobra"
"github.com/spf13/viper"
69 changes: 64 additions & 5 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,10 @@ import (
"github.com/sirupsen/logrus"
)

// TokenProvider is a function that returns the current GitHub token.
// This allows for dynamic token refresh without restarting the server.
type TokenProvider func() string

type MCPServerConfig struct {
// Version of the server
Version string
@@ -30,8 +34,13 @@ type MCPServerConfig struct {
Host string

// GitHub Token to authenticate with the GitHub API
// Deprecated: Use TokenProvider for dynamic token support
Token string

// TokenProvider is a function that returns the current GitHub token
// If set, this takes precedence over Token
TokenProvider TokenProvider

// EnabledToolsets is a list of toolsets to enable
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
EnabledToolsets []string
@@ -53,8 +62,25 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
return nil, fmt.Errorf("failed to parse API host: %w", err)
}

// Construct our REST client
restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token)
// Set up token provider - use the provided one or create one from static token
tokenProvider := cfg.TokenProvider
if tokenProvider == nil {
if cfg.Token == "" {
return nil, fmt.Errorf("either Token or TokenProvider must be set")
Copy link
Preview

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this error message more explicit by referencing the config type and fields, for example: "MCPServerConfig requires either Token or TokenProvider to be set".

Suggested change
return nil, fmt.Errorf("either Token or TokenProvider must be set")
return nil, fmt.Errorf("MCPServerConfig requires either Token or TokenProvider to be set")

Copilot uses AI. Check for mistakes.

}
// Create a token provider that returns the static token
staticToken := cfg.Token
tokenProvider = func() string { return staticToken }
}

// Construct our REST client with a custom transport
restHTTPClient := &http.Client{
Transport: &tokenProviderTransport{
transport: http.DefaultTransport,
tokenProvider: tokenProvider,
},
}
restClient := gogithub.NewClient(restHTTPClient)
restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version)
restClient.BaseURL = apiHost.baseRESTURL
restClient.UploadURL = apiHost.uploadURL
@@ -63,9 +89,9 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
// We're using NewEnterpriseClient here unconditionally as opposed to NewClient because we already
// did the necessary API host parsing so that github.com will return the correct URL anyway.
gqlHTTPClient := &http.Client{
Transport: &bearerAuthTransport{
transport: http.DefaultTransport,
token: cfg.Token,
Transport: &bearerAuthProviderTransport{
transport: http.DefaultTransport,
tokenProvider: tokenProvider,
},
} // We're going to wrap the Transport later in beforeInit
gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)
@@ -147,8 +173,13 @@ type StdioServerConfig struct {
Host string

// GitHub Token to authenticate with the GitHub API
// Deprecated: Use TokenProvider for dynamic token support
Token string

// TokenProvider is a function that returns the current GitHub token
// If set, this takes precedence over Token
TokenProvider TokenProvider

// EnabledToolsets is a list of toolsets to enable
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
EnabledToolsets []string
@@ -183,6 +214,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
Version: cfg.Version,
Host: cfg.Host,
Token: cfg.Token,
TokenProvider: cfg.TokenProvider,
EnabledToolsets: cfg.EnabledToolsets,
DynamicToolsets: cfg.DynamicToolsets,
ReadOnly: cfg.ReadOnly,
@@ -368,6 +400,7 @@ func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error
return t.transport.RoundTrip(req)
}

// bearerAuthTransport is deprecated - use bearerAuthProviderTransport
type bearerAuthTransport struct {
transport http.RoundTripper
token string
@@ -378,3 +411,29 @@ func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro
req.Header.Set("Authorization", "Bearer "+t.token)
return t.transport.RoundTrip(req)
}

// bearerAuthProviderTransport uses a token provider for dynamic token support
type bearerAuthProviderTransport struct {
transport http.RoundTripper
tokenProvider TokenProvider
}

func (t *bearerAuthProviderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
token := t.tokenProvider()
req.Header.Set("Authorization", "Bearer "+token)
return t.transport.RoundTrip(req)
}

// tokenProviderTransport adds GitHub authentication using a token provider
type tokenProviderTransport struct {
transport http.RoundTripper
tokenProvider TokenProvider
}

func (t *tokenProviderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
token := t.tokenProvider()
req.Header.Set("Authorization", "Bearer "+token)
return t.transport.RoundTrip(req)
}
Comment on lines +415 to +439
Copy link
Preview

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bearerAuthProviderTransport and tokenProviderTransport implementations share identical RoundTrip logic. Consider consolidating them into a single reusable auth transport to reduce duplication and simplify maintenance.

Suggested change
// bearerAuthProviderTransport uses a token provider for dynamic token support
type bearerAuthProviderTransport struct {
transport http.RoundTripper
tokenProvider TokenProvider
}
func (t *bearerAuthProviderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
token := t.tokenProvider()
req.Header.Set("Authorization", "Bearer "+token)
return t.transport.RoundTrip(req)
}
// tokenProviderTransport adds GitHub authentication using a token provider
type tokenProviderTransport struct {
transport http.RoundTripper
tokenProvider TokenProvider
}
func (t *tokenProviderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
token := t.tokenProvider()
req.Header.Set("Authorization", "Bearer "+token)
return t.transport.RoundTrip(req)
}
// authTransport is a reusable transport that adds GitHub authentication using a token provider
type authTransport struct {
transport http.RoundTripper
tokenProvider TokenProvider
}
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
token := t.tokenProvider()
req.Header.Set("Authorization", "Bearer "+token)
return t.transport.RoundTrip(req)
}

Copilot uses AI. Check for mistakes.

230 changes: 230 additions & 0 deletions pkg/ghmcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# GitHub MCP Server Go Library

This package provides a Go library interface to the GitHub MCP Server, allowing you to embed the server functionality in your own Go applications.

## Installation

```bash
go get github.com/github/github-mcp-server/pkg/ghmcp
```

## Usage

### Running a Stdio Server with Static Token

The most common use case is running the MCP server using stdio for communication:

```go
package main

import (
"log"
"os"

"github.com/github/github-mcp-server/pkg/ghmcp"
)

func main() {
config := ghmcp.StdioServerConfig{
Version: "1.0.0",
Host: "https://github.com", // or your GitHub Enterprise URL
Token: os.Getenv("GITHUB_TOKEN"),
EnabledToolsets: []string{"repos", "issues", "pulls"},
ReadOnly: false,
}

if err := ghmcp.RunStdioServer(config); err != nil {
log.Fatal(err)
}
}
```

### Running a Stdio Server with Dynamic Token Provider

For applications that need to refresh tokens without restarting the server:

```go
package main

import (
"log"
"sync"
"time"

"github.com/github/github-mcp-server/pkg/ghmcp"
)

// TokenManager manages dynamic token updates
type TokenManager struct {
mu sync.RWMutex
currentToken string
}

func (tm *TokenManager) GetToken() string {
tm.mu.RLock()
defer tm.mu.RUnlock()
return tm.currentToken
}

func (tm *TokenManager) UpdateToken(newToken string) {
tm.mu.Lock()
defer tm.mu.Unlock()
tm.currentToken = newToken
}

func main() {
tokenManager := &TokenManager{
currentToken: getInitialToken(), // Your initial token
}

// The token provider will be called on each API request
tokenProvider := func() string {
return tokenManager.GetToken()
}

config := ghmcp.StdioServerConfig{
Version: "1.0.0",
Host: "https://github.com",
TokenProvider: tokenProvider, // Use TokenProvider instead of Token
EnabledToolsets: []string{"repos", "issues", "pulls"},
ReadOnly: false,
}

// Start a goroutine to refresh the token periodically
go func() {
for {
time.Sleep(30 * time.Minute)
newToken := refreshTokenFromAuthService() // Your token refresh logic
tokenManager.UpdateToken(newToken)
}
}()

if err := ghmcp.RunStdioServer(config); err != nil {
log.Fatal(err)
}
}
```

### Creating a Custom MCP Server

For more advanced use cases, you can create an MCP server instance directly:

```go
package main

import (
"log"

"github.com/github/github-mcp-server/pkg/ghmcp"
"github.com/github/github-mcp-server/pkg/translations"
)

func main() {
config := ghmcp.MCPServerConfig{
Version: "1.0.0",
Host: "https://github.com",
Token: "your-github-token",
EnabledToolsets: []string{"repos", "issues"},
ReadOnly: true,
Translator: translations.NullTranslationHelper,
}

server, err := ghmcp.NewMCPServer(config)
if err != nil {
log.Fatal(err)
}

// Use the server instance as needed
_ = server
}
```

## Configuration Options

### StdioServerConfig

- `Version`: Version of your server
- `Host`: GitHub API host (e.g., "https://github.com" or "https://github.enterprise.com")
- `Token`: GitHub personal access token (static token, use TokenProvider for dynamic tokens)
Copy link
Preview

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the Token field is now deprecated in code, update this line to mark it as Deprecated in the README (e.g., "Deprecated: use TokenProvider...").

Suggested change
- `Token`: GitHub personal access token (static token, use TokenProvider for dynamic tokens)
- `Token` (Deprecated): GitHub personal access token (static token). Use `TokenProvider` for dynamic tokens.

Copilot uses AI. Check for mistakes.

- `TokenProvider`: Function that returns the current GitHub token (takes precedence over Token)
- `EnabledToolsets`: List of toolsets to enable (e.g., "repos", "issues", "pulls", "users", "search")
- `DynamicToolsets`: Enable dynamic toolset discovery
- `ReadOnly`: Restrict to read-only operations
- `ExportTranslations`: Export translations to a JSON file
- `EnableCommandLogging`: Log all command requests and responses
- `LogFilePath`: Path to log file (defaults to stderr)

### MCPServerConfig

- `Version`: Version of your server
- `Host`: GitHub API host
- `Token`: GitHub personal access token (static token, use TokenProvider for dynamic tokens)
- `TokenProvider`: Function that returns the current GitHub token (takes precedence over Token)
- `EnabledToolsets`: List of toolsets to enable
- `DynamicToolsets`: Enable dynamic toolset discovery
- `ReadOnly`: Restrict to read-only operations
- `Translator`: Translation helper function (use `translations.NullTranslationHelper` for default)

## Available Toolsets

- `repos`: Repository management tools
- `issues`: Issue management tools
- `pulls`: Pull request management tools
- `users`: User management tools
- `search`: Search functionality
- `all`: Enable all available toolsets

## Token Provider Best Practices

When using a `TokenProvider`:

1. **Thread Safety**: Ensure your token provider is thread-safe as it will be called concurrently from multiple goroutines.
2. **Performance**: The token provider is called on each API request, so it should be fast. Consider caching the token.
3. **Error Handling**: The token provider should always return a valid token. Handle errors internally and fall back to a cached token if necessary.
4. **Logging**: Be careful not to log the full token. Log only the last few characters for debugging.
5. **Graceful Updates**: When updating tokens, ensure there's no downtime. The old token should remain valid until the new one is ready.

Example of a production-ready token provider:

```go
type TokenCache struct {
mu sync.RWMutex
token string
expiry time.Time
refreshFunc func() (string, time.Time, error)
}

func (tc *TokenCache) GetToken() string {
tc.mu.RLock()
if time.Now().Before(tc.expiry) {
defer tc.mu.RUnlock()
return tc.token
}
tc.mu.RUnlock()

// Token expired, refresh it
tc.mu.Lock()
defer tc.mu.Unlock()

// Double-check after acquiring write lock
if time.Now().Before(tc.expiry) {
return tc.token
}

newToken, newExpiry, err := tc.refreshFunc()
if err != nil {
// Log error and return cached token
log.Printf("Failed to refresh token: %v", err)
return tc.token
}

tc.token = newToken
tc.expiry = newExpiry
return tc.token
}
```

## Requirements

- Go 1.21 or later
- Valid GitHub personal access token with appropriate permissions
Loading
Oops, something went wrong.