Skip to main content
Glama
dragomirweb
by dragomirweb
README.md
# gitlab-mcp

<!-- CI badge: update the URL once the repo is pushed to GitHub -->
<!-- ![CI](https://github.com/<owner>/gitlab-mcp/actions/workflows/ci.yml/badge.svg) -->

An MCP (Model Context Protocol) server for AI-powered GitLab merge request code reviews.

Provides 26 tools that give an AI agent everything it needs to perform thorough, structured code reviews: fetch MR details, read diffs and files, inspect CI jobs and logs, post inline comments and suggestions, manage labels, track review sessions with re-review intelligence, and approve/unapprove merge requests.

## Prerequisites

- [Bun](https://bun.sh/) >= 1.0
- A GitLab Personal Access Token (PAT) or OAuth token with `api` scope

## Quick Start

```bash
# Clone and install
git clone <your-repo-url>
cd gitlab-mcp
bun install

# Configure authentication
cp .env.example .env
# Edit .env with your GitLab PAT (see "Creating Tokens" below)

# Run the server (stdio transport)
bun run start
```

> **Note:** The `.env` file is automatically loaded when you run `bun run start` or `bun run dev` directly. When the server is spawned as a subprocess by an MCP client (Claude Code, OpenCode, etc.), environment variables must be passed through the client's configuration — see the setup examples below.

## MCP Client Setup

Connect this server to your AI coding agent. The server uses **stdio transport** — it communicates over stdin/stdout.

### Claude Code

Add via the CLI:

```bash
claude mcp add gitlab-mr-review --transport stdio \
  --env GITLAB_PAT=glpat-xxxxxxxxxxxxxxxxxxxx \
  --env GITLAB_BASE_URL=https://gitlab.com \
  --env GITLAB_PROJECT_ID=my-group/my-project \
  -- bun run start
```

Or to share with your team, add a `.mcp.json` file to the project root (use `--scope project`):

```bash
claude mcp add gitlab-mr-review --transport stdio --scope project \
  --env GITLAB_PAT=glpat-xxxxxxxxxxxxxxxxxxxx \
  --env GITLAB_BASE_URL=https://gitlab.com \
  --env GITLAB_PROJECT_ID=my-group/my-project \
  -- bun run start
```

This creates a `.mcp.json` that can be committed to version control:

```json
{
  "mcpServers": {
    "gitlab-mr-review": {
      "command": "bun",
      "args": ["run", "start"],
      "env": {
        "GITLAB_PAT": "${GITLAB_PAT}",
        "GITLAB_BASE_URL": "${GITLAB_BASE_URL:-https://gitlab.com}",
        "GITLAB_PROJECT_ID": "${GITLAB_PROJECT_ID}"
      }
    }
  }
}
```

> **Tip:** Claude Code supports `${VAR}` and `${VAR:-default}` syntax in `.mcp.json`, so you can reference environment variables instead of hardcoding secrets.

### OpenCode

Add the server to your `opencode.json` (or `opencode.jsonc`):

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "gitlab-mr-review": {
      "type": "local",
      "command": ["bun", "run", "start"],
      "enabled": true,
      "environment": {
        "GITLAB_PAT": "{env:GITLAB_PAT}",
        "GITLAB_BASE_URL": "{env:GITLAB_BASE_URL}",
        "GITLAB_PROJECT_ID": "{env:GITLAB_PROJECT_ID}"
      }
    }
  }
}
```

> **Tip:** OpenCode uses `{env:VAR}` syntax to reference environment variables. Set `GITLAB_PAT`, `GITLAB_BASE_URL`, and `GITLAB_PROJECT_ID` in your shell environment or `.env` file.

### Environment Variables

Both clients need these environment variables to be available:

| Variable | Required | Description |
|---|---|---|
| `GITLAB_PAT` | Yes* | GitLab Personal Access Token (`glpat-...`) |
| `GITLAB_OAUTH_TOKEN` | Yes* | OAuth token (alternative to PAT) |
| `GITLAB_BASE_URL` | No | GitLab instance URL (defaults to `https://gitlab.com`) |
| `GITLAB_PROJECT_ID` | No | Default project ID or path (e.g., `my-group/my-project`) |
| `LOG_LEVEL` | No | Minimum log level: `debug`, `info`, `warning`, `error` (default: `info`) |
| `GITLAB_MCP_DB_PATH` | No | SQLite file path (defaults to `gitlab-mcp.db` in this repo, not the client's working directory) |

\* One of `GITLAB_PAT` or `GITLAB_OAUTH_TOKEN` is required.

> **Important:** When the server is launched as an MCP subprocess (by Claude Code, OpenCode, etc.), the `.env` file is **not** loaded automatically. Configure environment variables through your MCP client's config (see the Claude Code and OpenCode examples above). The `.env` file is only used when running the server directly via `bun run start` or `bun run dev`.

### Creating Tokens

#### Personal Access Token (PAT) — Recommended

1. Go to your GitLab instance → click your **Avatar** (top-right) → **Edit profile**
2. In the left sidebar, select **Access** → **Personal access tokens**
3. Select **Add new token**
4. Fill in:
   - **Token name**: e.g., `mcp-code-review`
   - **Expiration date**: set an appropriate date (max 365 days)
   - **Scopes**: select **`api`** (required for full read/write access to MRs, discussions, labels, approvals)
5. Select **Create personal access token**
6. **Copy the token immediately** — it won't be shown again
7. Set it as `GITLAB_PAT` in your environment or MCP client config

The token will look like `glpat-xxxxxxxxxxxxxxxxxxxx`.

For full details, see the [GitLab PAT documentation](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token).

#### OAuth 2.0 Token (Alternative)

If you have an OAuth 2.0 access token obtained through an [OAuth2 authorization flow](https://docs.gitlab.com/api/oauth2/), set it as `GITLAB_OAUTH_TOKEN`. The server sends it via the `Authorization: Bearer` header.

Note: OAuth tokens expire after 2 hours and require external refresh. For MCP server use, **PATs are recommended** as they have longer lifetimes and don't require a refresh flow.

### Default Project ID

A project ID is a single repository — either its numeric ID or its full path, such as
`my-group/my-subgroup/my-project`. It is **not** a group or namespace: a monorepo and a
backend service are separate projects with separate IDs.

Most tools require a `project_id` parameter. You can skip passing it explicitly by
configuring a default:

1. **Environment variable:** Set `GITLAB_PROJECT_ID` in your `.env`
2. **Runtime setting:** Call `set_setting(key: "default_project_id", value: "group/project")`
3. **Explicit parameter:** Always wins when provided

Priority: explicit param > runtime setting > env var.

> **Only set a default if you review one project.** Merge request IIDs are per-project —
> every project has its own `!1`, `!42`, and so on. If you review across projects and a
> tool call omits `project_id`, the default silently resolves to the wrong project, and
> an MR with that IID probably *exists* there. The call succeeds, a review session opens
> on an unrelated merge request, and comments are posted to it — with no error at any
> point, because every later call reuses the session's project.
>
> With no default configured, the same call fails immediately with an actionable message.
> For multi-project review, leaving `GITLAB_PROJECT_ID` unset is the safer choice; agents
> can read the project path straight out of an MR URL.

## Available Tools

### Merge Request Tools (16)

| Tool | Description |
|---|---|
| `get_merge_request` | Fetch enriched MR details (approvals, commits, deployments) |
| `get_mr_diff` | Get MR diff with file filtering and generated-file exclusion |
| `get_mr_discussions` | List all discussion threads on an MR |
| `get_mr_commits` | List commits in an MR |
| `get_mr_file_content` | Read a file from the MR branch (base64 decoded) |
| `get_mr_pipelines` | Get CI/CD pipeline status for an MR |
| `get_pipeline_jobs` | List jobs in a pipeline with stage, status, and failure reason (`scope="failed"` to isolate breakage) |
| `get_job_trace` | Read a job's log output — last N lines, ANSI stripped |
| `get_mr_changes_since` | Diff changes since a previous review SHA |
| `list_merge_requests` | List MRs with filtering (state, labels, scope, search) |
| `create_merge_request_note` | Post a comment on an MR |
| `create_mr_discussion_reply` | Reply to an existing discussion thread |
| `delete_merge_request_note` | Delete a note — retract a mis-posted review comment and drop its tracked item |
| `resolve_merge_request_thread` | Resolve or unresolve a discussion thread |
| `approve_merge_request` | Approve an MR (with optional SHA safety check) |
| `unapprove_merge_request` | Remove approval from an MR |

### Label Tools (4)

| Tool | Description |
|---|---|
| `get_project_labels` | List all labels in a project |
| `add_mr_label` | Add a label to an MR |
| `remove_mr_label` | Remove a label from an MR |
| `set_mr_labels` | Replace all labels on an MR |

### Review Session Tools (4)

| Tool | Description |
|---|---|
| `start_review` | Start or resume an active review session; starts a fresh session with prior context after completed reviews |
| `add_review_comment` | Add a comment or suggestion to the review (posts to GitLab inline, tagged with a `category`; requires a diff `position`) |
| `get_review_status` | Get review progress with item details and resolution status |
| `complete_review` | Finalize review: update status, post summary, set labels, approve, or request changes |

### Settings Tools (2)

| Tool | Description |
|---|---|
| `get_setting` | Read a configuration value |
| `set_setting` | Store a configuration value |

## Usage with AI Code Reviewer

The typical review workflow:

```
1. start_review        -- Start a session, detect re-reviews
2. get_merge_request   -- Fetch MR details, approvals, CI status
3. get_mr_diff         -- Read the diff (auto-filters generated files)
4. get_mr_file_content -- Read full files for context
5. add_review_comment  -- Post inline comments and suggestions
6. complete_review     -- Post summary, set labels, approve, or request changes
```

On re-review, `start_review` automatically detects review history and provides:
- Resolution status of prior comments
- Whether new commits have been pushed since last review
- A diff of changes since the last review via `get_mr_changes_since`

### Per-author patterns

Every comment posted with `add_review_comment` can carry a `category`
(`unnecessary_comments`, `naming`, `error_handling`, `test_coverage`, `duplication`,
`complexity`, `type_safety`, `security`, `performance`, `style`, `api_contract`,
`dead_code`, `other`). Sessions record the MR author, so `start_review` can report what
that author repeatedly gets flagged for:

```json
"author": "emart",
"hint": "emart has recurring findings in past reviews: unnecessary_comments (3x across 3 MRs). Pay extra attention to these areas.",
"author_patterns": [
  { "category": "unnecessary_comments", "occurrences": 3, "unresolved": 3,
    "merge_requests": 3, "last_seen": "...", "examples": ["..."] }
]
```

A category must appear at least twice, in a session other than the one being reviewed,
before it counts as a pattern. This builds up automatically as you review — nothing to
maintain by hand.

Completed sessions (`approved`, `requested_changes`, or `closed`) are treated as
historical context, not writable sessions. If no active session exists,
`start_review` creates a fresh `in_progress` session and includes the latest
completed session under `previous_session` / `previous_review`.

## Architecture

```
MCP Client (AI Agent)
       |
       | stdio (JSON-RPC)
       |
  MCP Server (Bun)
   /        \
GitLab API   SQLite
 (REST)     (bun:sqlite)
```

- **Transport:** stdio (stdin/stdout for JSON-RPC, stderr for logs)
- **GitLab Client:** HTTP with retry/backoff, pagination, structured errors
- **Database:** SQLite for review sessions, review items, and settings
- **Logging:** Dual-mode -- JSON to stderr + MCP logging messages to client

## Development

```bash
bun run dev          # Start with hot reload
bun run start        # Start server (stdio transport)
bun run typecheck    # TypeScript type checking
bun test             # Run all tests
bun run lint         # Lint with Biome
bun run format       # Format with Biome
bun run check        # Lint + format with Biome
bun run build        # Build to dist/
```

### Project Structure

```
src/
  index.ts              Entry point
  server.ts             MCP server setup, environment validation
  logger.ts             Structured logging (stderr JSON + MCP)
  tools/
    index.ts            Tool registry with auto-logging wrapper
    merge-requests.ts   16 MR tools
    labels.ts           4 label tools
    reviews.ts          4 review session tools
    settings.ts         2 settings tools
  gitlab/
    client.ts           REST client (25 methods, retry, pagination)
    auth.ts             PAT/OAuth token handling
    errors.ts           GitLabApiError with classification
    types.ts            GitLab API response types
  db/
    index.ts            SQLite init, migrations, singleton
    schema.ts           Table DDL + TypeScript interfaces
    queries.ts          Typed query helpers
  schemas/
    index.ts            Zod schemas for all tool inputs
tests/
  16 test files, 351 tests, 698 expect() calls
```

### Adding a New Tool

1. Add Zod schema to `src/schemas/index.ts`
2. Add tool registration in the appropriate `src/tools/*.ts` file
3. If new file, export register function and add to `src/tools/index.ts`
4. Add tests
5. Run `bun run typecheck && bun test && bun run check`

## License

ISC