io.github.detailobsessed/efficient-gitlab
Token-efficient GitLab MCP: 167 tools via 3 meta-tools, field projection, keyset pagination
Open source Open in the app JSON README (API)
About
Token-efficient GitLab MCP: 167 tools via 3 meta-tools, field projection, keyset pagination
Details
- Kind
- MCP servers
- Topic
- Version control
- Publisher
- detailobsessed
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 2.33.0
- Stars
- 3
- Forks
- 1
- Last push
- 2026-05-22T20:02:48Z
- Repository state
- arquivado
- Language
- TypeScript
- License
- MIT
- Added
- 2026-08-29 03:02:41
- Updated
- 2026-08-29 03:02:41
- Origin id
io.github.detailobsessed/efficient-gitlab
README
# Efficient GitLab MCP
> [!CAUTION]
> **This repository is archived (2026-05-22).**
>
> 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.
[](https://www.npmjs.com/package/efficient-gitlab-mcp-server)
[](https://www.npmjs.com/package/efficient-gitlab-mcp-server)
[](https://github.com/detailobsessed/efficient-gitlab-mcp/actions/workflows/ci.yml)
[](#available-categories)
[](#available-categories)
[](https://opensource.org/licenses/MIT)
[](https://bun.sh/)
[](https://modelcontextprotocol.io/)
[](https://docs.gitlab.com/ee/api/)
[](https://www.typescriptlang.org/)
[](https://biomejs.dev/)
**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.
If 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.
---
## Quick Start
### Prerequisites
- A GitLab Personal Access Token with `api` scope (or `read_api` for read-only). [Create one →](https://gitlab.com/-/user_settings/personal_access_tokens)
- Node.js 18+ (for `npx`) or [Bun](https://bun.sh/) 1.0+ (for `bunx`)
### MCP client config (recommended for most users)
Add this to your MCP client config — Claude Desktop, Cursor, Claude Code, IDE extensions, etc.:
```json
{
"mcpServers": {
"gitlab": {
"command": "npx",
"args": ["efficient-gitlab-mcp-server@latest"],
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx",
"GITLAB_API_URL": "https://gitlab.com"
}
}
}
}
```
Restart 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.
> Prefer `bun`? Replace `"command": "npx"` with `"command": "bunx"`.
### Variants
**Self-hosted GitLab** — point at your instance's base URL (the server appends `/api/v4` itself):
```json
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx",
"GITLAB_API_URL": "https://gitlab.your-company.com"
}
```
**Pinned to a single project** — agents don't need to repeat `project_id`; it's used as a default:
```json
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx",
"GITLAB_API_URL": "https://gitlab.com",
"GITLAB_PROJECT_ID": "12345"
}
```
**Restricted to multiple projects** — every call must specify a `project_id` from the allow-list:
```json
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx",
"GITLAB_API_URL": "https://gitlab.com",
"GITLAB_ALLOWED_PROJECT_IDS": "12345,67890,123"
}
```
**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.
**Read-only (forced)** — keep your `api`-scope PAT but force read-only mode at the server level:
```json
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx",
"GITLAB_API_URL": "https://gitlab.com",
"GITLAB_READ_ONLY_MODE": "true"
}
```
### Other entry points
**Claude Code CLI** (one-liner add):
```bash
claude mcp add -s user gitlab \
-e GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx \
-e GITLAB_API_URL=https://gitlab.com \
-- npx efficient-gitlab-mcp-server@latest
```
**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.
**From source** (development):
```bash
git clone https://github.com/detailobsessed/efficient-gitlab-mcp.git
cd efficient-gitlab-mcp
bun install
bun run build
bun start
```
Hit a snag? See [Troubleshooting](#troubleshooting). Need to tune more env vars? See [full Configuration reference](#configuration).
---
## Table of Contents
- [Quick Start](#quick-start)
- [Why this fork?](#why-this-fork)
- [Token Efficiency](#token-efficiency)
- [Progressive Disclosure](#progressive-disclosure)
- [Field Projection](#field-projection)
- [Server-Side File Trimming](#server-side-file-trimming)
- [Keyset Pagination](#keyset-pagination)
- [What's Different From Upstream?](#whats-different-from-upstream)
- [Available Categories](#available-categories)
- [How It Works](#how-it-works)
- [Configuration](#configuration)
- [Features](#features)
- [Read-Only Mode & PAT Safety](#read-only-mode--pat-safety)
- [Secret Redaction](#secret-redaction)
- [Tool Annotations](#tool-annotations)
- [MCP Protocol Logging](#mcp-protocol-logging)
- [HTTP Transport Security](#http-transport-security)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [Upstream Tracking](#upstream-tracking)
- [Security](#security)
- [Acknowledgments](#acknowledgments)
- [License](#license)
---
## Why this fork?
GitLab'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:
1. **Tool definitions at startup.** Hundreds of tool schemas are forced into the prompt before the first user turn.
2. **List-endpoint responses.** GitLab list endpoints return objects with 100+ fields per row by default; relevant signal is usually <10 fields.
3. **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.
This fork addresses all three: progressive disclosure for tool definitions, field projection for list responses, and trimming + keyset pagination for content.
---
## Token Efficiency
### Progressive Disclosure
Instead of exposing 167 individual tools, the server exposes **3 meta-tools**:
| Meta-Tool | Purpose |
|-----------|---------|
| `list_categories` | Discover available tool categories and their activation status |
| `activate_tools` | Enable all tools in one or more categories |
| `deactivate_tools` | Disable a category once you're done — frees the tokens back |
| Approach | Tools Exposed | Approximate Token Cost |
|----------|---------------|------------------------|
| Traditional | 167 tools | ~20,000+ tokens |
| Progressive Disclosure | 3 meta-tools | ~1,500 tokens |
**~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.
### Field Projection
List 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"]`.
Currently applied to:
- `list_projects`, `list_group_projects`, `get_project`
- `list_issues`, `my_issues`, `get_issue`
- `list_merge_requests`, `get_merge_request`
- `list_pipelines`, `get_pipeline`
- `list_releases`
- `list_commits`, `get_commit`
- `get_current_user`, `get_user`, `get_users`, `search_users`
A 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.
Example — fetch a merge request with the compact default vs. the full GitLab payload:
```jsonc
// Default: ~17 fields (iid, title, state, draft, labels, branches, author, …)
{ "name": "get_merge_request", "arguments": { "merge_request_iid": 42 } }
// Opt out: the raw GitLab response
{ "name": "get_merge_request", "arguments": { "merge_request_iid": 42, "fields": "all" } }
// Custom pick
{ "name": "get_merge_request", "arguments": { "merge_request_iid": 42, "fields": ["iid", "title", "state"] } }
```
The 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.
### Server-Side File Trimming
`get_file_contents` accepts trim parameters so agents don't have to pull whole files into context just to read a function:
| Parameter | Purpose |
|-----------|---------|
| `head: N` | Return only the first N lines |
| `tail: N` | Return only the last N lines |
| `range: "start-end"` | Return a specific line range |
| `max_bytes: N` | Hard byte cap (composes with line-based trims) |
Truncated 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.
### Keyset Pagination
`get_repository_tree` supports keyset pagination (`pagination=keyset`) and returns an envelope:
```json
{
"items": [...],
"pagination_note": "Next page available — call again with pagination=keyset&page_token=..."
}
```
Large 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.
---
## What's Different From Upstream?
This 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.
### Architecture at a Glance
| Area | Upstream | This Fork |
|------|----------|-----------|
| **Architecture** | Single `index.ts` (~10K lines) | Modular `src/` with 16 tool modules |
| **Tool Discovery** | All 140+ tools exposed at once | SDK-native progressive disclosure (3 meta-tools) |
| **List Responses** | Full GitLab payload (100+ fields/row) | Field projection: compact default + opt-in `fields` |
| **File Contents** | Whole-file fetch | Server-side `head` / `tail` / `range` / `max_bytes` trimming |
| **Tree Listing** | Offset pagination only | Offset + keyset (`pagination=keyset`) with cursor envelope |
| **Tool Annotations** | Partial | Complete: `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` on every tool |
| **Configuration** | Flat individual exports | Typed `ServerConfig` interface with `loadConfig()` |
| **Logging** | `console.log` | Structured MCP protocol logger for agent observability |
| **Runtime** | Node.js + npm | Bun (faster builds, native TypeScript) |
| **Linting** | ESLint + Prettier | Strict Biome rules (`noExplicitAny`, `noNonNullAssertion`, cognitive complexity cap) |
| **CI/CD** | Basic | GitHub Actions (lint, build, test, semantic-release) |
| **Pre-commit** | None | prek hooks (typos, formatting, build verification) |
| **Feature Flags** | `USE_PIPELINE`, `USE_MILESTONE`, `USE_GITLAB_WIKI` required | None — all categories registered, dormant until activated |
### Other Improvements
- **Read-Only Mode & PAT Safety** — Automatic PAT scope detection, explicit read-only mode, and actionable 403 error messages.
- **Secret redaction** — `runners_token` is redacted from project responses by default; opt back in with `include_secrets: true`.
- **Robust schema coercion** — Booleans, numeric IDs, and stringified arrays are all coerced defensively (LLMs serialize inconsistently).
- **HTTP transport security** — DNS rebinding protection, configurable allowed hosts/origins.
- **Comprehensive test suite** — 280+ tests covering registry, config, logger, MCP integration, read-only mode, projection, and meta-tools.
- **Strict code quality** — Zero `any` types, no non-null assertions, enforced cognitive complexity limits.
- **Automated releases** — Semantic versioning with conventional commits.
---
## Available Categories
All GitLab operations are organized into **16 categories** totaling **167 tools**. All categories are registered at startup but dormant — activate the ones you need.
| Category | Tools | Description |
|----------|------:|-------------|
| `repositories` | 11 | Search, create, fork repos. Get/push files, manage branches, list tree |
| `merge-requests` | 33 | Create, update, merge MRs. Discussions, threads, diffs |
| `issues` | 14 | Create, update, delete issues. Links, discussions |
| `pipelines` | 19 | List, create, retry, cancel pipelines. Job output |
| `projects` | 10 | Project details, list, members, labels |
| `commits` | 3 | List commits, get commits, get diffs |
| `namespaces` | 3 | List, get, verify namespaces |
| `users` | 8 | User details, search, audit/project events, file uploads, current user (whoami) |
| `search` | 6 | Global, project, and group search across code, issues, MRs, commits |
| `wiki` | 10 | Wiki page management for projects and groups |
| `milestones` | 9 | Create, edit, delete milestones. Burndown events |
| `releases` | 7 | List, create, update, delete releases. Download assets |
| `webhooks` | 3 | List project webhooks and recent events |
| `work-items` | 12 | GraphQL work items: create, update, hierarchy, notes, incidents |
| `graphql` | 1 | Execute arbitrary GraphQL queries |
| `emoji-reactions` | 18 | Add, remove, and list emoji reactions on MRs / issues / work items / notes (REST + GraphQL) |
---
## How It Works
A typical agent session uses three phases — **discover**, **activate**, **work** — and optionally cleans up with **deactivate** once a category is no longer needed.
### 1. Discover (~1.5K tokens)
When the MCP client connects, the server only exposes 3 meta-tools. The agent calls `list_categories` to see what's available:
```jsonc
> list_categories()
{
"categories": [
{ "name": "repositories", "tools": 11, "active": false, "description": "Search, create, fork repos. Get/push files, manage branches, list tree" },
{ "name": "merge-requests", "tools": 33, "active": false, "description": "Create, update, merge MRs. Discussions, threads, diffs" },
{ "name": "issues", "tools": 14, "active": false, "description": "Create, update, delete issues. Links, discussions" },
// ... 13 more, 167 tools total
]
}
```
### 2. Activate
The agent decides what it needs and activates a category:
```jsonc
> activate_tools({ categories: ["merge-requests"] })
"Activated 33 tools in category 'merge-requests'."
```
The server fires a `tools/list_changed` notification so the client picks up the 33 new tool definitions live.
> **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.
### 3. Work
```jsonc
> create_merge_request({
project_id: "123",
title: "Fix bug",
source_branch: "fix",
target_branch: "main"
})
{ "id": 7891, "iid": 42, "title": "Fix bug", "state": "opened", ... }
```
### 4. Deactivate (optional)
When the agent is done with this category, it can free the tokens back:
```jsonc
> deactivate_tools({ categories: ["merge-requests"] })
"Deactivated 33 tools in category 'merge-requests'."
```
This 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.
---
## Configuration
### Core Settings
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `GITLAB_PERSONAL_ACCESS_TOKEN` | Yes\* | - | GitLab personal access token (takes priority over CI_JOB_TOKEN) |
| `CI_JOB_TOKEN` | No | - | GitLab CI job token (auto-detected in CI pipelines) |
| `GITLAB_API_URL` | No | `https://gitlab.com` | GitLab instance URL |
| `GITLAB_PROJECT_ID` | No | - | Default project ID when tools omit `project_id` |
| `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 |
| `GITLAB_READ_ONLY_MODE` | No | `false` | Only expose read-only tools. Auto-detected from PAT scopes if not set |
| `GITLAB_IS_OLD` | No | `false` | For older GitLab instances |
\*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)).
### Transport Settings
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `STREAMABLE_HTTP` | No | `false` | Enable HTTP transport |
| `SSE` | No | `false` | Enable SSE transport |
| `PORT` | No | `3002` | HTTP server port |
| `HOST` | No | `127.0.0.1` | HTTP server host |
### Logging & Security
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `LOG_LEVEL` | No | `info` | `debug`, `info`, `warn`, `error` |
| `LOG_FORMAT` | No | `pretty` | `json`, `pretty` |
| `HTTP_ALLOWED_HOSTS` | No | `localhost,127.0.0.1` | Allowed Host headers |
| `HTTP_ALLOWED_ORIGINS` | No | (any) | Allowed Origin headers |
| `HTTP_ENABLE_DNS_REBINDING_PROTECTION` | No | `true` | Enable DNS rebinding attack protection |
### Remote Authorization (Multi-tenant)
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `REMOTE_AUTHORIZATION` | No | `false` | Enable remote auth |
| `ENABLE_DYNAMIC_API_URL` | No | `false` | Allow dynamic GitLab URLs |
| `SESSION_TIMEOUT_SECONDS` | No | `3600` | Session timeout |
| `MAX_SESSIONS` | No | `1000` | Maximum concurrent sessions |
| `MAX_REQUESTS_PER_MINUTE` | No | `60` | Rate limit per session |
---
## Features
### Read-Only Mode & PAT Safety
The server provides three layers of protection for users with limited-scope Personal Access Tokens:
**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.
**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.
**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.
```
# Explicit read-only mode
GITLAB_READ_ONLY_MODE=true
# Or just use a read_api token — auto-detected!
GITLAB_PERSONAL_ACCESS_TOKEN=glpat-your-read-only-token
```
### Secret Redaction
GitLab 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`.
### Tool Annotations
Every 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:
| Tool kind | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` |
|-----------|---------------|-------------------|-----------------|-----------------|
| Read-only (list/get) | `true` | _(omit)_ | _(omit)_ | `true` |
| Create | `false` | `false` | `false` | `true` |
| Update | `false` | `true` | `true` | `true` |
| Delete | `false` | `true` | `true` | `true` |
`openWorldHint` is always `true` because every tool talks to GitLab's API. The annotation matrix is enforced by an invariants test.
### MCP Protocol Logging
The server supports MCP protocol logging for agent observability. When connected, LLM clients can receive structured log messages showing what the server is doing:
- Tool execution logs
- GitLab API call details
- Error information with context
This helps agents understand server behavior and debug issues — instead of opaque `console.log` output that only the developer sees.
### HTTP Transport Security
When using HTTP transport (`STREAMABLE_HTTP=true`), the server includes security features:
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `HTTP_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated list of allowed Host headers |
| `HTTP_ALLOWED_ORIGINS` | (any) | Comma-separated list of allowed Origin headers |
| `HTTP_ENABLE_DNS_REBINDING_PROTECTION` | `true` | Enable DNS rebinding attack protection |
Example for production:
```bash
HTTP_ALLOWED_HOSTS=api.example.com,localhost \
HTTP_ALLOWED_ORIGINS=https://app.example.com \
STREAMABLE_HTTP=true \
bun start
```
---
## Troubleshooting
### My agent activated a category but can't see the new tools
Your 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.
In **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.
### "403 Forbidden" on a tool I expected to work
The 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.
### `project_id` keeps getting rejected
If `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).
### Self-hosted GitLab not connecting
`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.
### CI tools don't work in GitLab CI
If `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.
### `runners_token` is missing from project responses
It's [redacted by default](#secret-redaction) for safety. To get it back, pass `include_secrets: true` on the call.
### List endpoint returns fewer results than expected
For `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.
### `GitLab response failed schema validation` in MCP server logs
GitLab 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.
---
## Development
```bash
# Install dependencies
bun install
# Run tests (280+ tests, <1s)
bun test
# Run tests with coverage
bun test --coverage
# Lint and format
bun run check
# Build
bun run build
```
### Schema-drift CI
The 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.
Run it locally:
```bash
export GITLAB_API_URL=https://gitlab.com
export GITLAB_PERSONAL_ACCESS_TOKEN=glpat-... # read_api scope only
export GITLAB_PROJECT_ID=12345 # must have ≥1 MR/commit/issue/pipeline
bun run drift
```
When to run it manually:
- **Before merging a PR that touches `src/schemas/`** — catches schema bugs against a real instance, complementing the fixture-based unit tests.
- **After upgrading a self-hosted GitLab** — quick sanity check that nothing in the response shape moved.
- **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.
The 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.
---
## Upstream Tracking
We 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.
---
## Security
- **Never commit tokens** — Use `.env` files (gitignored)
- **Rotate tokens** — Regenerate periodically
- **Least privilege** — Only grant necessary API scopes
- **Audit logs** — Monitor API access
- **Secret redaction** — `runners_token` is redacted by default; see [Secret Redaction](#secret-redaction)
---
## Acknowledgments
This 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.
---
## Resources
- **MCP Protocol**: [modelcontextprotocol.io](https://modelcontextprotocol.io/)
- **GitLab API**: [docs.gitlab.com/ee/api](https://docs.gitlab.com/ee/api/)
- **Bun**: [bun.sh](https://bun.sh/)
---
## License
MIT License — See [LICENSE](LICENSE) for details.
---
<p align="center">
<strong>Efficient GitLab MCP</strong><br>
AI-Powered GitLab Management with Token Efficiency<br>
<sub>Built with Bun and the Model Context Protocol</sub>
</p>