170 lines
5.2 KiB
Go
170 lines
5.2 KiB
Go
package ghmcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/github/github-mcp-server/pkg/errors"
|
|
"github.com/github/github-mcp-server/pkg/github"
|
|
mcplog "github.com/github/github-mcp-server/pkg/log"
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
type StdioServerConfig struct {
|
|
// Version of the server
|
|
Version string
|
|
|
|
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
|
|
Host string
|
|
|
|
// GitHub Token to authenticate with the GitHub API
|
|
Token string
|
|
|
|
// EnabledToolsets is a list of toolsets to enable
|
|
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
|
|
EnabledToolsets []string
|
|
|
|
// EnabledTools is a list of specific tools to enable (additive to toolsets)
|
|
// When specified, these tools are registered in addition to any specified toolset tools
|
|
EnabledTools []string
|
|
|
|
// EnabledFeatures is a list of feature flags that are enabled
|
|
// Items with FeatureFlagEnable matching an entry in this list will be available
|
|
EnabledFeatures []string
|
|
|
|
// Whether to enable dynamic toolsets
|
|
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
|
|
DynamicToolsets bool
|
|
|
|
// ReadOnly indicates if we should only register read-only tools
|
|
ReadOnly bool
|
|
|
|
// ExportTranslations indicates if we should export translations
|
|
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions
|
|
ExportTranslations bool
|
|
|
|
// EnableCommandLogging indicates if we should log commands
|
|
EnableCommandLogging bool
|
|
|
|
// Path to the log file if not stderr
|
|
LogFilePath string
|
|
|
|
// Content window size
|
|
ContentWindowSize int
|
|
|
|
// LockdownMode indicates if we should enable lockdown mode
|
|
LockdownMode bool
|
|
|
|
// RepoAccessCacheTTL overrides the default TTL for repository access cache entries.
|
|
RepoAccessCacheTTL *time.Duration
|
|
}
|
|
|
|
// RunStdioServer is not concurrent safe.
|
|
func RunStdioServer(cfg StdioServerConfig) error {
|
|
// Create app context
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
t, dumpTranslations := translations.TranslationHelper()
|
|
|
|
var slogHandler slog.Handler
|
|
var logOutput io.Writer
|
|
if cfg.LogFilePath != "" {
|
|
file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open log file: %w", err)
|
|
}
|
|
logOutput = file
|
|
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
|
|
} else {
|
|
logOutput = os.Stderr
|
|
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
|
|
}
|
|
logger := slog.New(slogHandler)
|
|
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode)
|
|
|
|
// Fetch token scopes for scope-based tool filtering (PAT tokens only)
|
|
// Only classic PATs (ghp_ prefix) return OAuth scopes via X-OAuth-Scopes header.
|
|
// Fine-grained PATs and other token types don't support this, so we skip filtering.
|
|
var tokenScopes []string
|
|
if strings.HasPrefix(cfg.Token, "ghp_") {
|
|
fetchedScopes, err := github.FetchTokenScopesForHost(ctx, cfg.Token, cfg.Host)
|
|
if err != nil {
|
|
logger.Warn("failed to fetch token scopes, continuing without scope filtering", "error", err)
|
|
} else {
|
|
tokenScopes = fetchedScopes
|
|
logger.Info("token scopes fetched for filtering", "scopes", tokenScopes)
|
|
}
|
|
} else {
|
|
logger.Debug("skipping scope filtering for non-PAT token")
|
|
}
|
|
|
|
ghServer, err := github.NewMCPServer(github.MCPServerConfig{
|
|
Version: cfg.Version,
|
|
Host: cfg.Host,
|
|
Token: cfg.Token,
|
|
EnabledToolsets: cfg.EnabledToolsets,
|
|
EnabledTools: cfg.EnabledTools,
|
|
EnabledFeatures: cfg.EnabledFeatures,
|
|
DynamicToolsets: cfg.DynamicToolsets,
|
|
ReadOnly: cfg.ReadOnly,
|
|
Translator: t,
|
|
ContentWindowSize: cfg.ContentWindowSize,
|
|
LockdownMode: cfg.LockdownMode,
|
|
Logger: logger,
|
|
RepoAccessTTL: cfg.RepoAccessCacheTTL,
|
|
TokenScopes: tokenScopes,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create MCP server: %w", err)
|
|
}
|
|
|
|
if cfg.ExportTranslations {
|
|
// Once server is initialized, all translations are loaded
|
|
dumpTranslations()
|
|
}
|
|
|
|
// Start listening for messages
|
|
errC := make(chan error, 1)
|
|
go func() {
|
|
var in io.ReadCloser
|
|
var out io.WriteCloser
|
|
|
|
in = os.Stdin
|
|
out = os.Stdout
|
|
|
|
if cfg.EnableCommandLogging {
|
|
loggedIO := mcplog.NewIOLogger(in, out, logger)
|
|
in, out = loggedIO, loggedIO
|
|
}
|
|
|
|
// enable GitHub errors in the context
|
|
ctx := errors.ContextWithGitHubErrors(ctx)
|
|
errC <- ghServer.Run(ctx, &mcp.IOTransport{Reader: in, Writer: out})
|
|
}()
|
|
|
|
// Output github-mcp-server string
|
|
_, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n")
|
|
|
|
// Wait for shutdown signal
|
|
select {
|
|
case <-ctx.Done():
|
|
logger.Info("shutting down server", "signal", "context done")
|
|
case err := <-errC:
|
|
if err != nil {
|
|
logger.Error("error running server", "error", err)
|
|
return fmt.Errorf("error running server: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|