-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Token scope detection at startup with graceful degradation #188
Description
Summary
Detect token scopes at startup and gracefully degrade available tools based on actual permissions. Show a clear, user-friendly message when scopes are insufficient. No error stack traces for expected scope limitations.
Problem
Currently, when a token has insufficient scopes (e.g., only read_user instead of api):
- GraphQL introspection fails with ugly stack trace in logs:
WARN: Failed to detect GitLab version via GraphQL... WARN: Schema introspection failed, using fallback schema info err: { "type": "Error", "message": "GraphQL request failed: 401 Unauthorized", "stack": Error: GraphQL request failed: 401 Unauthorized at GraphQLClient.request (...) at SchemaIntrospector.introspectSchema (...) at ConnectionManager.initialize (...) } - Server falls back to default schema info without explaining WHY
- Tools are registered but fail at runtime with unhelpful errors
- User has no idea what's wrong or how to fix it
Proposed Solution
1. Scope detection BEFORE GraphQL introspection
During ConnectionManager.initialize(), call GET /api/v4/personal_access_tokens/self FIRST to discover token scopes:
interface TokenInfo {
id: number;
name: string;
scopes: string[];
expires_at: string | null;
active: boolean;
}
async function detectTokenScopes(baseUrl: string, token: string): Promise<TokenInfo> {
const response = await fetch(`${baseUrl}/api/v4/personal_access_tokens/self`, {
headers: { "PRIVATE-TOKEN": token }
});
return response.json();
}2. Skip GraphQL introspection when scopes are insufficient
CRITICAL: If token scopes don't include api or read_api, do NOT attempt GraphQL introspection at all. This eliminates the ugly 401 error stack traces entirely:
async initialize(): Promise<void> {
const tokenInfo = await detectTokenScopes(GITLAB_BASE_URL, GITLAB_TOKEN);
const hasGraphQLAccess = tokenInfo.scopes.some(s => ['api', 'read_api'].includes(s));
if (!hasGraphQLAccess) {
// Clean INFO message, NO error/warning, NO stack trace
logger.info({
tokenName: tokenInfo.name,
scopes: tokenInfo.scopes,
availableTools: getToolsForScopes(tokenInfo.scopes).length,
}, "Token has limited scopes - GraphQL introspection skipped");
// Set minimal instance info without GraphQL
this.instanceInfo = await this.detectVersionViaREST();
this.isInitialized = true;
return; // Skip GraphQL entirely
}
// Full GraphQL introspection only when we KNOW it will work
const [instanceInfo, schemaInfo] = await Promise.all([
this.versionDetector.detectInstance(),
this.schemaIntrospector.introspectSchema(),
]);
// ...
}3. Scope-to-capability mapping
| Scope | Capabilities |
|---|---|
read_user |
User info, user search, public events only |
read_api |
All read operations (projects, MRs, issues, etc.) + GraphQL |
api |
Full read/write access + GraphQL |
read_repository |
File/repo content access |
write_repository |
File mutations |
4. Clear startup message (no scary errors)
When scopes are limited, show ONLY clean INFO messages:
[INFO] Token "mcp" detected (scopes: read_user, expires: 2026-02-23)
[INFO] Limited scopes - 2 of 45 tools available (browse_users, browse_events)
[INFO] For full functionality, add 'api' scope:
[INFO] https://gitlab.com/-/user_settings/personal_access_tokens?name=gitlab-mcp&scopes=api,read_user
NOT this (current behavior):
[WARN] Failed to detect GitLab version via GraphQL: 401 Unauthorized
[WARN] Schema introspection failed err: { "stack": "Error: GraphQL request failed..." }
5. Graceful tool registration
Only register tools that work with the available scopes:
const SCOPE_REQUIREMENTS: Record<string, string[]> = {
browse_projects: ["api", "read_api"],
manage_project: ["api"],
browse_merge_requests: ["api", "read_api"],
manage_merge_request: ["api"],
browse_users: ["read_user", "api", "read_api"], // works with any of these
browse_files: ["api", "read_api", "read_repository"],
manage_files: ["api", "write_repository"],
// ... etc
};6. Edge cases to handle
- OAuth mode: Skip scope detection (scopes come from OAuth app config)
- Group/Project tokens:
/personal_access_tokens/selfmay not work - fall back to testing endpoints - Token without self-introspection: Some older GitLab versions may not support this endpoint - fall back to capability probing
- Token expiry warning: If
expires_atis within 7 days, show a warning
Documentation: Create docs/guide/authentication.md
Problem with current docs
Current documentation about token creation is scattered and minimal:
quick-start.md— 1 line: "Create a PAT withapiandread_userscopes" + link to GitLab docsinstallation/manual.md— 3 steps without detailtroubleshooting/connection.md— brief scope tablesecurity/oauth.md— detailed OAuth but assumes PAT knowledge
Missing entirely:
- Step-by-step PAT creation with exact GitLab UI navigation
- Explanation of what each scope gives and what breaks without it
- Decision tree: when to use PAT vs OAuth
- What happens with wrong scopes (the exact error messages users will see)
- Server/Docker deployment → OAuth path with full walkthrough
Proposed: New docs/guide/authentication.md
Create a comprehensive authentication guide with two clear paths:
---
title: Authentication
description: "Choose and configure authentication for GitLab MCP Server — Personal Access Token or OAuth"
---
# Authentication
GitLab MCP Server supports two authentication modes. Choose based on your use case:
## Which mode do I need?
| Use Case | Mode | Why |
|----------|------|-----|
| Personal local use (Claude Code, Cursor, etc.) | **PAT** | Simple, one token in config |
| CI/CD pipelines, automation | **PAT** | No user interaction needed |
| Shared server for a team | **OAuth** | Per-user identity, audit trail |
| Docker/cloud deployment | **OAuth** | No shared secrets, token rotation |
| Claude.ai Custom Connector | **OAuth** | Required by Claude.ai |
## Option A: Personal Access Token (PAT)
Best for: individual developers running gitlab-mcp locally.
### Step 1: Open Token Settings in GitLab
Navigate to your GitLab instance:
**GitLab.com:**
`https://gitlab.com/-/user_settings/personal_access_tokens`
**Self-hosted:**
`https://your-gitlab.com/-/user_settings/personal_access_tokens`
Or via UI: **Avatar (top-left) > Edit profile > Access tokens** (left sidebar)
### Step 2: Create the Token
| Field | Value | Notes |
|-------|-------|-------|
| **Token name** | `gitlab-mcp` | Any descriptive name |
| **Expiration date** | 90 days recommended | GitLab enforces max 365 days |
| **Select scopes** | See table below | |
### Step 3: Select Scopes
::: danger Required Scopes
You MUST select `api` scope. Without it, most tools will not work.
:::
| Scope | Required? | What it enables |
|-------|-----------|-----------------|
| `api` | **YES** | All 45+ tools (projects, MRs, issues, pipelines, etc.) |
| `read_user` | Recommended | User info display, avatar, email |
| `read_api` | Alternative | Read-only access (use with `GITLAB_READ_ONLY_MODE=true`) |
| `read_repository` | Optional | File content access (covered by `api`) |
| `write_repository` | Optional | File mutations (covered by `api`) |
**Minimum for full functionality:** `api` + `read_user`
**Minimum for read-only:** `read_api` + `read_user`
::: warning Common mistake
Selecting only `read_user` gives access to just 2 of 45 tools (user search only).
GraphQL API is completely blocked without `api` or `read_api`.
:::
### Step 4: Copy and Configure
1. Click **"Create personal access token"**
2. **Copy the token immediately** — it's shown only once!
3. Token format: `glpat-xxxxxxxxxxxxxxxxxxxx`
Add to your MCP client config:
```json
{
"env": {
"GITLAB_TOKEN": "glpat-your-token-here",
"GITLAB_API_URL": "https://gitlab.com"
}
}Scope Comparison: What Breaks Without api
| Token Scopes | Available Tools | GraphQL | REST Projects | What Works |
|---|---|---|---|---|
api, read_user |
45/45 | Full | Full | Everything |
read_api, read_user |
~25/45 | Read-only | Read-only | All browse_* tools |
read_user only |
2/45 | Blocked | Blocked | Only browse_users |
| No scopes | 0/45 | Blocked | Blocked | Nothing |
Option B: OAuth (Server/Team Deployment)
Best for: shared servers, Docker deployments, team access.
With OAuth, each user authenticates with their own GitLab identity — no shared tokens.
Step 1: Create GitLab OAuth Application
Navigate to: GitLab > User Settings > Applications
(or Admin > Applications for instance-wide)
| Field | Value |
|---|---|
| Name | GitLab MCP Server |
| Redirect URI | https://your-mcp-server.com/oauth/callback |
| Confidential | No (PKCE provides security) |
| Scopes | api and read_user |
Click Save application and copy the Application ID.
Step 2: Configure Server Environment
# Required
OAUTH_ENABLED=true
OAUTH_SESSION_SECRET=$(openssl rand -base64 32)
GITLAB_OAUTH_CLIENT_ID=your-application-id
GITLAB_API_URL=https://gitlab.com
# Server
PORT=3333
HOST=0.0.0.0Step 3: Deploy
Docker:
docker run -d --name gitlab-mcp \
-e OAUTH_ENABLED=true \
-e OAUTH_SESSION_SECRET="$(openssl rand -base64 32)" \
-e GITLAB_OAUTH_CLIENT_ID=your-app-id \
-e GITLAB_API_URL=https://gitlab.com \
-e PORT=3333 \
-p 3333:3333 \
ghcr.io/structured-world/gitlab-mcp:latestDocker Compose: See Docker Compose deployment
Step 4: Add HTTPS
OAuth requires HTTPS. Use a reverse proxy:
- TLS/HTTPS Configuration
- Cloudflare, nginx, Caddy, or Traefik
Step 5: Connect Clients
Each user connects and authenticates individually:
- Add server URL to MCP client config
- On first use, receive a device code
- Enter code in GitLab to authorize
- Start using tools with personal identity
See OAuth details for full flow documentation.
Troubleshooting
"GraphQL request failed: 401 Unauthorized"
Your token is missing api scope. The server will show which scopes are detected:
[INFO] Token "mcp" detected (scopes: read_user)
[INFO] Limited scopes - 2 of 45 tools available
[INFO] For full functionality, add 'api' scope
Fix: Create a new token with api + read_user scopes.
"403 Forbidden" on specific operations
Token has read_api but not api. Write operations (create, update, delete) need api scope.
Token expired
Tokens have an expiration date. Check in GitLab > Settings > Access Tokens.
The server warns when a token expires within 7 days.
### Updates to existing docs
**`docs/guide/quick-start.md`** — Replace line 19:
```markdown
## 1. Get a GitLab Token
Create a [Personal Access Token](/guide/authentication#option-a-personal-access-token-pat) with `api` and `read_user` scopes.
::: tip First time?
See the [step-by-step authentication guide](/guide/authentication) for detailed instructions with screenshots.
:::
docs/guide/installation/manual.md — Replace lines 31-35:
### 1. Create a GitLab Token
Follow the [Authentication Guide](/guide/authentication#step-2-create-the-token) to create a PAT with correct scopes.docs/troubleshooting/connection.md — Add link in Scope Issues section:
For detailed scope requirements and common mistakes, see [Authentication Guide - Scope Comparison](/guide/authentication#scope-comparison-what-breaks-without-api).Sidebar navigation — Add to docs/guide/ section:
Guide
├── Quick Start
├── Authentication ← NEW
├── Installation
├── Configuration
└── Transport
Scope availability matrix (verified on gitlab.com)
With read_user only:
/api/v4/user- 200/api/v4/users- 200/api/v4/personal_access_tokens/self- 200/api/v4/events- 200/api/v4/projects- 403/api/v4/groups- 403/api/v4/todos- 403- GraphQL (
/api/graphql) - 401
Acceptance Criteria
Code
- No WARN/ERROR log entries when token has known limited scopes
- No stack traces for expected 401/403 responses
- Clean INFO message showing token name, scopes, and available tools
- GraphQL introspection skipped entirely when scope insufficient
- Only matching tools registered in MCP tool list
- Token expiry warning when < 7 days remaining
- Actionable fix URL shown to user
Documentation
- New
docs/guide/authentication.mdwith full PAT + OAuth walkthrough - Decision tree: PAT vs OAuth (when to use which)
- Step-by-step PAT creation with exact GitLab UI navigation
- Scope comparison table (what breaks without
api) - OAuth server setup walkthrough (Docker, env vars, HTTPS)
- Update
quick-start.mdto link to auth guide - Update
installation/manual.mdto link to auth guide - Update
troubleshooting/connection.mdto link scope section - Add "Authentication" to sidebar navigation
Benefits
- No scary errors - Expected limitations shown as clean INFO, not error stack traces
- Clear UX - User immediately knows what's wrong and how to fix it
- No runtime surprises - Tools that won't work are simply not registered
- Self-documenting - Shows exactly which scopes are needed
- Token hygiene - Warns about expiring tokens
- Minimal overhead - Single REST call at startup
- Comprehensive docs - Users guided through auth setup from start to finish
Related
- fix: Use PRIVATE-TOKEN header for PAT authentication instead of Bearer #187 - Use PRIVATE-TOKEN header for PAT authentication