Skip to content

Commit 28bab03

Browse files
polazsemantic-release-botCopilot
authored
feat: Token scope detection at startup with graceful degradation (#190)
* feat: detect token scopes at startup with graceful degradation (#188) Add TokenScopeDetector service that calls /api/v4/personal_access_tokens/self at startup to discover token scopes. Based on detected scopes: - Skip GraphQL introspection when token lacks api/read_api (no 401 stack traces) - Register only tools matching the token's permissions - Show clean INFO messages with actionable fix URLs - Warn when token expires within 7 days Also adds comprehensive authentication documentation (docs/guide/authentication.md) with PAT and OAuth walkthroughs, scope comparison table, and troubleshooting. Links added across all installation/deployment docs. * chore(release): 6.41.4 [skip ci] ## [6.41.4](v6.41.3...v6.41.4) (2026-01-24) ### Bug Fixes * **auth:** use PRIVATE-TOKEN header for PAT authentication instead of Bearer ([#189](#189)) ([7799dde](7799dde)), closes [#187](#187) * fix(auth): use enhancedFetch and UTC dates in token scope detection - Replace global fetch with enhancedFetch in detectTokenScopes and detectVersionViaREST (respects timeouts, proxy, TLS settings) - Parse expires_at as UTC date to avoid timezone off-by-one errors - Use URL/URLSearchParams for proper encoding in getTokenCreationUrl - Derive totalTools count dynamically from getToolScopeRequirements() - Use jest.useFakeTimers() for deterministic expiry tests - Test project/group token types, REST fallback, scope-gated paths * fix(auth): deduplicate getTokenCreationUrl call, remove as-any casts - Cache getTokenCreationUrl result in logTokenScopeInfo to avoid double URL construction - Replace `as any` with proper GitLabScope[] type in test fixtures * fix(token-scope): validate API response with Zod, remove manage_context from scope map Replace unsafe `as` type cast on /personal_access_tokens/self response with Zod schema validation via safeParse(). Returns null with a debug log if the response shape doesn't match expectations. Remove manage_context from TOOL_SCOPE_REQUIREMENTS since it manages local session state and never calls GitLab API — it's always available. * fix(token-scope): deep-clone scope map, preserve URL subpath, align doc counts - getToolScopeRequirements() now returns deep clone (arrays copied too) - getTokenCreationUrl() preserves subpath for self-hosted instances - Documentation updated: 43 scope-gated tools, read_user=2, read_api=23 - Example log output fixed to show URL-encoded comma (%2C) * fix(token-scope): validate scopes via z.enum, remove unreliable token type heuristic - Replace unsafe `as GitLabScope[]` cast with z.enum validation that filters unknown scopes (future GitLab scopes are silently ignored) - Derive GitLabScope type from const array via z.infer - Remove token type detection by name prefix — token names are user-controlled and cannot reliably determine token type * fix(token-scope): clarify log wording, use unknown token type, add scope filter test - Log message now says "scope-gated tools" to clarify count excludes always-available tools like manage_context - tokenType defaults to "unknown" since type cannot be reliably inferred - Add RegistryManager unit test verifying scope-based tool filtering * fix(test): update tokenType assertion to match unknown default * fix(token-scope): handle schemeless URL, fix expiry-today logic, update docs - getTokenCreationUrl() now catches URL parse errors for schemeless baseUrl and falls back to string concatenation - daysUntilExpiry === 0 now logs "expires today" instead of "has expired" - Troubleshooting docs updated to match current log format * Update src/services/ConnectionManager.ts Co-authored-by: Copilot <[email protected]> * Update src/services/TokenScopeDetector.ts Co-authored-by: Copilot <[email protected]> * fix(test): update schemeless URL assertion for URLSearchParams encoding --------- Co-authored-by: semantic-release-bot <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 3fa96c5 commit 28bab03

20 files changed

+1607
-11
lines changed

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default defineConfig({
7373
items: [
7474
{ text: "Introduction", link: "/guide/" },
7575
{ text: "Quick Start", link: "/guide/quick-start" },
76+
{ text: "Authentication", link: "/guide/authentication" },
7677
],
7778
},
7879
{

docs/deployment/docker-standalone.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Select "standalone" when prompted for deployment type.
6767
|----------|----------|-------------|---------|
6868
| `PORT` | Yes | Internal HTTP port ||
6969
| `HOST` | No | Bind address | `0.0.0.0` |
70-
| `GITLAB_TOKEN` | Yes | GitLab Personal Access Token ||
70+
| `GITLAB_TOKEN` | Yes | [GitLab Personal Access Token](/guide/authentication#pat) ||
7171
| `GITLAB_API_URL` | No | GitLab instance URL | `https://gitlab.com` |
7272
| `GITLAB_READ_ONLY_MODE` | No | Restrict to read-only tools | `false` |
7373

docs/deployment/local-stdio.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ All configuration is via environment variables in the `env` object:
6666

6767
| Variable | Required | Description |
6868
|----------|----------|-------------|
69-
| `GITLAB_TOKEN` | Yes | Personal Access Token |
69+
| `GITLAB_TOKEN` | Yes | [Personal Access Token](/guide/authentication#pat) |
7070
| `GITLAB_API_URL` | No | GitLab instance URL (default: `https://gitlab.com`) |
7171
| `GITLAB_PROJECT_ID` | No | Default project context |
7272
| `GITLAB_READ_ONLY_MODE` | No | Restrict to read-only tools |

docs/guide/authentication.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
---
2+
title: Authentication
3+
description: "Choose and configure authentication for GitLab MCP Server — Personal Access Token (PAT) for local use or OAuth for team deployments"
4+
head:
5+
- - meta
6+
- name: keywords
7+
content: gitlab mcp authentication, personal access token, PAT, oauth, token scopes, gitlab api token
8+
---
9+
10+
# Authentication
11+
12+
GitLab MCP Server supports two authentication modes. Choose based on your use case.
13+
14+
## Which mode do I need?
15+
16+
| Use Case | Mode | Why |
17+
|----------|------|-----|
18+
| Personal local use (Claude Code, Cursor, etc.) | **PAT** | Simple, one token in config |
19+
| CI/CD pipelines, automation | **PAT** | No user interaction needed |
20+
| Shared server for a team | **OAuth** | Per-user identity, audit trail |
21+
| Docker/cloud deployment | **OAuth** | No shared secrets, token rotation |
22+
| Claude.ai Custom Connector | **OAuth** | Required by Claude.ai |
23+
24+
## Option A: Personal Access Token (PAT) {#pat}
25+
26+
Best for: individual developers running gitlab-mcp locally.
27+
28+
### Step 1: Open Token Settings in GitLab
29+
30+
Navigate to your GitLab instance:
31+
32+
**GitLab.com:**
33+
`https://gitlab.com/-/user_settings/personal_access_tokens`
34+
35+
**Self-hosted:**
36+
`https://your-gitlab.com/-/user_settings/personal_access_tokens`
37+
38+
Or via UI: **Avatar (top-left) → Edit profile → Access tokens** (left sidebar)
39+
40+
### Step 2: Create the Token {#create-token}
41+
42+
| Field | Value | Notes |
43+
|-------|-------|-------|
44+
| **Token name** | `gitlab-mcp` | Any descriptive name |
45+
| **Expiration date** | 90 days recommended | GitLab enforces max 365 days |
46+
| **Select scopes** | See table below | |
47+
48+
### Step 3: Select Scopes {#scopes}
49+
50+
::: danger Required Scopes
51+
You MUST select `api` scope for full functionality. Without it, most tools will not work.
52+
:::
53+
54+
| Scope | Required? | What it enables |
55+
|-------|-----------|-----------------|
56+
| `api` | **YES** | All 43 scope-gated tools (projects, MRs, issues, pipelines, etc.) |
57+
| `read_user` | Recommended | User info display, avatar, email |
58+
| `read_api` | Alternative | Read-only access (use with `GITLAB_READ_ONLY_MODE=true`) |
59+
| `read_repository` | Optional | File content access (covered by `api`) |
60+
| `write_repository` | Optional | File mutations (covered by `api`) |
61+
62+
**Minimum for full functionality:** `api` + `read_user`
63+
64+
**Minimum for read-only:** `read_api` + `read_user`
65+
66+
::: warning Common mistake
67+
Selecting only `read_user` gives access to just 2 of 43 tools (user search and events only).
68+
GraphQL API is completely blocked without `api` or `read_api`.
69+
:::
70+
71+
### Step 4: Copy and Configure
72+
73+
1. Click **"Create personal access token"**
74+
2. **Copy the token immediately** — it's shown only once!
75+
3. Token format: `glpat-xxxxxxxxxxxxxxxxxxxx`
76+
77+
Add to your MCP client config:
78+
79+
```json
80+
{
81+
"mcpServers": {
82+
"gitlab": {
83+
"command": "npx",
84+
"args": ["-y", "@structured-world/gitlab-mcp"],
85+
"env": {
86+
"GITLAB_TOKEN": "glpat-your-token-here",
87+
"GITLAB_API_URL": "https://gitlab.com"
88+
}
89+
}
90+
}
91+
}
92+
```
93+
94+
## Scope Detection at Startup {#scope-detection}
95+
96+
GitLab MCP Server automatically detects your token's scopes at startup using the `/api/v4/personal_access_tokens/self` endpoint. Based on the detected scopes, the server:
97+
98+
1. **Skips GraphQL introspection** when `api` or `read_api` scope is missing (avoids 401 errors)
99+
2. **Registers only matching tools** — tools that would fail are not shown to the AI agent
100+
3. **Shows a clear message** explaining what's available and how to fix it
101+
102+
### What you see with limited scopes
103+
104+
With full access (`api` + `read_user`):
105+
```
106+
[INFO] Token "gitlab-mcp" detected (scopes: api, read_user)
107+
```
108+
109+
With limited access (`read_user` only):
110+
```
111+
[INFO] Token "gitlab-mcp" has limited scopes - 2 of 43 scope-gated tools available
112+
[INFO] GraphQL introspection skipped (requires 'api' or 'read_api' scope)
113+
[INFO] For full functionality, create a token with 'api' scope:
114+
https://gitlab.com/-/user_settings/personal_access_tokens?name=gitlab-mcp&scopes=api%2Cread_user
115+
```
116+
117+
### Token expiry warnings
118+
119+
When your token expires within 7 days:
120+
```
121+
[WARN] Token "gitlab-mcp" expires in 3 day(s)
122+
```
123+
124+
## Scope Comparison: What Breaks Without `api` {#scope-comparison}
125+
126+
| Token Scopes | Available Tools | GraphQL | REST Projects | What Works |
127+
|-------------|----------------|---------|---------------|------------|
128+
| `api, read_user` | **43/43** | Full | Full | Everything |
129+
| `read_api, read_user` | **23/43** | Read-only | Read-only | All browse_* tools |
130+
| `read_user` only | **2/43** | Blocked | Blocked | Only browse_users, browse_events |
131+
| `read_repository` only | **1/43** | Blocked | Blocked | Only browse_files |
132+
| No scopes | **0/43** | Blocked | Blocked | Nothing |
133+
134+
## Option B: OAuth (Server/Team Deployment) {#oauth}
135+
136+
Best for: shared servers, Docker deployments, team access.
137+
138+
With OAuth, each user authenticates with their own GitLab identity — no shared tokens.
139+
140+
### Step 1: Create GitLab OAuth Application
141+
142+
Navigate to: **GitLab → User Settings → Applications**
143+
(or **Admin → Applications** for instance-wide)
144+
145+
| Field | Value |
146+
|-------|-------|
147+
| **Name** | `GitLab MCP Server` |
148+
| **Redirect URI** | `https://your-mcp-server.com/oauth/callback` |
149+
| **Confidential** | No (PKCE provides security) |
150+
| **Scopes** | `api` and `read_user` |
151+
152+
Click **Save application** and copy the **Application ID**.
153+
154+
### Step 2: Configure Server Environment
155+
156+
```bash
157+
# Required
158+
OAUTH_ENABLED=true
159+
OAUTH_SESSION_SECRET=$(openssl rand -base64 32)
160+
GITLAB_OAUTH_CLIENT_ID=your-application-id
161+
GITLAB_API_URL=https://gitlab.com
162+
163+
# Server
164+
PORT=3333
165+
HOST=0.0.0.0
166+
```
167+
168+
### Step 3: Deploy
169+
170+
**Docker:**
171+
```bash
172+
docker run -d --name gitlab-mcp \
173+
-e OAUTH_ENABLED=true \
174+
-e OAUTH_SESSION_SECRET="$(openssl rand -base64 32)" \
175+
-e GITLAB_OAUTH_CLIENT_ID=your-app-id \
176+
-e GITLAB_API_URL=https://gitlab.com \
177+
-e PORT=3333 \
178+
-p 3333:3333 \
179+
ghcr.io/structured-world/gitlab-mcp:latest
180+
```
181+
182+
**Docker Compose:** See [Docker Compose deployment](/guide/installation/docker)
183+
184+
### Step 4: Add HTTPS
185+
186+
OAuth requires HTTPS. Use a reverse proxy:
187+
- [TLS/HTTPS Configuration](/advanced/tls)
188+
- Cloudflare, nginx, Caddy, or Traefik
189+
190+
### Step 5: Connect Clients
191+
192+
Each user connects and authenticates individually:
193+
1. Add server URL to MCP client config
194+
2. On first use, receive a device code
195+
3. Enter code in GitLab to authorize
196+
4. Start using tools with personal identity
197+
198+
See [OAuth details](/security/oauth) for full flow documentation.
199+
200+
## Troubleshooting {#troubleshooting}
201+
202+
### "GraphQL request failed: 401 Unauthorized"
203+
204+
Your token is missing `api` or `read_api` scope. The server will show which scopes are detected:
205+
```
206+
[INFO] Token "mcp" has limited scopes - 2 of 43 scope-gated tools available
207+
[INFO] GraphQL introspection skipped (requires 'api' or 'read_api' scope)
208+
```
209+
210+
**Fix:** Create a new token with `api` + `read_user` scopes.
211+
212+
### "403 Forbidden" on specific operations
213+
214+
Token has `read_api` but not `api`. Write operations (create, update, delete) need `api` scope.
215+
216+
### Token expired
217+
218+
Tokens have an expiration date. Check in **GitLab → Settings → Access Tokens**.
219+
The server warns when a token expires within 7 days.
220+
221+
### Scope detection not working
222+
223+
The `/personal_access_tokens/self` endpoint requires GitLab 14.0+. On older versions, the server falls back to attempting operations directly.
224+
225+
For more connection issues, see [Troubleshooting](/troubleshooting/connection).

docs/guide/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Complete reference for all environment variables.
1111

1212
| Variable | Description | Default |
1313
|----------|-------------|---------|
14-
| `GITLAB_TOKEN` | GitLab personal access token ||
14+
| `GITLAB_TOKEN` | [GitLab personal access token](/guide/authentication#pat) ||
1515
| `GITLAB_API_URL` | GitLab instance URL | `https://gitlab.com` |
1616
| `GITLAB_AUTH_COOKIE_PATH` | Path to auth cookie file (cookie-based auth) ||
1717

docs/guide/installation/claude-desktop.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ During installation, you'll be prompted for:
3030

3131
| Setting | Required | Description |
3232
|---------|----------|-------------|
33-
| GitLab Token | Yes | Personal Access Token with `api` + `read_user` scopes |
33+
| GitLab Token | Yes | [Personal Access Token](/guide/authentication#pat) with `api` + `read_user` scopes |
3434
| GitLab URL | No | Instance URL (default: `https://gitlab.com`) |
3535
| Read-Only Mode | No | Disable all write operations |
3636

docs/guide/installation/docker.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ Run GitLab MCP Server as a Docker container.
99

1010
## Prerequisites
1111

12-
Before running the container, ensure you have a GitLab Personal Access Token:
12+
Before running the container, ensure you have a [GitLab Personal Access Token](/guide/authentication#pat):
1313

1414
1. Go to **GitLab → Settings → Access Tokens → Personal Access Token**
1515
2. Create a token with `api,read_user` scopes (or `read_api,read_user` for read-only mode)
1616
3. Keep the token value ready — you'll pass it as `GITLAB_TOKEN` environment variable
1717

18+
See the [Authentication Guide](/guide/authentication#scopes) for scope details and what breaks without `api`.
19+
1820
::: tip
1921
For detailed deployment options (standalone, PostgreSQL, Compose), see [Deployment](/deployment/). For guided Docker setup, run:
2022
```bash

docs/guide/installation/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ See [Setup Wizard](/guide/installation/wizard) for a detailed walkthrough.
3232
## Prerequisites
3333

3434
- **Node.js >= 24.0.0** (for npm/npx methods)
35-
- **GitLab Personal Access Token** with `api` and `read_user` scopes
35+
- **[GitLab Personal Access Token](/guide/authentication#pat)** with `api` and `read_user` scopes
3636
- One or more [supported MCP clients](/clients/)
3737

3838
## Next Steps

docs/guide/installation/manual.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ All MCP clients use a JSON configuration with the same structure:
3030

3131
### 1. Create a GitLab Token
3232

33+
Follow the [Authentication Guide](/guide/authentication#create-token) for detailed steps, or briefly:
34+
3335
1. Go to **Settings > Access Tokens** in your GitLab instance
3436
2. Create a token with `api` and `read_user` scopes
3537
3. Copy the token value

docs/guide/installation/npm.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ All configuration is done via environment variables. See [Configuration](/guide/
5757

5858
| Variable | Description |
5959
|----------|-------------|
60-
| `GITLAB_TOKEN` | GitLab personal access token with `api` scope |
60+
| `GITLAB_TOKEN` | [GitLab personal access token](/guide/authentication#pat) with `api` scope |
6161

6262
### Optional
6363

0 commit comments

Comments
 (0)