{
  "markdown": "# Efficient GitLab MCP\n\n> [!CAUTION]\n> **This repository is archived (2026-05-22).**\n>\n> Upstream [`zereight/gitlab-mcp`](https://github.com/zereight/gitlab-mcp) v2.1+ has independently implemented progressive disclosure and schema slimming (commit [`44983ec`](https://github.com/zereight/gitlab-mcp/commit/44983ec)). The differentiators that justified this fork no longer apply — please use upstream instead.\n\n[![npm version](https://img.shields.io/npm/v/efficient-gitlab-mcp-server.svg)](https://www.npmjs.com/package/efficient-gitlab-mcp-server)\n[![npm downloads](https://img.shields.io/npm/dw/efficient-gitlab-mcp-server?logo=npm&color=cb3837)](https://www.npmjs.com/package/efficient-gitlab-mcp-server)\n[![CI](https://github.com/detailobsessed/efficient-gitlab-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/detailobsessed/efficient-gitlab-mcp/actions/workflows/ci.yml)\n[![Tools](https://img.shields.io/badge/tools-167-2563EB)](#available-categories)\n[![Categories](https://img.shields.io/badge/categories-16-10B981)](#available-categories)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Bun](https://img.shields.io/badge/Bun-1.0+-f9f1e1?logo=bun&logoColor=f9f1e1)](https://bun.sh/)\n[![MCP](https://img.shields.io/badge/MCP-Compatible-8B5CF6?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0wIDE4Yy00LjQxIDAtOC0zLjU5LTgtOHMzLjU5LTggOC04IDggMy41OSA4IDgtMy41OSA4LTggOHoiLz48L3N2Zz4=)](https://modelcontextprotocol.io/)\n[![GitLab](https://img.shields.io/badge/GitLab-API-FC6D26?logo=gitlab&logoColor=white)](https://docs.gitlab.com/ee/api/)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)\n[![Biome](https://img.shields.io/badge/Biome-Strict-60A5FA?logo=biome&logoColor=white)](https://biomejs.dev/)\n\n**Token-efficient GitLab MCP server.** A fork of [zereight/gitlab-mcp](https://github.com/zereight/gitlab-mcp) re-architected for agent context budgets: **167 tools delivered through 3 meta-tools**, with field projection on every list endpoint, server-side file trimming, and keyset pagination on large directories.\n\nIf your agent's first turn against an MCP server costs ~20K tokens of tool definitions before you've asked anything, this fork is for you.\n\n---\n\n## Quick Start\n\n### Prerequisites\n\n- A GitLab Personal Access Token with `api` scope (or `read_api` for read-only). [Create one →](https://gitlab.com/-/user_settings/personal_access_tokens)\n- Node.js 18+ (for `npx`) or [Bun](https://bun.sh/) 1.0+ (for `bunx`)\n\n### MCP client config (recommended for most users)\n\nAdd this to your MCP client config — Claude Desktop, Cursor, Claude Code, IDE extensions, etc.:\n\n```json\n{\n  \"mcpServers\": {\n    \"gitlab\": {\n      \"command\": \"npx\",\n      \"args\": [\"efficient-gitlab-mcp-server@latest\"],\n      \"env\": {\n        \"GITLAB_PERSONAL_ACCESS_TOKEN\": \"glpat-xxxxxxxxxxxxxxxxxxxx\",\n        \"GITLAB_API_URL\": \"https://gitlab.com\"\n      }\n    }\n  }\n}\n```\n\nRestart your client. The server is live with 3 meta-tools (`list_categories`, `activate_tools`, `deactivate_tools`). Your agent discovers GitLab tools by activating categories on demand — see [How It Works](#how-it-works) for a worked example.\n\n> Prefer `bun`? Replace `\"command\": \"npx\"` with `\"command\": \"bunx\"`.\n\n### Variants\n\n**Self-hosted GitLab** — point at your instance's base URL (the server appends `/api/v4` itself):\n\n```json\n\"env\": {\n  \"GITLAB_PERSONAL_ACCESS_TOKEN\": \"glpat-xxxxxxxxxxxxxxxxxxxx\",\n  \"GITLAB_API_URL\": \"https://gitlab.your-company.com\"\n}\n```\n\n**Pinned to a single project** — agents don't need to repeat `project_id`; it's used as a default:\n\n```json\n\"env\": {\n  \"GITLAB_PERSONAL_ACCESS_TOKEN\": \"glpat-xxxxxxxxxxxxxxxxxxxx\",\n  \"GITLAB_API_URL\": \"https://gitlab.com\",\n  \"GITLAB_PROJECT_ID\": \"12345\"\n}\n```\n\n**Restricted to multiple projects** — every call must specify a `project_id` from the allow-list:\n\n```json\n\"env\": {\n  \"GITLAB_PERSONAL_ACCESS_TOKEN\": \"glpat-xxxxxxxxxxxxxxxxxxxx\",\n  \"GITLAB_API_URL\": \"https://gitlab.com\",\n  \"GITLAB_ALLOWED_PROJECT_IDS\": \"12345,67890,123\"\n}\n```\n\n**Read-only (auto-detected)** — use a PAT with only `read_api` scope; the server detects the limited scope at startup and only exposes the 93 read tools. No extra config needed.\n\n**Read-only (forced)** — keep your `api`-scope PAT but force read-only mode at the server level:\n\n```json\n\"env\": {\n  \"GITLAB_PERSONAL_ACCESS_TOKEN\": \"glpat-xxxxxxxxxxxxxxxxxxxx\",\n  \"GITLAB_API_URL\": \"https://gitlab.com\",\n  \"GITLAB_READ_ONLY_MODE\": \"true\"\n}\n```\n\n### Other entry points\n\n**Claude Code CLI** (one-liner add):\n\n```bash\nclaude mcp add -s user gitlab \\\n  -e GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx \\\n  -e GITLAB_API_URL=https://gitlab.com \\\n  -- npx efficient-gitlab-mcp-server@latest\n```\n\n**GitLab CI runner** — `CI_JOB_TOKEN` is auto-detected if no PAT is set. Use `GITLAB_PROJECT_ID: $CI_PROJECT_ID` to scope to the running project. No extra setup.\n\n**From source** (development):\n\n```bash\ngit clone https://github.com/detailobsessed/efficient-gitlab-mcp.git\ncd efficient-gitlab-mcp\nbun install\nbun run build\nbun start\n```\n\nHit a snag? See [Troubleshooting](#troubleshooting). Need to tune more env vars? See [full Configuration reference](#configuration).\n\n---\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Why this fork?](#why-this-fork)\n- [Token Efficiency](#token-efficiency)\n  - [Progressive Disclosure](#progressive-disclosure)\n  - [Field Projection](#field-projection)\n  - [Server-Side File Trimming](#server-side-file-trimming)\n  - [Keyset Pagination](#keyset-pagination)\n- [What's Different From Upstream?](#whats-different-from-upstream)\n- [Available Categories](#available-categories)\n- [How It Works](#how-it-works)\n- [Configuration](#configuration)\n- [Features](#features)\n  - [Read-Only Mode & PAT Safety](#read-only-mode--pat-safety)\n  - [Secret Redaction](#secret-redaction)\n  - [Tool Annotations](#tool-annotations)\n  - [MCP Protocol Logging](#mcp-protocol-logging)\n  - [HTTP Transport Security](#http-transport-security)\n- [Troubleshooting](#troubleshooting)\n- [Development](#development)\n- [Upstream Tracking](#upstream-tracking)\n- [Security](#security)\n- [Acknowledgments](#acknowledgments)\n- [License](#license)\n\n---\n\n## Why this fork?\n\nGitLab's API surface is huge, and the upstream MCP server reflects that — every tool is exposed at startup, all the time. For an agent on a context budget, that's wasteful in three places:\n\n1. **Tool definitions at startup.** Hundreds of tool schemas are forced into the prompt before the first user turn.\n2. **List-endpoint responses.** GitLab list endpoints return objects with 100+ fields per row by default; relevant signal is usually <10 fields.\n3. **File contents and large directories.** Reading a single file can pull in thousands of irrelevant lines; listing a large repository tree returns everything at once.\n\nThis fork addresses all three: progressive disclosure for tool definitions, field projection for list responses, and trimming + keyset pagination for content.\n\n---\n\n## Token Efficiency\n\n### Progressive Disclosure\n\nInstead of exposing 167 individual tools, the server exposes **3 meta-tools**:\n\n| Meta-Tool | Purpose |\n|-----------|---------|\n| `list_categories` | Discover available tool categories and their activation status |\n| `activate_tools` | Enable all tools in one or more categories |\n| `deactivate_tools` | Disable a category once you're done — frees the tokens back |\n\n| Approach | Tools Exposed | Approximate Token Cost |\n|----------|---------------|------------------------|\n| Traditional | 167 tools | ~20,000+ tokens |\n| Progressive Disclosure | 3 meta-tools | ~1,500 tokens |\n\n**~90% reduction in tool-definition tokens at startup.** Tools are registered with the MCP SDK but kept disabled (`tool.disable()`) until the LLM activates a category — activation triggers a `tools/list_changed` notification so the client picks them up live.\n\n### Field Projection\n\nList endpoints — and a growing set of singular `get_*` endpoints — return a curated, allow-listed default set of fields per resource. Callers can opt into the full payload with `fields: \"all\"` or pick their own list with `fields: [\"id\", \"name\"]`.\n\nCurrently applied to:\n\n- `list_projects`, `list_group_projects`, `get_project`\n- `list_issues`, `my_issues`, `get_issue`\n- `list_merge_requests`, `get_merge_request`\n- `list_pipelines`, `get_pipeline`\n- `list_releases`\n- `list_commits`, `get_commit`\n- `get_current_user`, `get_user`, `get_users`, `search_users`\n\nA spike measurement against `list_projects` with 5 owned projects went from **~32 KB → ~3 KB** by switching to the compact default. Because it's allow-list based, the compact output stays compact when GitLab adds new fields upstream.\n\nExample — fetch a merge request with the compact default vs. the full GitLab payload:\n\n```jsonc\n// Default: ~17 fields (iid, title, state, draft, labels, branches, author, …)\n{ \"name\": \"get_merge_request\", \"arguments\": { \"merge_request_iid\": 42 } }\n\n// Opt out: the raw GitLab response\n{ \"name\": \"get_merge_request\", \"arguments\": { \"merge_request_iid\": 42, \"fields\": \"all\" } }\n\n// Custom pick\n{ \"name\": \"get_merge_request\", \"arguments\": { \"merge_request_iid\": 42, \"fields\": [\"iid\", \"title\", \"state\"] } }\n```\n\nThe slim defaults are derived from the same Zod response schemas that validate GitLab API responses (see `src/schemas/`), so they stay in sync with the type-level shape and there's a single source of truth per resource.\n\n### Server-Side File Trimming\n\n`get_file_contents` accepts trim parameters so agents don't have to pull whole files into context just to read a function:\n\n| Parameter | Purpose |\n|-----------|---------|\n| `head: N` | Return only the first N lines |\n| `tail: N` | Return only the last N lines |\n| `range: \"start-end\"` | Return a specific line range |\n| `max_bytes: N` | Hard byte cap (composes with line-based trims) |\n\nTruncated responses include a note like `Showing lines 100-200 of 5234`, so a follow-up call can target a different range without re-fetching to count lines first.\n\n### Keyset Pagination\n\n`get_repository_tree` supports keyset pagination (`pagination=keyset`) and returns an envelope:\n\n```json\n{\n  \"items\": [...],\n  \"pagination_note\": \"Next page available — call again with pagination=keyset&page_token=...\"\n}\n```\n\nLarge monorepos no longer dump 10K entries into a single response. The server reads the cursor from `X-Next-Page-Token` (or falls back to `X-Next-Page` on older GitLab instances) and surfaces it inline.\n\n---\n\n## What's Different From Upstream?\n\nThis fork builds on [zereight/gitlab-mcp](https://github.com/zereight/gitlab-mcp) with a redesigned architecture focused on token efficiency and maintainability. We regularly review upstream commits and selectively port new features and bugfixes — we don't blindly rebase, since the codebases have structurally diverged.\n\n### Architecture at a Glance\n\n| Area | Upstream | This Fork |\n|------|----------|-----------|\n| **Architecture** | Single `index.ts` (~10K lines) | Modular `src/` with 16 tool modules |\n| **Tool Discovery** | All 140+ tools exposed at once | SDK-native progressive disclosure (3 meta-tools) |\n| **List Responses** | Full GitLab payload (100+ fields/row) | Field projection: compact default + opt-in `fields` |\n| **File Contents** | Whole-file fetch | Server-side `head` / `tail` / `range` / `max_bytes` trimming |\n| **Tree Listing** | Offset pagination only | Offset + keyset (`pagination=keyset`) with cursor envelope |\n| **Tool Annotations** | Partial | Complete: `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` on every tool |\n| **Configuration** | Flat individual exports | Typed `ServerConfig` interface with `loadConfig()` |\n| **Logging** | `console.log` | Structured MCP protocol logger for agent observability |\n| **Runtime** | Node.js + npm | Bun (faster builds, native TypeScript) |\n| **Linting** | ESLint + Prettier | Strict Biome rules (`noExplicitAny`, `noNonNullAssertion`, cognitive complexity cap) |\n| **CI/CD** | Basic | GitHub Actions (lint, build, test, semantic-release) |\n| **Pre-commit** | None | prek hooks (typos, formatting, build verification) |\n| **Feature Flags** | `USE_PIPELINE`, `USE_MILESTONE`, `USE_GITLAB_WIKI` required | None — all categories registered, dormant until activated |\n\n### Other Improvements\n\n- **Read-Only Mode & PAT Safety** — Automatic PAT scope detection, explicit read-only mode, and actionable 403 error messages.\n- **Secret redaction** — `runners_token` is redacted from project responses by default; opt back in with `include_secrets: true`.\n- **Robust schema coercion** — Booleans, numeric IDs, and stringified arrays are all coerced defensively (LLMs serialize inconsistently).\n- **HTTP transport security** — DNS rebinding protection, configurable allowed hosts/origins.\n- **Comprehensive test suite** — 280+ tests covering registry, config, logger, MCP integration, read-only mode, projection, and meta-tools.\n- **Strict code quality** — Zero `any` types, no non-null assertions, enforced cognitive complexity limits.\n- **Automated releases** — Semantic versioning with conventional commits.\n\n---\n\n## Available Categories\n\nAll GitLab operations are organized into **16 categories** totaling **167 tools**. All categories are registered at startup but dormant — activate the ones you need.\n\n| Category | Tools | Description |\n|----------|------:|-------------|\n| `repositories` | 11 | Search, create, fork repos. Get/push files, manage branches, list tree |\n| `merge-requests` | 33 | Create, update, merge MRs. Discussions, threads, diffs |\n| `issues` | 14 | Create, update, delete issues. Links, discussions |\n| `pipelines` | 19 | List, create, retry, cancel pipelines. Job output |\n| `projects` | 10 | Project details, list, members, labels |\n| `commits` | 3 | List commits, get commits, get diffs |\n| `namespaces` | 3 | List, get, verify namespaces |\n| `users` | 8 | User details, search, audit/project events, file uploads, current user (whoami) |\n| `search` | 6 | Global, project, and group search across code, issues, MRs, commits |\n| `wiki` | 10 | Wiki page management for projects and groups |\n| `milestones` | 9 | Create, edit, delete milestones. Burndown events |\n| `releases` | 7 | List, create, update, delete releases. Download assets |\n| `webhooks` | 3 | List project webhooks and recent events |\n| `work-items` | 12 | GraphQL work items: create, update, hierarchy, notes, incidents |\n| `graphql` | 1 | Execute arbitrary GraphQL queries |\n| `emoji-reactions` | 18 | Add, remove, and list emoji reactions on MRs / issues / work items / notes (REST + GraphQL) |\n\n---\n\n## How It Works\n\nA typical agent session uses three phases — **discover**, **activate**, **work** — and optionally cleans up with **deactivate** once a category is no longer needed.\n\n### 1. Discover (~1.5K tokens)\n\nWhen the MCP client connects, the server only exposes 3 meta-tools. The agent calls `list_categories` to see what's available:\n\n```jsonc\n> list_categories()\n\n{\n  \"categories\": [\n    { \"name\": \"repositories\",    \"tools\": 11, \"active\": false, \"description\": \"Search, create, fork repos. Get/push files, manage branches, list tree\" },\n    { \"name\": \"merge-requests\",  \"tools\": 33, \"active\": false, \"description\": \"Create, update, merge MRs. Discussions, threads, diffs\" },\n    { \"name\": \"issues\",          \"tools\": 14, \"active\": false, \"description\": \"Create, update, delete issues. Links, discussions\" },\n    // ... 13 more, 167 tools total\n  ]\n}\n```\n\n### 2. Activate\n\nThe agent decides what it needs and activates a category:\n\n```jsonc\n> activate_tools({ categories: [\"merge-requests\"] })\n\n\"Activated 33 tools in category 'merge-requests'.\"\n```\n\nThe server fires a `tools/list_changed` notification so the client picks up the 33 new tool definitions live.\n\n> **Claude Code latency note**: tools activated mid-turn become callable starting from the *next* turn (Claude Code rebuilds its deferred-tool index between turns). Other clients can be eager.\n\n### 3. Work\n\n```jsonc\n> create_merge_request({\n    project_id: \"123\",\n    title: \"Fix bug\",\n    source_branch: \"fix\",\n    target_branch: \"main\"\n  })\n\n{ \"id\": 7891, \"iid\": 42, \"title\": \"Fix bug\", \"state\": \"opened\", ... }\n```\n\n### 4. Deactivate (optional)\n\nWhen the agent is done with this category, it can free the tokens back:\n\n```jsonc\n> deactivate_tools({ categories: [\"merge-requests\"] })\n\n\"Deactivated 33 tools in category 'merge-requests'.\"\n```\n\nThis is especially useful in long agent sessions where context is at a premium — pull only what you need, drop it when you're done, then pull a different category.\n\n---\n\n## Configuration\n\n### Core Settings\n\n| Variable | Required | Default | Description |\n|----------|----------|---------|-------------|\n| `GITLAB_PERSONAL_ACCESS_TOKEN` | Yes\\* | - | GitLab personal access token (takes priority over CI_JOB_TOKEN) |\n| `CI_JOB_TOKEN` | No | - | GitLab CI job token (auto-detected in CI pipelines) |\n| `GITLAB_API_URL` | No | `https://gitlab.com` | GitLab instance URL |\n| `GITLAB_PROJECT_ID` | No | - | Default project ID when tools omit `project_id` |\n| `GITLAB_ALLOWED_PROJECT_IDS` | No | - | Restrict tools to these projects (comma-separated). With a single project, acts as default. With multiple, `project_id` is required per call |\n| `GITLAB_READ_ONLY_MODE` | No | `false` | Only expose read-only tools. Auto-detected from PAT scopes if not set |\n| `GITLAB_IS_OLD` | No | `false` | For older GitLab instances |\n\n\\*PAT is recommended. `CI_JOB_TOKEN` is auto-detected in GitLab CI pipelines when no PAT is set. OAuth support is planned (see [OAuth Setup Guide](./docs/oauth-setup.md)).\n\n### Transport Settings\n\n| Variable | Required | Default | Description |\n|----------|----------|---------|-------------|\n| `STREAMABLE_HTTP` | No | `false` | Enable HTTP transport |\n| `SSE` | No | `false` | Enable SSE transport |\n| `PORT` | No | `3002` | HTTP server port |\n| `HOST` | No | `127.0.0.1` | HTTP server host |\n\n### Logging & Security\n\n| Variable | Required | Default | Description |\n|----------|----------|---------|-------------|\n| `LOG_LEVEL` | No | `info` | `debug`, `info`, `warn`, `error` |\n| `LOG_FORMAT` | No | `pretty` | `json`, `pretty` |\n| `HTTP_ALLOWED_HOSTS` | No | `localhost,127.0.0.1` | Allowed Host headers |\n| `HTTP_ALLOWED_ORIGINS` | No | (any) | Allowed Origin headers |\n| `HTTP_ENABLE_DNS_REBINDING_PROTECTION` | No | `true` | Enable DNS rebinding attack protection |\n\n### Remote Authorization (Multi-tenant)\n\n| Variable | Required | Default | Description |\n|----------|----------|---------|-------------|\n| `REMOTE_AUTHORIZATION` | No | `false` | Enable remote auth |\n| `ENABLE_DYNAMIC_API_URL` | No | `false` | Allow dynamic GitLab URLs |\n| `SESSION_TIMEOUT_SECONDS` | No | `3600` | Session timeout |\n| `MAX_SESSIONS` | No | `1000` | Maximum concurrent sessions |\n| `MAX_REQUESTS_PER_MINUTE` | No | `60` | Rate limit per session |\n\n---\n\n## Features\n\n### Read-Only Mode & PAT Safety\n\nThe server provides three layers of protection for users with limited-scope Personal Access Tokens:\n\n**1. Explicit read-only mode** — Set `GITLAB_READ_ONLY_MODE=true` to restrict the server to read-only tools. Write tools won't appear in `list_categories` counts and can't be activated. This is driven by the `readOnlyHint` annotation on every tool.\n\n**2. Automatic PAT scope detection** — On startup, the server calls GitLab's `GET /personal_access_tokens/self` to inspect your token's scopes. If the token lacks the `api` scope (e.g., only has `read_api`), read-only mode is automatically enabled. No configuration needed — it just works.\n\n**3. Actionable 403 error messages** — If a tool call hits a 403 Forbidden error, the error message includes specific guidance about which PAT scopes are needed, so the LLM can inform the user rather than retrying blindly.\n\n```\n# Explicit read-only mode\nGITLAB_READ_ONLY_MODE=true\n\n# Or just use a read_api token — auto-detected!\nGITLAB_PERSONAL_ACCESS_TOKEN=glpat-your-read-only-token\n```\n\n### Secret Redaction\n\nGitLab project responses include a `runners_token` field by default — anyone with that token can register CI runners against the project. The server **redacts `runners_token` by default** on `get_project` and `list_projects` responses. To opt back in (e.g. when an agent specifically needs to manage runner registration), pass `include_secrets: true`.\n\n### Tool Annotations\n\nEvery tool declares a complete set of [MCP tool annotations](https://modelcontextprotocol.io/specification/draft/server/tools/#tool-annotations) so MCP-aware clients can offer per-action confirmation, distinguish destructive operations from idempotent updates, and filter by side-effect profile:\n\n| Tool kind | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` |\n|-----------|---------------|-------------------|-----------------|-----------------|\n| Read-only (list/get) | `true` | _(omit)_ | _(omit)_ | `true` |\n| Create | `false` | `false` | `false` | `true` |\n| Update | `false` | `true` | `true` | `true` |\n| Delete | `false` | `true` | `true` | `true` |\n\n`openWorldHint` is always `true` because every tool talks to GitLab's API. The annotation matrix is enforced by an invariants test.\n\n### MCP Protocol Logging\n\nThe server supports MCP protocol logging for agent observability. When connected, LLM clients can receive structured log messages showing what the server is doing:\n\n- Tool execution logs\n- GitLab API call details\n- Error information with context\n\nThis helps agents understand server behavior and debug issues — instead of opaque `console.log` output that only the developer sees.\n\n### HTTP Transport Security\n\nWhen using HTTP transport (`STREAMABLE_HTTP=true`), the server includes security features:\n\n| Environment Variable | Default | Description |\n|---------------------|---------|-------------|\n| `HTTP_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated list of allowed Host headers |\n| `HTTP_ALLOWED_ORIGINS` | (any) | Comma-separated list of allowed Origin headers |\n| `HTTP_ENABLE_DNS_REBINDING_PROTECTION` | `true` | Enable DNS rebinding attack protection |\n\nExample for production:\n\n```bash\nHTTP_ALLOWED_HOSTS=api.example.com,localhost \\\nHTTP_ALLOWED_ORIGINS=https://app.example.com \\\nSTREAMABLE_HTTP=true \\\nbun start\n```\n\n---\n\n## Troubleshooting\n\n### My agent activated a category but can't see the new tools\n\nYour MCP client needs to support the [`tools/list_changed` notification](https://modelcontextprotocol.io/specification/draft/server/tools/#tool-list-update-notifications) for runtime activations to be picked up. Most modern clients do.\n\nIn **Claude Code** specifically, activated tools become callable starting from the **next** turn — the client rebuilds its deferred-tool index between turns, not synchronously inside one. So calling `activate_tools({ categories: [\"issues\"] })` and then `list_issues()` in the same turn won't work; the next turn will. Other clients (Claude Desktop, Cursor) tend to be eager.\n\n### \"403 Forbidden\" on a tool I expected to work\n\nThe server returns actionable 403s — the error message tells you which PAT scopes are missing. Common cause: your PAT only has `read_api` scope (read-only) but the tool you called requires `api`. Either regenerate a PAT with `api` scope, or stay in read-only mode and use the read tools.\n\n### `project_id` keeps getting rejected\n\nIf `GITLAB_ALLOWED_PROJECT_IDS` is set with **multiple** comma-separated IDs, every tool call needs an explicit `project_id` matching one of them — there's no default. With a **single** ID, that ID is used as the default if no `project_id` is passed. Empty/unset means no restriction (any project ID is allowed).\n\n### Self-hosted GitLab not connecting\n\n`GITLAB_API_URL` should be your instance's **base URL** (`https://gitlab.your-company.com`), not the API path. The server appends `/api/v4` itself. If you use the base path with `/api/v4` already in it, calls will hit `/api/v4/api/v4/...` and 404.\n\n### CI tools don't work in GitLab CI\n\nIf `GITLAB_PERSONAL_ACCESS_TOKEN` isn't set, the server falls back to `CI_JOB_TOKEN` automatically (auto-detected from the GitLab CI environment). Set `GITLAB_PROJECT_ID: $CI_PROJECT_ID` in your `.gitlab-ci.yml` so the running pipeline's project is used as the default scope.\n\n### `runners_token` is missing from project responses\n\nIt's [redacted by default](#secret-redaction) for safety. To get it back, pass `include_secrets: true` on the call.\n\n### List endpoint returns fewer results than expected\n\nFor `list_issues`, `list_merge_requests`, etc.: GitLab's global endpoints (when no `project_id` is supplied) historically defaulted to `scope: created_by_me`. To see everything, pass `scope: \"all\"` explicitly. If you supply `project_id`, the call routes to the project-scoped endpoint and this default doesn't apply.\n\n### `GitLab response failed schema validation` in MCP server logs\n\nGitLab responses on the server's schematized read endpoints (users, projects, merge requests, commits, issues, pipelines, repository tree) run through a Zod schema. On a mismatch the server logs `WARN GitLab response failed schema validation; passing through unchanged` with the field path and Zod error code, then passes the response on to your LLM unchanged — so the call still succeeds, but you've got a signal that GitLab returned a shape we don't know about. Causes are usually GitLab API drift (new field, type change, removal) or a self-hosted EE instance returning EE-only fields. If you see one of these warnings, [open an issue](https://github.com/detailobsessed/efficient-gitlab-mcp/issues) with the `path`, `code`, and which tool triggered it — that's our cue to update the schema.\n\n---\n\n## Development\n\n```bash\n# Install dependencies\nbun install\n\n# Run tests (280+ tests, <1s)\nbun test\n\n# Run tests with coverage\nbun test --coverage\n\n# Lint and format\nbun run check\n\n# Build\nbun run build\n```\n\n### Schema-drift CI\n\nThe runtime path through `parseGitLabResponse` is intentionally lenient (`.safeParse()` + log warning + pass through) so an unexpected GitLab field never blocks an MCP tool call. The drift gate is the strict counterpart: a Bun script that calls every response-schema-bearing GitLab REST endpoint and `.parse()`s each response against its declared Zod schema, failing on any mismatch.\n\nRun it locally:\n\n```bash\nexport GITLAB_API_URL=https://gitlab.com\nexport GITLAB_PERSONAL_ACCESS_TOKEN=glpat-...   # read_api scope only\nexport GITLAB_PROJECT_ID=12345                  # must have ≥1 MR/commit/issue/pipeline\nbun run drift\n```\n\nWhen to run it manually:\n\n- **Before merging a PR that touches `src/schemas/`** — catches schema bugs against a real instance, complementing the fixture-based unit tests.\n- **After upgrading a self-hosted GitLab** — quick sanity check that nothing in the response shape moved.\n- **Triaging suspicious LLM behavior** — if responses look wrong but the tool returned OK, a schema mismatch silently passed through; drift check confirms or rules that out.\n\nThe same script runs in CI via `.github/workflows/schema-drift.yml` — `schedule` Mondays 06:00 UTC and `workflow_dispatch` on demand. The same three env vars are wired as repo secrets.\n\n---\n\n## Upstream Tracking\n\nWe maintain `main` as a read-only mirror of upstream. New features and bugfixes from upstream are reviewed and ported into our architecture as needed — we don't blindly rebase, since the codebases have structurally diverged. If you're looking for a specific upstream feature, check our [releases](https://github.com/detailobsessed/efficient-gitlab-mcp/releases) or open an issue.\n\n---\n\n## Security\n\n- **Never commit tokens** — Use `.env` files (gitignored)\n- **Rotate tokens** — Regenerate periodically\n- **Least privilege** — Only grant necessary API scopes\n- **Audit logs** — Monitor API access\n- **Secret redaction** — `runners_token` is redacted by default; see [Secret Redaction](#secret-redaction)\n\n---\n\n## Acknowledgments\n\nThis project is a fork of [zereight/gitlab-mcp](https://github.com/zereight/gitlab-mcp). Thanks to the original author for the comprehensive GitLab API implementation.\n\n---\n\n## Resources\n\n- **MCP Protocol**: [modelcontextprotocol.io](https://modelcontextprotocol.io/)\n- **GitLab API**: [docs.gitlab.com/ee/api](https://docs.gitlab.com/ee/api/)\n- **Bun**: [bun.sh](https://bun.sh/)\n\n---\n\n## License\n\nMIT License — See [LICENSE](LICENSE) for details.\n\n---\n\n<p align=\"center\">\n  <strong>Efficient GitLab MCP</strong><br>\n  AI-Powered GitLab Management with Token Efficiency<br>\n  <sub>Built with Bun and the Model Context Protocol</sub>\n</p>\n",
  "bytes": 28802,
  "sha": "fe9d9fd34e1c4cf0b03044e654250e2fcf5a966d55b0f2426c9cf6c96dd63aca",
  "repo_slug": "detailobsessed/efficient-gitlab-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_detailobsessed_efficient_gitla_0d04208f/readme"
}