Skip to content

feat: Token scope detection at startup with graceful degradation #188

@polaz

Description

@polaz

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/self may 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_at is 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 with api and read_user scopes" + link to GitLab docs
  • installation/manual.md — 3 steps without detail
  • troubleshooting/connection.md — brief scope table
  • security/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.0

Step 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:latest

Docker Compose: See Docker Compose deployment

Step 4: Add HTTPS

OAuth requires HTTPS. Use a reverse proxy:

Step 5: Connect Clients

Each user connects and authenticates individually:

  1. Add server URL to MCP client config
  2. On first use, receive a device code
  3. Enter code in GitLab to authorize
  4. 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.md with 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.md to link to auth guide
  • Update installation/manual.md to link to auth guide
  • Update troubleshooting/connection.md to link scope section
  • Add "Authentication" to sidebar navigation

Benefits

  1. No scary errors - Expected limitations shown as clean INFO, not error stack traces
  2. Clear UX - User immediately knows what's wrong and how to fix it
  3. No runtime surprises - Tools that won't work are simply not registered
  4. Self-documenting - Shows exactly which scopes are needed
  5. Token hygiene - Warns about expiring tokens
  6. Minimal overhead - Single REST call at startup
  7. Comprehensive docs - Users guided through auth setup from start to finish

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions