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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,9 @@ The following sets of tools are available:
- **get_me** - Get my user profile
- No parameters required

- **get_repository_context** - Get repository context
- No parameters required

- **get_team_members** - Get team members
- **Required OAuth Scopes**: `read:org`
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `write:org`
Expand Down
6 changes: 6 additions & 0 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ var (
InsidersMode: viper.GetBool("insiders"),
ExcludeTools: excludeTools,
RepoAccessCacheTTL: &ttl,
DefaultRepository: viper.GetString("repository"),
AllowDiscoveryTools: viper.GetBool("allow-discovery-tools"),
}
return ghmcp.RunStdioServer(stdioServerConfig)
},
Expand Down Expand Up @@ -181,6 +183,8 @@ func init() {
rootCmd.PersistentFlags().Bool("lockdown-mode", false, "Enable lockdown mode")
rootCmd.PersistentFlags().Bool("insiders", false, "Enable insiders features")
rootCmd.PersistentFlags().Duration("repo-access-cache-ttl", 5*time.Minute, "Override the repo access cache TTL (e.g. 1m, 0s to disable)")
rootCmd.PersistentFlags().String("repository", "", "Default owner/repo for project-focused mode (also GITHUB_REPOSITORY env var)")
rootCmd.PersistentFlags().Bool("allow-discovery-tools", false, "Keep open-world discovery tools when --repository is set")

// HTTP-specific flags
httpCmd.Flags().Int("port", 8082, "HTTP server port")
Expand All @@ -203,6 +207,8 @@ func init() {
_ = viper.BindPFlag("lockdown-mode", rootCmd.PersistentFlags().Lookup("lockdown-mode"))
_ = viper.BindPFlag("insiders", rootCmd.PersistentFlags().Lookup("insiders"))
_ = viper.BindPFlag("repo-access-cache-ttl", rootCmd.PersistentFlags().Lookup("repo-access-cache-ttl"))
_ = viper.BindPFlag("repository", rootCmd.PersistentFlags().Lookup("repository"))
_ = viper.BindPFlag("allow-discovery-tools", rootCmd.PersistentFlags().Lookup("allow-discovery-tools"))
_ = viper.BindPFlag("port", httpCmd.Flags().Lookup("port"))
_ = viper.BindPFlag("base-url", httpCmd.Flags().Lookup("base-url"))
_ = viper.BindPFlag("base-path", httpCmd.Flags().Lookup("base-path"))
Expand Down
46 changes: 46 additions & 0 deletions docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ We currently support the following ways in which the GitHub MCP Server can be co
| Insiders Mode | `X-MCP-Insiders` header or `/insiders` URL | `--insiders` flag or `GITHUB_INSIDERS` env var |
| Feature Flags | `X-MCP-Features` header | `--features` flag |
| Scope Filtering | Always enabled | Always enabled |
| Default Repository | Not available | `--repository` flag or `GITHUB_REPOSITORY` env var |
| Server Name/Title | Not available | `GITHUB_MCP_SERVER_NAME` / `GITHUB_MCP_SERVER_TITLE` env vars or `github-mcp-server-config.json` |

> **Default behavior:** If you don't specify any configuration, the server uses the **default toolsets**: `context`, `issues`, `pull_requests`, `repos`, `users`.
Expand Down Expand Up @@ -446,6 +447,49 @@ MCP Apps is enabled by [Insiders Mode](#insiders-mode), or independently via the

---

### Repository Focus Mode

**Best for:** single-repository development workflows where agents should behave like `gh` inside a git checkout—working directly on one project instead of searching across your account first.

Set a default repository with `--repository owner/repo` or the `GITHUB_REPOSITORY` environment variable. The value accepts common formats such as `owner/repo`, `https://github.com/owner/repo`, or `git@github.com:owner/repo.git`.

When a default repository is configured:

- The `get_repository_context` tool returns the configured owner/repo and verifies token access (including fine-grained PAT permission hints when access fails).
- Server instructions tell agents to call `get_repository_context` first and use the configured owner/repo with repo-scoped tools like `list_issues`.
- Open-world discovery tools (`search_repositories`, `search_users`, `search_orgs`, `list_starred_repositories`, `create_repository`, `fork_repository`) are hidden unless you pass `--allow-discovery-tools`.

**Example (local server):**

```json
{
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"-e",
"GITHUB_REPOSITORY",
"ghcr.io/github/github-mcp-server",
"stdio",
"--read-only",
"--toolsets",
"context,issues,pull_requests"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}",
"GITHUB_REPOSITORY": "owner/repo"
}
}
```

> **Note:** The MCP server cannot read your local `.git` directory directly. Configure `GITHUB_REPOSITORY` in your MCP host settings (or pass `--repository`) to bind an instance to the project you are working on.

---

### Scope Filtering

**Automatic feature:** The server handles OAuth scopes differently depending on authentication type:
Expand All @@ -467,6 +511,8 @@ See [Scope Filtering](./scope-filtering.md) for details on how filtering works w
| Server fails to start | Invalid tool name in `--tools` or `X-MCP-Tools` | Check tool name spelling; use exact names from [Tools list](../README.md#tools) |
| Write tools not working | Read-only mode enabled | Remove `--read-only` flag or `X-MCP-Readonly` header |
| Tools missing | Toolset not enabled | Add the required toolset or specific tool |
| Agent searches all repos first | No default repository configured | Set `GITHUB_REPOSITORY` or `--repository owner/repo` and call `get_repository_context` |
| Fine-grained PAT cannot access collaborator repo | Token not authorized for that repository | Grant repository access and required permissions on the fine-grained PAT; check `get_repository_context` hint |

---

Expand Down
46 changes: 46 additions & 0 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
if err != nil {
return nil, fmt.Errorf("failed to create observability exporters: %w", err)
}
repositoryContext, err := github.BuildRepositoryContextConfig(cfg.DefaultRepository, cfg.Token, cfg.AllowDiscoveryTools)
if err != nil {
return nil, fmt.Errorf("failed to parse default repository: %w", err)
}
deps := github.NewBaseDeps(
clients.rest,
clients.gql,
Expand All @@ -146,8 +150,36 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
},
cfg.ContentWindowSize,
featureChecker,
repositoryContext,
obs,
)
if repositoryContext.DefaultRepository != nil {
access := github.VerifyRepositoryAccess(
ctx,
clients.rest,
*repositoryContext.DefaultRepository,
github.DetectTokenType(cfg.Token),
)
if access.Accessible {
cfg.Logger.Info(
"default repository access verified",
"repository",
repositoryContext.DefaultRepository.FullName,
"private",
access.Private,
)
} else {
cfg.Logger.Warn(
"default repository is not accessible with the current token",
"repository",
repositoryContext.DefaultRepository.FullName,
"error",
access.Error,
"hint",
access.Hint,
)
}
}
// Build and register the tool/resource/prompt inventory
inventoryBuilder := github.NewInventory(cfg.Translator).
WithDeprecatedAliases(github.DeprecatedToolAliases).
Expand All @@ -156,8 +188,14 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
WithTools(github.CleanTools(cfg.EnabledTools)).
WithExcludeTools(cfg.ExcludeTools).
WithServerInstructions().
WithDefaultRepository(cfg.DefaultRepository).
WithFocusRepository(repositoryContext.FocusMode).
WithFeatureChecker(featureChecker)

if repositoryContext.FocusMode {
inventoryBuilder = inventoryBuilder.WithFilter(github.CreateRepositoryFocusFilter(true))
}

// Apply token scope filtering if scopes are known (for PAT filtering)
if cfg.TokenScopes != nil {
inventoryBuilder = inventoryBuilder.WithFilter(github.CreateToolScopeFilter(cfg.TokenScopes))
Expand Down Expand Up @@ -229,6 +267,12 @@ type StdioServerConfig struct {

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

// DefaultRepository scopes the server to a single owner/repo (owner/repo format).
DefaultRepository string

// AllowDiscoveryTools keeps open-world discovery tools when DefaultRepository is set.
AllowDiscoveryTools bool
}

// RunStdioServer is not concurrent safe.
Expand Down Expand Up @@ -287,6 +331,8 @@ func RunStdioServer(cfg StdioServerConfig) error {
Logger: logger,
RepoAccessTTL: cfg.RepoAccessCacheTTL,
TokenScopes: tokenScopes,
DefaultRepository: cfg.DefaultRepository,
AllowDiscoveryTools: cfg.AllowDiscoveryTools,
})
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
Expand Down
1 change: 1 addition & 0 deletions pkg/github/context_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func Test_GetMe_IFC_FeatureFlag(t *testing.T) {
func(_ context.Context, flagName string) (bool, error) {
return flagName == FeatureFlagIFCLabels && enabled, nil
},
RepositoryContextConfig{},
stubExporters(),
)
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/github/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ type ToolDependencies interface {

// Metrics returns the metrics client
Metrics(ctx context.Context) metrics.Metrics

// GetRepositoryContext returns configured default repository context for project-focused mode.
GetRepositoryContext() RepositoryContextConfig
}

// RepositoryContextConfig holds server-level repository scoping configuration.
type RepositoryContextConfig struct {
DefaultRepository *RepositoryRef
FocusMode bool
Token string
}

// BaseDeps is the standard implementation of ToolDependencies for the local server.
Expand All @@ -125,6 +135,9 @@ type BaseDeps struct {
// Feature flag checker for runtime checks
featureChecker inventory.FeatureFlagChecker

// Repository context for project-focused mode
RepositoryContext RepositoryContextConfig

// Observability exporters (includes logger)
Obsv observability.Exporters
}
Expand All @@ -142,6 +155,7 @@ func NewBaseDeps(
flags FeatureFlags,
contentWindowSize int,
featureChecker inventory.FeatureFlagChecker,
repositoryContext RepositoryContextConfig,
obsv observability.Exporters,
) *BaseDeps {
return &BaseDeps{
Expand All @@ -153,6 +167,7 @@ func NewBaseDeps(
Flags: flags,
ContentWindowSize: contentWindowSize,
featureChecker: featureChecker,
RepositoryContext: repositoryContext,
Obsv: obsv,
}
}
Expand Down Expand Up @@ -196,6 +211,11 @@ func (d BaseDeps) Metrics(ctx context.Context) metrics.Metrics {
return d.Obsv.Metrics(ctx)
}

// GetRepositoryContext implements ToolDependencies.
func (d BaseDeps) GetRepositoryContext() RepositoryContextConfig {
return d.RepositoryContext
}

// IsFeatureEnabled checks if a feature flag is enabled.
// Returns false if the feature checker is nil, flag name is empty, or an error occurs.
// This allows tools to conditionally change behavior based on feature flags.
Expand Down Expand Up @@ -441,3 +461,8 @@ func (d *RequestDeps) IsFeatureEnabled(ctx context.Context, flagName string) boo

return enabled
}

// GetRepositoryContext implements ToolDependencies.
func (d *RequestDeps) GetRepositoryContext() RepositoryContextConfig {
return RepositoryContextConfig{}
}
4 changes: 4 additions & 0 deletions pkg/github/dependencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestIsFeatureEnabled_WithEnabledFlag(t *testing.T) {
github.FeatureFlags{},
0, // contentWindowSize
checker, // featureChecker
github.RepositoryContextConfig{},
testExporters(),
)

Expand All @@ -61,6 +62,7 @@ func TestIsFeatureEnabled_WithoutChecker(t *testing.T) {
github.FeatureFlags{},
0, // contentWindowSize
nil, // featureChecker (nil)
github.RepositoryContextConfig{},
testExporters(),
)

Expand All @@ -86,6 +88,7 @@ func TestIsFeatureEnabled_EmptyFlagName(t *testing.T) {
github.FeatureFlags{},
0, // contentWindowSize
checker, // featureChecker
github.RepositoryContextConfig{},
testExporters(),
)

Expand All @@ -111,6 +114,7 @@ func TestIsFeatureEnabled_CheckerError(t *testing.T) {
github.FeatureFlags{},
0, // contentWindowSize
checker, // featureChecker
github.RepositoryContextConfig{},
testExporters(),
)

Expand Down
1 change: 1 addition & 0 deletions pkg/github/feature_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func TestHelloWorld_ConditionalBehavior_Featureflag(t *testing.T) {
FeatureFlags{},
0,
featureCheckerFor(enabledFlags...),
RepositoryContextConfig{},
stubExporters(),
)

Expand Down
Loading