diff --git a/README.md b/README.md index dff62321b8..49911befa5 100644 --- a/README.md +++ b/README.md @@ -1476,6 +1476,12 @@ The following sets of tools are available: +## Agent Security + +The GitHub MCP Server exposes many tools, including destructive and write operations. For production agent workflows, combine server configuration (read-only mode, tool allowlists, excluded tools) with authentication best practices and optional MCP enforcement proxies for per-tool rate limiting. + +See the **[Agent Security & Rate Limiting Guide](docs/agent-security-guide.md)** for recommended profiles, a sample security policy template, and deployment patterns. For organization-level controls, see [Policies & Governance](docs/policies-and-governance.md). + ## Read-Only Mode To run the server in read-only mode, you can use the `--read-only` flag. This will only offer read-only tools, preventing any modifications to repositories, issues, pull requests, etc. diff --git a/docs/agent-security-guide.md b/docs/agent-security-guide.md new file mode 100644 index 0000000000..f64e15cfb2 --- /dev/null +++ b/docs/agent-security-guide.md @@ -0,0 +1,159 @@ +# Agent Security & Rate Limiting Guide + +The GitHub MCP Server exposes dozens of tools, including destructive operations like `delete_file`, write operations like `push_files` and `merge_pull_request`, and resource creation like `create_repository`. By default, every enabled tool is available to the connected agent at all times. There are no built-in per-tool rate limits. + +This guide covers practical ways to limit what an AI agent can do when using the GitHub MCP Server in production workflows — without relying solely on coarse-grained PAT scopes. + +## Risks in Agent Workflows + +Agents running in a loop or responding to prompt injection can: + +- Create dozens of repositories, issues, or pull requests +- Delete files or trigger destructive GitHub Actions operations +- Merge pull requests or push commits without human review +- Exhaust GitHub API rate limits through rapid tool calls + +PAT scopes alone are often too broad for per-tool control. A `repo`-scoped token grants write access to every repository tool, not just the ones you want the agent to use. + +## Defense in Depth + +Combine multiple layers for the strongest protection: + +| Layer | What it controls | Best for | +|-------|------------------|----------| +| [Server configuration](#built-in-server-controls) | Which tools are registered at startup | All deployments | +| [PAT scopes](#authentication-and-token-scopes) | What the GitHub API allows | Local server, any host | +| [Organization policies](policies-and-governance.md) | Who can connect and with what credentials | Enterprise teams | +| [MCP enforcement proxy](#mcp-enforcement-proxies) | Per-tool blocks and rate limits at runtime | Production agent workflows | + +## Built-in Server Controls + +The GitHub MCP Server ships with configuration options that restrict tool access before any call reaches the GitHub API. See the [Server Configuration Guide](server-configuration.md) for host-specific examples. + +### Read-only mode + +The simplest safeguard. Disables all write tools regardless of other configuration. + +```bash +github-mcp-server stdio --read-only +``` + +Remote server equivalent: `X-MCP-Readonly: true` header or `/readonly` URL path. + +### Toolset and tool allowlists + +Enable only the toolsets or individual tools your workflow needs: + +```bash +github-mcp-server stdio \ + --toolsets=context,repos,issues,pull_requests \ + --tools=get_file_contents,issue_read,pull_request_read +``` + +This reduces context size for the LLM and prevents access to tools you did not explicitly enable. + +### Exclude specific tools + +When you need a broad toolset but want to block high-risk tools, use `--exclude-tools`: + +```bash +github-mcp-server stdio \ + --toolsets=repos,pull_requests \ + --exclude-tools=delete_file,merge_pull_request,push_files,create_repository +``` + +Excluded tools take precedence over toolsets and individual tool allowlists. + +Tools annotated with `DestructiveHint` in the server source are the highest-risk operations. As of this writing, they are: `delete_file`, `actions_run_trigger`, `delete_pending_pull_request_review`, `discussion_comment_write`, `projects_write`, and `remove_sub_issue`. Block these via `--exclude-tools` or your enforcement proxy even when other write tools are allowed. + +### Lockdown mode + +Limits content surfaced from public repositories to collaborators with push access. Useful when agents browse public repos but should not act on unverified external content. + +```bash +github-mcp-server stdio --lockdown-mode +``` + +### Recommended profiles + +| Use case | Configuration | +|----------|---------------| +| Code review assistant | `--read-only` or `--toolsets=context,repos,pull_requests` with `--exclude-tools=merge_pull_request` | +| Issue triage bot | `--toolsets=context,issues` with `--exclude-tools=issue_write` | +| PR authoring agent | `--toolsets=context,repos,pull_requests` with `--exclude-tools=merge_pull_request,delete_file` | +| Full automation (trusted) | Default toolsets + MCP enforcement proxy for rate limits | + +## Authentication and Token Scopes + +### Prefer fine-grained PATs + +Fine-grained PATs limit access to specific repositories and permissions. Use the minimum permissions required: + +- **Contents: Read-only** for agents that only browse code +- **Issues: Read and write** only when the agent should create or update issues +- **Pull requests: Read and write** only when the agent should open PRs + +See [PAT Scope Filtering](scope-filtering.md) for how classic PAT scopes filter available tools at startup. + +### Separate tokens per environment + +Use different tokens for development, staging, and production agent workflows. Rotate tokens on a regular schedule and store them in platform credential managers, not source code. + +### OAuth over long-lived PATs + +When your host supports it, prefer OAuth authentication via the [remote server](remote-server.md). OAuth uses scope challenges so permissions are granted incrementally as tools are used, rather than upfront with a broad PAT. + +## MCP Enforcement Proxies + +For production agent workflows, server configuration alone cannot enforce runtime rate limits or block a tool that was enabled at startup. An **MCP enforcement proxy** sits between the host application and the MCP server, inspecting each tool call before it reaches GitHub. + +Proxies can: + +- Block specific tools entirely (e.g., `delete_file`) +- Rate-limit write operations (e.g., 30 calls per hour across all write tools) +- Apply per-tool limits (e.g., 5 `create_repository` calls per hour) +- Log tool invocations for audit and alerting + +Place the proxy in your MCP server configuration so the host connects to the proxy instead of directly to the GitHub MCP Server: + +```json +{ + "servers": { + "github": { + "command": "your-mcp-proxy", + "args": ["--policy", "/path/to/security-policy.yaml", "--", "docker", "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"] + } + } +} +``` + +The exact configuration depends on your proxy. Several open-source MCP proxies support policy-based tool filtering and rate limiting. + +## Sample Security Policy + +This repository includes a [recommended security policy template](examples/recommended-security-policy.yaml) with suggested defaults: + +- **Blocked tools:** all tools annotated with `DestructiveHint` in the server (`delete_file`, `actions_run_trigger`, `delete_pending_pull_request_review`, `discussion_comment_write`, `projects_write`, and `remove_sub_issue`) +- **Write rate limit:** 30 invocations per hour across write tools +- **Repository creation limit:** 5 per hour +- **Merge limit:** 10 per hour + +The policy file is a reference template for MCP enforcement proxies. The GitHub MCP Server does not read this file directly — configure your proxy to load it, or translate the rules into your proxy's native format. + +Adapt the template to your workflow. A read-only code review agent needs fewer write allowances than a PR authoring agent. + +## Monitoring and Response + +Even with controls in place, monitor agent activity: + +- Review GitHub audit logs for unexpected API activity from your token or app +- Set up alerts on GitHub API rate limit headers (`X-RateLimit-Remaining`) +- Watch for bursts of repository, issue, or PR creation +- Require human approval for merge and delete operations in high-risk workflows + +## Related Documentation + +- [Server Configuration Guide](server-configuration.md) — toolsets, exclude-tools, read-only mode +- [Policies & Governance](policies-and-governance.md) — organization-level controls +- [Scope Filtering](scope-filtering.md) — how PAT scopes filter tools +- [Host Integration Guide](host-integration.md) — architecture for embedding the server diff --git a/docs/examples/recommended-security-policy.yaml b/docs/examples/recommended-security-policy.yaml new file mode 100644 index 0000000000..f18d935318 --- /dev/null +++ b/docs/examples/recommended-security-policy.yaml @@ -0,0 +1,96 @@ +# Recommended security policy for GitHub MCP Server agent workflows +# +# This is a reference template for MCP enforcement proxies. The GitHub MCP +# Server does not read this file directly — configure your proxy to load it, +# or translate these rules into your proxy's native policy format. +# +# See docs/agent-security-guide.md for context and deployment guidance. + +version: 1 +server: github-mcp-server +description: > + Suggested defaults for limiting destructive operations and rate-limiting + write tools when connecting the GitHub MCP Server to AI agents. + +# Tools that should never be available to agents in this policy profile. +# These map to tools annotated with DestructiveHint in the server, plus +# other high-risk operations. +blocked_tools: + - delete_file + - actions_run_trigger # can cancel runs and delete workflow logs + - delete_pending_pull_request_review # deletes a pending PR review + - discussion_comment_write # includes delete method; block if not needed + - projects_write # can delete project items and status updates + - remove_sub_issue # removes sub-issue links from a parent issue + +# Per-tool rate limits. Counts apply per authenticated token/session. +rate_limits: + # Repository and file writes + - tools: + - create_repository + limit: 5 + window: 1h + action: deny + + - tools: + - push_files + - create_or_update_file + - create_branch + - fork_repository + limit: 20 + window: 1h + action: deny + + # Pull request operations + - tools: + - merge_pull_request + limit: 10 + window: 1h + action: deny + + - tools: + - create_pull_request + - update_pull_request + - pull_request_review_write + - add_comment_to_pending_review + limit: 30 + window: 1h + action: deny + + # Issue operations + - tools: + - issue_write + - add_issue_comment + - sub_issue_write + limit: 30 + window: 1h + action: deny + + # Catch-all for remaining write tools not listed above + - category: write + limit: 30 + window: 1h + action: deny + +# Tools in this list are allowed without rate limiting (reads are cheap +# and low-risk, though GitHub API rate limits still apply). +allowed_without_limit: + category: read + +# Suggested server configuration to pair with this policy. +# Apply these flags when starting the local server, or use equivalent +# headers on the remote server. +server_configuration: + read_only: false + exclude_tools: + - delete_file + - actions_run_trigger + - delete_pending_pull_request_review + - discussion_comment_write + - projects_write + - remove_sub_issue + toolsets: + - context + - repos + - issues + - pull_requests diff --git a/docs/host-integration.md b/docs/host-integration.md index 9a1d9396ff..60cb87c7a3 100644 --- a/docs/host-integration.md +++ b/docs/host-integration.md @@ -180,6 +180,7 @@ Organizations may block **GitHub Apps** and **OAuth Apps** until explicitly appr - **PKCE:** We strongly recommend implementing [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) for all OAuth flows to prevent code interception, to prepare for upcoming PKCE support. ## Additional Resources +- [Agent Security & Rate Limiting Guide](./agent-security-guide.md) — per-tool blocks, rate limits, and recommended agent profiles - [MCP Official Spec](https://modelcontextprotocol.io/specification/draft) - [MCP SDKs](https://modelcontextprotocol.io/sdk/java/mcp-overview) - [GitHub Docs on Creating GitHub Apps](https://docs.github.com/en/apps/creating-github-apps) diff --git a/docs/policies-and-governance.md b/docs/policies-and-governance.md index d7f52212ab..b470e69c73 100644 --- a/docs/policies-and-governance.md +++ b/docs/policies-and-governance.md @@ -157,6 +157,18 @@ At present, MCP traffic appears in standard GitHub audit logs as normal API call Until those arrive, teams can continue to monitor MCP activity through existing API log entries and OAuth/GitHub App events. +## Per-Tool Controls for Agent Workflows + +Organization policies above govern *who* can connect to GitHub and *what credentials* they use. They do not provide per-tool rate limits or the ability to allow reads while blocking deletes. + +For developers connecting the GitHub MCP Server to AI agents (Claude Code, Cursor, Copilot agent mode, etc.), use server configuration and optional MCP enforcement proxies: + +- **Read-only mode** and **exclude-tools** to block destructive operations like `delete_file` +- **Toolset allowlists** to reduce the agent's available surface area +- **MCP enforcement proxies** for runtime rate limits on write operations + +See the [Agent Security & Rate Limiting Guide](./agent-security-guide.md) for recommended profiles and a [sample security policy template](./examples/recommended-security-policy.yaml). + ## Security Best Practices ### For Organizations diff --git a/docs/remote-server.md b/docs/remote-server.md index aa083d2f29..47c1190e6d 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -142,3 +142,8 @@ Example: "url": "https://api.githubcopilot.com/mcp/x/issues/readonly" } ``` + +## Related Documentation + +- [Agent Security & Rate Limiting Guide](./agent-security-guide.md) — per-tool blocks, rate limits, and recommended agent profiles +- [Server Configuration Guide](./server-configuration.md) — headers, toolsets, and exclude-tools diff --git a/docs/scope-filtering.md b/docs/scope-filtering.md index f29d631ca1..4ca185b985 100644 --- a/docs/scope-filtering.md +++ b/docs/scope-filtering.md @@ -98,6 +98,7 @@ WARN: failed to fetch token scopes, continuing without scope filtering ## Related Documentation +- [Agent Security & Rate Limiting Guide](./agent-security-guide.md) — per-tool blocks and rate limits for agent workflows - [Server Configuration Guide](./server-configuration.md) - [GitHub PAT Documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) - [OAuth Scopes Reference](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps) diff --git a/docs/server-configuration.md b/docs/server-configuration.md index 2342664c3a..9dc21db923 100644 --- a/docs/server-configuration.md +++ b/docs/server-configuration.md @@ -475,5 +475,6 @@ See [Scope Filtering](./scope-filtering.md) for details on how filtering works w - [README: Tool Configuration](../README.md#tool-configuration) - [README: Available Toolsets](../README.md#available-toolsets) — Complete list of toolsets - [README: Tools](../README.md#tools) — Complete list of individual tools +- [Agent Security & Rate Limiting Guide](./agent-security-guide.md) — Production safeguards, sample policy template - [Remote Server Documentation](./remote-server.md) — Remote-specific options and headers - [Installation Guides](./installation-guides) — Host-specific setup instructions diff --git a/pkg/github/security_policy_test.go b/pkg/github/security_policy_test.go new file mode 100644 index 0000000000..da2bd89501 --- /dev/null +++ b/pkg/github/security_policy_test.go @@ -0,0 +1,113 @@ +package github + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +type recommendedSecurityPolicy struct { + BlockedTools []string `yaml:"blocked_tools"` + RateLimits []struct { + Tools []string `yaml:"tools"` + Category string `yaml:"category"` + } `yaml:"rate_limits"` + ServerConfiguration struct { + ExcludeTools []string `yaml:"exclude_tools"` + } `yaml:"server_configuration"` +} + +func destructiveToolNames(t *testing.T) []string { + t.Helper() + + var names []string + for _, tool := range AllTools(identityTranslationHelper) { + annotations := tool.Tool.Annotations + if annotations == nil || annotations.DestructiveHint == nil || !*annotations.DestructiveHint { + continue + } + names = append(names, tool.Tool.Name) + } + + return names +} + +func loadRecommendedSecurityPolicy(t *testing.T) recommendedSecurityPolicy { + t.Helper() + + policyPath := filepath.Join("..", "..", "docs", "examples", "recommended-security-policy.yaml") + data, err := os.ReadFile(policyPath) + require.NoError(t, err, "recommended security policy file should exist") + + var policy recommendedSecurityPolicy + require.NoError(t, yaml.Unmarshal(data, &policy)) + + return policy +} + +func allToolNames(t *testing.T) map[string]struct{} { + t.Helper() + + names := make(map[string]struct{}) + for _, tool := range AllTools(identityTranslationHelper) { + names[tool.Tool.Name] = struct{}{} + } + + return names +} + +func TestRecommendedSecurityPolicyBlocksDestructiveTools(t *testing.T) { + policy := loadRecommendedSecurityPolicy(t) + destructiveTools := destructiveToolNames(t) + + blocked := make(map[string]struct{}, len(policy.BlockedTools)) + for _, name := range policy.BlockedTools { + blocked[name] = struct{}{} + } + + for _, name := range destructiveTools { + _, ok := blocked[name] + assert.True(t, ok, "blocked_tools should include destructive tool %q", name) + } + + assert.ElementsMatch(t, destructiveTools, policy.BlockedTools, + "blocked_tools should match tools annotated with DestructiveHint") +} + +func TestRecommendedSecurityPolicyReferencesValidTools(t *testing.T) { + policy := loadRecommendedSecurityPolicy(t) + knownTools := allToolNames(t) + + referenced := append([]string{}, policy.BlockedTools...) + referenced = append(referenced, policy.ServerConfiguration.ExcludeTools...) + for _, limit := range policy.RateLimits { + referenced = append(referenced, limit.Tools...) + } + + for _, name := range referenced { + if name == "" { + continue + } + _, ok := knownTools[name] + assert.True(t, ok, "policy references unknown tool %q", name) + } +} + +func TestRecommendedSecurityPolicyExcludeToolsMatchBlockedTools(t *testing.T) { + policy := loadRecommendedSecurityPolicy(t) + + assert.ElementsMatch(t, policy.BlockedTools, policy.ServerConfiguration.ExcludeTools, + "server_configuration.exclude_tools should mirror blocked_tools") +} + +func identityTranslationHelper(key string, defaultValue string) string { + if strings.HasPrefix(key, "TOOL_") { + return defaultValue + } + return key +}