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
4 changes: 2 additions & 2 deletions cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func generateReadmeDocs(readmePath string) error {
t, _ := translations.TranslationHelper()

// Create toolset group with mock clients
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{})

// Generate toolsets documentation
toolsetsDoc := generateToolsetsDoc(tsg)
Expand Down Expand Up @@ -302,7 +302,7 @@ func generateRemoteToolsetsDoc() string {
t, _ := translations.TranslationHelper()

// Create toolset group with mock clients
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{})

// Generate table header
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
Expand Down
3 changes: 3 additions & 0 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var (
EnableCommandLogging: viper.GetBool("enable-command-logging"),
LogFilePath: viper.GetString("log-file"),
ContentWindowSize: viper.GetInt("content-window-size"),
LockdownMode: viper.GetBool("lockdown-mode"),
}
return ghmcp.RunStdioServer(stdioServerConfig)
},
Expand All @@ -82,6 +83,7 @@ func init() {
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
rootCmd.PersistentFlags().Int("content-window-size", 5000, "Specify the content window size")
rootCmd.PersistentFlags().Bool("lockdown-mode", false, "Enable lockdown mode")

// Bind flag to viper
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
Expand All @@ -92,6 +94,7 @@ func init() {
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
_ = viper.BindPFlag("content-window-size", rootCmd.PersistentFlags().Lookup("content-window-size"))
_ = viper.BindPFlag("lockdown-mode", rootCmd.PersistentFlags().Lookup("lockdown-mode"))

// Add subcommands
rootCmd.AddCommand(stdioCmd)
Expand Down
19 changes: 17 additions & 2 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type MCPServerConfig struct {

// Content window size
ContentWindowSize int

// LockdownMode indicates if we should enable lockdown mode
LockdownMode bool
}

const stdioServerLogPrefix = "stdioserver"
Expand Down Expand Up @@ -154,7 +157,15 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
}

// Create default toolsets
tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator, cfg.ContentWindowSize)
tsg := github.DefaultToolsetGroup(
cfg.ReadOnly,
getClient,
getGQLClient,
getRawClient,
cfg.Translator,
cfg.ContentWindowSize,
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
)
err = tsg.EnableToolsets(enabledToolsets, nil)

if err != nil {
Expand Down Expand Up @@ -205,6 +216,9 @@ type StdioServerConfig struct {

// Content window size
ContentWindowSize int

// LockdownMode indicates if we should enable lockdown mode
LockdownMode bool
}

// RunStdioServer is not concurrent safe.
Expand All @@ -224,6 +238,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
ReadOnly: cfg.ReadOnly,
Translator: t,
ContentWindowSize: cfg.ContentWindowSize,
LockdownMode: cfg.LockdownMode,
})
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
Expand All @@ -245,7 +260,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
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)
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode)
stdLogger := log.New(logOutput, stdioServerLogPrefix, 0)
stdioServer.SetErrorLogger(stdLogger)

Expand Down
6 changes: 6 additions & 0 deletions pkg/github/feature_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package github

// FeatureFlags defines runtime feature toggles that adjust tool behavior.
type FeatureFlags struct {
LockdownMode bool
}
31 changes: 22 additions & 9 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/lockdown"
"github.com/github/github-mcp-server/pkg/sanitize"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/go-viper/mapstructure/v2"
Expand Down Expand Up @@ -227,7 +228,7 @@ func fragmentToIssue(fragment IssueFragment) *github.Issue {
}

// GetIssue creates a tool to get details of a specific issue in a GitHub repository.
func IssueRead(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
func IssueRead(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc, flags FeatureFlags) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("issue_read",
mcp.WithDescription(t("TOOL_ISSUE_READ_DESCRIPTION", "Get information about a specific issue in a GitHub repository.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Expand Down Expand Up @@ -296,20 +297,20 @@ Options are:

switch method {
case "get":
return GetIssue(ctx, client, owner, repo, issueNumber)
return GetIssue(ctx, client, gqlClient, owner, repo, issueNumber, flags)
case "get_comments":
return GetIssueComments(ctx, client, owner, repo, issueNumber, pagination)
return GetIssueComments(ctx, client, owner, repo, issueNumber, pagination, flags)
case "get_sub_issues":
return GetSubIssues(ctx, client, owner, repo, issueNumber, pagination)
return GetSubIssues(ctx, client, owner, repo, issueNumber, pagination, flags)
case "get_labels":
return GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber)
return GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber, flags)
default:
return mcp.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil
}
}
}

func GetIssue(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {
func GetIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner string, repo string, issueNumber int, flags FeatureFlags) (*mcp.CallToolResult, error) {
issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber)
if err != nil {
return nil, fmt.Errorf("failed to get issue: %w", err)
Expand All @@ -324,6 +325,18 @@ func GetIssue(ctx context.Context, client *github.Client, owner string, repo str
return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil
}

if flags.LockdownMode {
if issue.User != nil {
shouldRemoveContent, err := lockdown.ShouldRemoveContent(ctx, gqlClient, *issue.User.Login, owner, repo)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to check lockdown mode: %v", err)), nil
}
if shouldRemoveContent {
return mcp.NewToolResultError("access to issue details is restricted by lockdown mode"), nil
}
}
}

// Sanitize title/body on response
if issue != nil {
if issue.Title != nil {
Expand All @@ -342,7 +355,7 @@ func GetIssue(ctx context.Context, client *github.Client, owner string, repo str
return mcp.NewToolResultText(string(r)), nil
}

func GetIssueComments(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
func GetIssueComments(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, pagination PaginationParams, _ FeatureFlags) (*mcp.CallToolResult, error) {
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
Page: pagination.Page,
Expand Down Expand Up @@ -372,7 +385,7 @@ func GetIssueComments(ctx context.Context, client *github.Client, owner string,
return mcp.NewToolResultText(string(r)), nil
}

func GetSubIssues(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
func GetSubIssues(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, pagination PaginationParams, _ FeatureFlags) (*mcp.CallToolResult, error) {
opts := &github.IssueListOptions{
ListOptions: github.ListOptions{
Page: pagination.Page,
Expand Down Expand Up @@ -407,7 +420,7 @@ func GetSubIssues(ctx context.Context, client *github.Client, owner string, repo
return mcp.NewToolResultText(string(r)), nil
}

func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {
func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string, repo string, issueNumber int, _ FeatureFlags) (*mcp.CallToolResult, error) {
// Get current labels on the issue using GraphQL
var query struct {
Repository struct {
Expand Down
Loading