Skip to content

Commit 8afb4fb

Browse files
Only check scopes for classic PATs (ghp_ prefix)
- Scope filtering only applies to classic PATs which return X-OAuth-Scopes - Fine-grained PATs and other token types skip filtering (all tools shown) - Updated docs to clarify PAT filtering vs OAuth scope challenges
1 parent f45b94a commit 8afb4fb

File tree

2 files changed

+31
-11
lines changed

2 files changed

+31
-11
lines changed

docs/scope-filtering.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
# OAuth Scope Filtering
1+
# PAT Scope Filtering
22

3-
The GitHub MCP Server automatically filters available tools based on your Personal Access Token's (PAT) OAuth scopes. This ensures you only see tools that your token has permission to use, reducing clutter and preventing errors from attempting operations your token can't perform.
3+
The GitHub MCP Server automatically filters available tools based on your classic Personal Access Token's (PAT) OAuth scopes. This ensures you only see tools that your token has permission to use, reducing clutter and preventing errors from attempting operations your token can't perform.
4+
5+
> **Note:** This feature applies to **classic PATs** (tokens starting with `ghp_`). Fine-grained PATs and other token types don't support scope detection.
46
57
## How It Works
68

7-
When the server starts, it makes a lightweight HTTP HEAD request to the GitHub API to discover your token's scopes from the `X-OAuth-Scopes` header. Tools that require scopes your token doesn't have are automatically hidden.
9+
When the server starts with a classic PAT, it makes a lightweight HTTP HEAD request to the GitHub API to discover your token's scopes from the `X-OAuth-Scopes` header. Tools that require scopes your token doesn't have are automatically hidden.
810

911
**Example:** If your token only has `repo` and `gist` scopes, you won't see tools that require `admin:org`, `project`, or `notifications` scopes.
1012

13+
## PAT vs OAuth Authentication
14+
15+
| Authentication | Scope Handling |
16+
|---------------|----------------|
17+
| **Classic PAT** (`ghp_`) | Filters tools at startup based on token scopes—tools requiring unavailable scopes are hidden |
18+
| **OAuth** (remote server only) | Uses OAuth scope challenges—when a tool needs a scope you haven't granted, you're prompted to authorize it |
19+
| **Fine-grained PAT** (`github_pat_`) | No filtering—all tools shown, API enforces permissions |
20+
21+
With OAuth, the remote server can dynamically request additional scopes as needed. With PATs, scopes are fixed at token creation, so the server proactively hides tools you can't use.
22+
1123
## Checking Your Token's Scopes
1224

1325
To see what scopes your token has, you can run:
@@ -70,9 +82,11 @@ If the server cannot fetch your token's scopes (e.g., network issues, rate limit
7082
WARN: failed to fetch token scopes, continuing without scope filtering
7183
```
7284

73-
## Fine-Grained Personal Access Tokens
85+
## Classic vs Fine-Grained Personal Access Tokens
86+
87+
**Classic PATs** (`ghp_` prefix) support OAuth scopes and return them in the `X-OAuth-Scopes` header. Scope filtering works fully with these tokens.
7488

75-
Fine-grained PATs use a different permission model and don't return OAuth scopes in the `X-OAuth-Scopes` header. When using fine-grained PATs, scope filtering will be skipped and all tools will be available. The GitHub API will still enforce permissions at the API level.
89+
**Fine-grained PATs** (`github_pat_` prefix) use a different permission model based on repository access and specific permissions rather than OAuth scopes. They don't return the `X-OAuth-Scopes` header, so scope filtering is skipped. All tools will be available, but the GitHub API will still enforce permissions at the API level—you'll get errors if you try to use tools your token doesn't have permission for.
7690

7791
## Troubleshooting
7892

internal/ghmcp/server.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,20 @@ func RunStdioServer(cfg StdioServerConfig) error {
350350
logger := slog.New(slogHandler)
351351
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode)
352352

353-
// Fetch token scopes for scope-based tool filtering
353+
// Fetch token scopes for scope-based tool filtering (PAT tokens only)
354+
// Only classic PATs (ghp_ prefix) return OAuth scopes via X-OAuth-Scopes header.
355+
// Fine-grained PATs and other token types don't support this, so we skip filtering.
354356
var tokenScopes []string
355-
fetchedScopes, err := fetchTokenScopesForHost(ctx, cfg.Token, cfg.Host)
356-
if err != nil {
357-
logger.Warn("failed to fetch token scopes, continuing without scope filtering", "error", err)
357+
if strings.HasPrefix(cfg.Token, "ghp_") {
358+
fetchedScopes, err := fetchTokenScopesForHost(ctx, cfg.Token, cfg.Host)
359+
if err != nil {
360+
logger.Warn("failed to fetch token scopes, continuing without scope filtering", "error", err)
361+
} else {
362+
tokenScopes = fetchedScopes
363+
logger.Info("token scopes fetched for filtering", "scopes", tokenScopes)
364+
}
358365
} else {
359-
tokenScopes = fetchedScopes
360-
logger.Info("token scopes fetched for filtering", "scopes", tokenScopes)
366+
logger.Debug("skipping scope filtering for non-PAT token")
361367
}
362368

363369
ghServer, err := NewMCPServer(MCPServerConfig{

0 commit comments

Comments
 (0)