Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add in memory cache for lockdown mode#1416

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
JoannaaKL merged 22 commits intomainfromlockdown-mode-more-tools
Nov 21, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
22 commits
Select commitHold shift + click to select a range
2f7f3ab
Apply lockdown mode to issues and pull requests
JoannaaKLNov 14, 2025
5562335
Add cache
JoannaaKLNov 17, 2025
e29a179
Unlock in defer
JoannaaKLNov 17, 2025
b456547
Add muesli/cache2go
JoannaaKLNov 18, 2025
b27f1e2
[WIP] Replace custom cache in lockdown.go with cache2go struct (#1425)
CopilotNov 18, 2025
626ade2
Merge branch 'lockdown-mode-more-tools' of github.com:github/github-m…
JoannaaKLNov 18, 2025
215b2db
Use muesli for cache
JoannaaKLNov 18, 2025
5bba60a
Make RepoAccessCache a singleton (#1426)
CopilotNov 18, 2025
2d630e5
Update mutexes
JoannaaKLNov 18, 2025
a09d976
Merge branch 'main' into lockdown-mode-more-tools
JoannaaKLNov 18, 2025
5da1d0a
.
JoannaaKLNov 18, 2025
d9e0e0c
Reuse cache
JoannaaKLNov 18, 2025
c46bd2e
.
JoannaaKLNov 18, 2025
7d3657e
Merge branch 'main' into lockdown-mode-more-tools
JoannaaKLNov 19, 2025
c0edac0
.
JoannaaKLNov 19, 2025
eda6b28
Fix logic after vibe coding
JoannaaKLNov 19, 2025
53c3a25
Update docs
JoannaaKLNov 19, 2025
60ce461
.
JoannaaKLNov 19, 2025
2de28f7
Refactoring to make the code pretty
JoannaaKLNov 19, 2025
c8d5b6c
Hide lockdown logic behind shouldFilter function
JoannaaKLNov 20, 2025
447c902
.
JoannaaKLNov 21, 2025
f40df27
Tests
JoannaaKLNov 21, 2025
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
17 changes: 15 additions & 2 deletionsREADME.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1264,7 +1264,7 @@ docker run -i --rm \

## Lockdown Mode

Lockdown mode limits the content that the server will surface from public repositories. When enabled,requests that fetch issue details will return an error iftheissue was created by someone who does not havepush access to the repository. Private repositories are unaffected, and collaboratorscan still access their ownissues.
Lockdown mode limits the content that the server will surface from public repositories. When enabled,the server checks whethertheauthor of each item haspush access to the repository. Private repositories are unaffected, and collaboratorskeep full accesstotheir owncontent.

```bash
./github-mcp-server --lockdown-mode
Expand All@@ -1279,7 +1279,20 @@ docker run -i --rm \
ghcr.io/github/github-mcp-server
```

At the moment lockdown mode applies to the issue read toolset, but it is designed to extend to additional data surfaces over time.
The behavior of lockdown mode depends on the tool invoked.

Following tools will return an error when the author lacks the push access:

- `issue_read:get`
- `pull_request_read:get`

Following tools will filter out content from users lacking the push access:

- `issue_read:get_comments`
- `issue_read:get_sub_issues`
- `pull_request_read:get_comments`
- `pull_request_read:get_review_comments`
- `pull_request_read:get_reviews`

## i18n / Overriding Descriptions

Expand Down
7 changes: 5 additions & 2 deletionscmd/github-mcp-server/generate_docs.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ import (
"strings"

"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/lockdown"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/toolsets"
"github.com/github/github-mcp-server/pkg/translations"
Expand DownExpand Up@@ -64,7 +65,8 @@ func generateReadmeDocs(readmePath string) error {
t, _ := translations.TranslationHelper()

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

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

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

// 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
5 changes: 5 additions & 0 deletionscmd/github-mcp-server/main.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/github/github-mcp-server/internal/ghmcp"
"github.com/github/github-mcp-server/pkg/github"
Expand DownExpand Up@@ -50,6 +51,7 @@ var (
enabledToolsets = []string{github.ToolsetMetadataDefault.ID}
}

ttl := viper.GetDuration("repo-access-cache-ttl")
stdioServerConfig := ghmcp.StdioServerConfig{
Version: version,
Host: viper.GetString("host"),
Expand All@@ -62,6 +64,7 @@ var (
LogFilePath: viper.GetString("log-file"),
ContentWindowSize: viper.GetInt("content-window-size"),
LockdownMode: viper.GetBool("lockdown-mode"),
RepoAccessCacheTTL: &ttl,
}
return ghmcp.RunStdioServer(stdioServerConfig)
},
Expand All@@ -84,6 +87,7 @@ func init() {
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")
rootCmd.PersistentFlags().Duration("repo-access-cache-ttl", 5*time.Minute, "Override the repo access cache TTL (e.g. 1m, 0s to disable)")

// Bind flag to viper
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
Expand All@@ -95,6 +99,7 @@ func init() {
_ = 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"))
_ = viper.BindPFlag("repo-access-cache-ttl", rootCmd.PersistentFlags().Lookup("repo-access-cache-ttl"))

// Add subcommands
rootCmd.AddCommand(stdioCmd)
Expand Down
3 changes: 2 additions & 1 deletiongo.mod
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ require (
github.com/mark3labs/mcp-go v0.36.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/migueleliasweb/go-github-mock v1.3.0
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
Expand DownExpand Up@@ -37,7 +38,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/google/go-querystring v1.1.0
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletionsgo.sum
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 h1:31Y+Yu373ymebRdJN1cWLLooHH8xAr0MhKTEJGV/87g=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
Expand Down
51 changes: 34 additions & 17 deletionsinternal/ghmcp/server.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ import (

"github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/lockdown"
mcplog "github.com/github/github-mcp-server/pkg/log"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
Expand DownExpand Up@@ -54,6 +55,9 @@ type MCPServerConfig struct {

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

// RepoAccessTTL overrides the default TTL for repository access cache entries.
RepoAccessTTL *time.Duration
}

const stdioServerLogPrefix = "stdioserver"
Expand All@@ -80,6 +84,14 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
},
} // We're going to wrap the Transport later in beforeInit
gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)
repoAccessOpts := []lockdown.RepoAccessOption{}
if cfg.RepoAccessTTL != nil {
repoAccessOpts = append(repoAccessOpts, lockdown.WithTTL(*cfg.RepoAccessTTL))
}
var repoAccessCache *lockdown.RepoAccessCache
if cfg.LockdownMode {
repoAccessCache = lockdown.GetInstance(gqlClient, repoAccessOpts...)
}

// When a client send an initialize request, update the user agent to include the client info.
beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) {
Expand DownExpand Up@@ -165,6 +177,7 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
cfg.Translator,
cfg.ContentWindowSize,
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
repoAccessCache,
)
err = tsg.EnableToolsets(enabledToolsets, nil)

Expand DownExpand Up@@ -219,6 +232,9 @@ type StdioServerConfig struct {

// 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.
Expand All@@ -229,23 +245,6 @@ func RunStdioServer(cfg StdioServerConfig) error {

t, dumpTranslations := translations.TranslationHelper()

ghServer, err := NewMCPServer(MCPServerConfig{
Version: cfg.Version,
Host: cfg.Host,
Token: cfg.Token,
EnabledToolsets: cfg.EnabledToolsets,
DynamicToolsets: cfg.DynamicToolsets,
ReadOnly: cfg.ReadOnly,
Translator: t,
ContentWindowSize: cfg.ContentWindowSize,
LockdownMode: cfg.LockdownMode,
})
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
}

stdioServer := server.NewStdioServer(ghServer)

var slogHandler slog.Handler
var logOutput io.Writer
if cfg.LogFilePath != "" {
Expand All@@ -262,6 +261,24 @@ func RunStdioServer(cfg StdioServerConfig) error {
logger := slog.New(slogHandler)
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)

ghServer, err := NewMCPServer(MCPServerConfig{
Version: cfg.Version,
Host: cfg.Host,
Token: cfg.Token,
EnabledToolsets: cfg.EnabledToolsets,
DynamicToolsets: cfg.DynamicToolsets,
ReadOnly: cfg.ReadOnly,
Translator: t,
ContentWindowSize: cfg.ContentWindowSize,
LockdownMode: cfg.LockdownMode,
RepoAccessTTL: cfg.RepoAccessCacheTTL,
})
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
}

stdioServer := server.NewStdioServer(ghServer)
stdioServer.SetErrorLogger(stdLogger)

if cfg.ExportTranslations {
Expand Down
77 changes: 65 additions & 12 deletionspkg/github/issues.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -228,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, flags FeatureFlags) (tool mcp.Tool, handler server.ToolHandlerFunc) {
func IssueRead(getClient GetClientFn, getGQLClient GetGQLClientFn,cache *lockdown.RepoAccessCache,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 DownExpand Up@@ -297,20 +297,20 @@ Options are:

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

func GetIssue(ctx context.Context, client *github.Client,gqlClient *githubv4.Client, owner string, repo string, issueNumber int, flags FeatureFlags) (*mcp.CallToolResult, error) {
func GetIssue(ctx context.Context, client *github.Client,cache *lockdown.RepoAccessCache, 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@@ -326,12 +326,16 @@ func GetIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Cl
}

if flags.LockdownMode {
if issue.User != nil {
shouldRemoveContent, err := lockdown.ShouldRemoveContent(ctx, gqlClient, *issue.User.Login, owner, repo)
if cache == nil {
return nil, fmt.Errorf("lockdown cache is not configured")
}
login := issue.GetUser().GetLogin()
if login != "" {
isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to check lockdown mode: %v", err)), nil
}
ifshouldRemoveContent {
if!isSafeContent {
return mcp.NewToolResultError("access to issue details is restricted by lockdown mode"), nil
}
}
Expand All@@ -355,7 +359,7 @@ func GetIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Cl
return mcp.NewToolResultText(string(r)), nil
}

func GetIssueComments(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, pagination PaginationParams,_ FeatureFlags) (*mcp.CallToolResult, error) {
func GetIssueComments(ctx context.Context, client *github.Client,cache *lockdown.RepoAccessCache,owner string, repo string, issueNumber int, pagination PaginationParams,flags FeatureFlags) (*mcp.CallToolResult, error) {
opts := &github.IssueListCommentsOptions{
ListOptions: github.ListOptions{
Page: pagination.Page,
Expand All@@ -376,6 +380,30 @@ func GetIssueComments(ctx context.Context, client *github.Client, owner string,
}
return mcp.NewToolResultError(fmt.Sprintf("failed to get issue comments: %s", string(body))), nil
}
if flags.LockdownMode {
if cache == nil {
return nil, fmt.Errorf("lockdown cache is not configured")
}
filteredComments := make([]*github.IssueComment, 0, len(comments))
for _, comment := range comments {
user := comment.User
if user == nil {
continue
}
login := user.GetLogin()
if login == "" {
continue
}
isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to check lockdown mode: %v", err)), nil
}
if isSafeContent {
filteredComments = append(filteredComments, comment)
}
}
comments = filteredComments
}

r, err := json.Marshal(comments)
if err != nil {
Expand All@@ -385,7 +413,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,_ FeatureFlags) (*mcp.CallToolResult, error) {
func GetSubIssues(ctx context.Context, client *github.Client,cache *lockdown.RepoAccessCache,owner string, repo string, issueNumber int, pagination PaginationParams,featureFlags FeatureFlags) (*mcp.CallToolResult, error) {
opts := &github.IssueListOptions{
ListOptions: github.ListOptions{
Page: pagination.Page,
Expand All@@ -412,6 +440,31 @@ func GetSubIssues(ctx context.Context, client *github.Client, owner string, repo
return mcp.NewToolResultError(fmt.Sprintf("failed to list sub-issues: %s", string(body))), nil
}

if featureFlags.LockdownMode {
if cache == nil {
return nil, fmt.Errorf("lockdown cache is not configured")
}
filteredSubIssues := make([]*github.SubIssue, 0, len(subIssues))
for _, subIssue := range subIssues {
user := subIssue.User
if user == nil {
continue
}
login := user.GetLogin()
if login == "" {
continue
}
isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to check lockdown mode: %v", err)), nil
}
if isSafeContent {
filteredSubIssues = append(filteredSubIssues, subIssue)
}
}
subIssues = filteredSubIssues
}

r, err := json.Marshal(subIssues)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
Expand All@@ -420,7 +473,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, _ FeatureFlags) (*mcp.CallToolResult, error) {
func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {
// Get current labels on the issue using GraphQL
var query struct {
Repository struct {
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp