GitLab MCP Server¶
The GitLab MCP Server (mamba-mcp-gitlab) exposes 18 MCP tools for interacting with GitLab merge requests, issues, CI/CD pipelines, and search. It connects to any GitLab instance (self-managed or GitLab.com) using either a Personal Access Token or OAuth 2.0 client credentials, with optional read-only mode and client-side rate limiting.
Overview¶
graph LR
A[MCP Client] -->|stdio / streamable-http| B[mamba-mcp-gitlab]
B -->|httpx async| C[GitLab API v4]
B --- D[Rate Limiter]
B --- E[Auth Strategy]
E -->|PAT| C
E -->|OAuth 2.0| C
| Feature | Details |
|---|---|
| Tools | 18 across 4 categories |
| Authentication | Personal Access Token (PAT) or OAuth 2.0 client credentials |
| Read-only mode | Runtime gating on 5 write tools |
| Rate limiting | Sliding window, configurable requests/window |
| HTTP client | httpx async with connection pooling |
| Transport | stdio (default), streamable-http |
Installation¶
Or within the monorepo workspace:
Dependencies
| Package | Version | Purpose |
|---|---|---|
mamba-mcp-core |
workspace | Shared CLI, errors, fuzzy matching |
mcp |
>=1.0.0 |
FastMCP server framework |
httpx |
>=0.27.0 |
Async HTTP client |
pydantic |
>=2.0.0 |
Data models and validation |
pydantic-settings |
>=2.0.0 |
Environment-based configuration |
typer |
>=0.12.0 |
CLI framework |
Quick Start¶
1. Create a configuration file¶
2. Test connectivity¶
A successful test prints the authenticated username and token scopes:
3. Start the server¶
# STDIO transport (default)
mamba-mcp-gitlab --env-file mamba.env
# Streamable HTTP transport
MAMBA_MCP_GITLAB_SERVER_TRANSPORT=streamable-http mamba-mcp-gitlab
4. Connect with mamba-mcp-client¶
uv run --package mamba-mcp-client mamba-mcp tools --stdio "mamba-mcp-gitlab"
uv run --package mamba-mcp-client mamba-mcp call list_mrs \
--args '{"project_id": 123, "state": "opened"}' \
--stdio "mamba-mcp-gitlab"
Configuration¶
Configuration is loaded from environment variables or a mamba.env file (auto-discovered in the current directory or ~/mamba.env). Use --env-file to specify a custom path.
GitLab Settings¶
Prefix: MAMBA_MCP_GITLAB_
| Variable | Type | Default | Description |
|---|---|---|---|
URL |
str |
required | GitLab instance URL (e.g., https://gitlab.example.com) |
TOKEN |
SecretStr |
None |
Personal Access Token for PAT authentication |
VERIFY_SSL |
bool |
true |
Whether to verify SSL certificates |
DEFAULT_PROJECT_ID |
int |
None |
Default project scope when project_id is omitted |
DEFAULT_GROUP_ID |
int |
None |
Default group scope for search operations |
READ_ONLY |
bool |
false |
Block write tools at runtime |
OAuth Settings¶
Prefix: MAMBA_MCP_GITLAB_OAUTH_
| Variable | Type | Default | Description |
|---|---|---|---|
CLIENT_ID |
str |
None |
OAuth 2.0 application client ID |
CLIENT_SECRET |
SecretStr |
None |
OAuth 2.0 application client secret |
REDIRECT_URI |
str |
None |
OAuth 2.0 redirect URI |
Server Settings¶
Prefix: MAMBA_MCP_GITLAB_SERVER_
| Variable | Type | Default | Description |
|---|---|---|---|
TRANSPORT |
str |
stdio |
Transport type: stdio, http, or streamable-http |
SERVER_HOST |
str |
127.0.0.1 |
HTTP server bind address |
SERVER_PORT |
int |
8004 |
HTTP server bind port (1--65535) |
LOG_LEVEL |
str |
INFO |
Logging level |
LOG_FORMAT |
str |
text |
Log format: json or text |
MAX_CONNECTIONS |
int |
10 |
Maximum concurrent HTTP connections to GitLab (1--100) |
Transport normalization
Both http and streamable-http are accepted as transport values. The server normalizes http to streamable-http internally via mamba-mcp-core.
Rate Limit Settings¶
Prefix: MAMBA_MCP_GITLAB_RATE_LIMIT_
| Variable | Type | Default | Description |
|---|---|---|---|
MAX_REQUESTS |
int |
100 |
Maximum requests per time window (0 disables) |
WINDOW_SECONDS |
int |
60 |
Sliding window duration in seconds |
Tool Reference¶
All 18 tools are organized into four categories. Every tool returns a structured Pydantic output model on success or a structured ToolError dict on failure. Pagination parameters (page, per_page) are automatically clamped to valid ranges (page >= 1, per_page in 1--100, default 20).
Project ID resolution
When project_id is omitted from a tool call, the server falls back to the configured MAMBA_MCP_GITLAB_DEFAULT_PROJECT_ID. If neither is set, a VALIDATION_ERROR is returned.
Merge Requests (7 tools)¶
list_mrs¶
List merge requests for a GitLab project. Use this as the entry point for discovering available MRs.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path. Falls back to default. |
state |
str |
No | Filter: opened, closed, merged, all |
page |
int |
No | Page number (1-based). Default: 1. |
per_page |
int |
No | Items per page (1--100). Default: 20. |
Returns: ListMergeRequestsOutput -- paginated list of MergeRequestSummary items with id, iid, title, state, author, source_branch, target_branch, web_url, and timestamps.
get_mr¶
Get full details of a single merge request, including description, assignees, reviewers, labels, merge status, diff refs, and pipeline.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path |
mr_iid |
int |
Yes | Merge request IID (project-level identifier) |
Returns: MergeRequestDetail -- extends summary with description, assignees, reviewers, labels, merge_status, diff_refs, and pipeline.
get_mr_diffs¶
Get file-level diffs for a merge request. Each diff includes old/new paths and unified diff content. Useful for code review.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path |
mr_iid |
int |
Yes | Merge request IID |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: MergeRequestDiffsOutput -- paginated list of MergeRequestDiff items with old_path, new_path, diff, new_file, renamed_file, deleted_file.
get_mr_commits¶
Get the list of commits included in a merge request.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path |
mr_iid |
int |
Yes | Merge request IID |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: MergeRequestCommitsOutput -- paginated list of MergeRequestCommit items with id (full SHA), short_id, title, author_name, authored_date, message.
get_mr_pipelines¶
Get pipelines associated with a merge request. Use this to check whether CI is passing before merging.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path |
mr_iid |
int |
Yes | Merge request IID |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: ListPipelinesOutput -- paginated list of PipelineSummary items.
create_mr :material-pencil:{ title="Write tool" }¶
Write Tool
Blocked when MAMBA_MCP_GITLAB_READ_ONLY=true. Returns a READ_ONLY error.
Create a new merge request from a source branch to a target branch.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path |
title |
str |
Yes | Merge request title |
source_branch |
str |
Yes | Source branch name |
target_branch |
str |
Yes | Target branch name |
description |
str |
No | MR description (Markdown) |
assignee_ids |
list[int] |
No | User IDs to assign |
reviewer_ids |
list[int] |
No | User IDs to request review from |
labels |
list[str] |
No | Label names to apply |
Returns: MergeRequestDetail -- the full details of the created merge request.
update_mr :material-pencil:{ title="Write tool" }¶
Write Tool
Blocked when MAMBA_MCP_GITLAB_READ_ONLY=true. Returns a READ_ONLY error.
Update fields on an existing merge request. Only provided fields are modified.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
No | Project ID or URL-encoded path |
mr_iid |
int |
Yes | Merge request IID |
title |
str |
No | New title |
description |
str |
No | New description (Markdown) |
assignee_ids |
list[int] |
No | New assignee user IDs |
reviewer_ids |
list[int] |
No | New reviewer user IDs |
labels |
list[str] |
No | New label names |
state_event |
str |
No | State transition: close or reopen |
Returns: MergeRequestDetail -- the updated merge request.
Issues (6 tools)¶
list_issues¶
List issues for a project with optional filters for state, labels, and assignee.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
state |
str |
No | Filter: opened, closed, all |
labels |
str |
No | Comma-separated label names (e.g., "bug,urgent") |
assignee_id |
int |
No | Filter by a single assignee user ID |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: ListIssuesOutput -- paginated list of IssueSummary items with id, iid, title, state, author, assignees, labels, web_url, and timestamps.
get_issue¶
Get full details of a single issue by its project-level IID.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
issue_iid |
int |
Yes | Project-level issue IID |
Returns: IssueDetail -- extends summary with description, milestone, user_notes_count, upvotes, downvotes.
list_issue_comments¶
List comments (notes) on a specific issue, including both user and system-generated notes.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
issue_iid |
int |
Yes | Project-level issue IID |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: IssueCommentsOutput -- paginated list of IssueComment items with id, body, author, created_at, updated_at, system.
create_issue :material-pencil:{ title="Write tool" }¶
Write Tool
Blocked when MAMBA_MCP_GITLAB_READ_ONLY=true. Returns a READ_ONLY error.
Create a new issue in a GitLab project.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
title |
str |
Yes | Issue title |
description |
str |
No | Issue description (Markdown) |
assignee_ids |
list[int] |
No | User IDs to assign |
labels |
str |
No | Comma-separated label names |
milestone_id |
int |
No | Milestone ID to associate |
Returns: IssueDetail -- the full details of the created issue.
update_issue :material-pencil:{ title="Write tool" }¶
Write Tool
Blocked when MAMBA_MCP_GITLAB_READ_ONLY=true. Returns a READ_ONLY error.
Update fields on an existing issue. Only provided fields are modified. Labels replace the full set when provided.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
issue_iid |
int |
Yes | Issue IID to update |
title |
str |
No | New title |
description |
str |
No | New description (Markdown) |
assignee_ids |
list[int] |
No | New assignee user IDs |
labels |
str |
No | New comma-separated label names |
state_event |
str |
No | State transition: close or reopen |
Returns: IssueDetail -- the updated issue.
add_issue_comment :material-pencil:{ title="Write tool" }¶
Write Tool
Blocked when MAMBA_MCP_GITLAB_READ_ONLY=true. Returns a READ_ONLY error.
Add a comment (note) to an existing issue. Supports Markdown formatting.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
issue_iid |
int |
Yes | Issue IID to comment on |
body |
str |
Yes | Comment text (Markdown) |
Returns: IssueComment -- the newly created comment with id, body, author, created_at, system.
Pipelines (4 tools)¶
All pipeline tools are read-only and always available regardless of the READ_ONLY setting.
list_pipelines¶
List CI/CD pipelines for a project with optional status and ref filters.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
status |
str |
No | Filter: running, pending, success, failed, canceled, skipped |
ref |
str |
No | Filter by git ref (branch or tag name) |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: ListPipelinesOutput -- paginated list of PipelineSummary items with id, iid, status, ref, sha, web_url, and timestamps.
get_pipeline¶
Get full details of a single pipeline, including duration, coverage, and trigger user.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
pipeline_id |
int |
Yes | Pipeline ID |
Returns: PipelineDetail -- extends summary with duration, started_at, finished_at, coverage, user.
get_pipeline_jobs¶
Get the list of jobs within a specific pipeline.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
pipeline_id |
int |
Yes | Pipeline ID |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Returns: PipelineJobsOutput -- paginated list of PipelineJob items with id, name, stage, status, duration, runner, web_url.
get_job_log¶
Get the text log output of a specific CI/CD job. Large logs are automatically truncated.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id |
int \| str |
Yes | Project ID or URL-encoded path |
job_id |
int |
Yes | Job ID |
max_bytes |
int |
No | Max log size in bytes (1,024--1,048,576). Default: 102,400 (100KB). |
Returns: JobLogOutput -- content (log text), truncated (bool), byte_count (total bytes before truncation).
Search (1 tool)¶
search¶
Search across GitLab resources at the instance, project, or group level.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
str |
Yes | Search query string |
scope |
str |
Yes | Resource scope: issues, merge_requests, projects, milestones, etc. |
project_id |
int \| str |
No | Project ID for project-scoped search |
group_id |
int |
No | Group ID for group-scoped search |
page |
int |
No | Page number. Default: 1. |
per_page |
int |
No | Items per page. Default: 20. |
Scope resolution priority
- Explicit
project_idparameter - Explicit
group_idparameter - Configured
DEFAULT_PROJECT_ID - Configured
DEFAULT_GROUP_ID - Instance-wide search (no scope)
Returns: SearchOutput -- paginated list of SearchResult items. Each result has a type (the scope) and a data dict containing the resource-specific payload from the GitLab API.
Authentication¶
The server supports two authentication strategies. PAT is simpler; OAuth 2.0 is better suited for service accounts and automated environments.
Personal Access Token (PAT)¶
The server sends a PRIVATE-TOKEN header with each API request. On startup, it validates the token by calling GET /api/v4/personal_access_tokens/self and checks that the token is:
- Active (not expired)
- Not revoked
- Has at least one of the required scopes:
apiorread_api
- Navigate to GitLab > User Settings > Access Tokens
- Set a descriptive name (e.g.,
mamba-mcp) - Select the
apiscope (orread_apifor read-only usage) - Set an expiration date
- Copy the token value into your
mamba.env
OAuth 2.0 (Client Credentials)¶
The server obtains a Bearer token via POST /oauth/token with grant_type=client_credentials. The token is automatically refreshed 30 seconds before expiry. Validation is performed against GET /api/v4/user.
- Navigate to GitLab > Admin > Applications (instance-level) or User Settings > Applications
- Set the name and callback URL
- Enable
client_credentialsgrant type - Select the required scopes (
apiorread_api) - Save and copy the client ID and secret
Comparison¶
| Feature | PAT | OAuth 2.0 |
|---|---|---|
| Setup complexity | Simple -- single token | Moderate -- requires OAuth app setup |
| Auth header | PRIVATE-TOKEN: {token} |
Authorization: Bearer {token} |
| Token rotation | Manual | Automatic (30s pre-expiry refresh) |
| User binding | Tied to a specific user | Tied to an OAuth application |
| Scope control | Token-level scopes | OAuth application scopes |
| Best for | Development, personal use | CI/CD, service accounts, production |
Auth priority
If both TOKEN and OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET are configured, PAT takes precedence. Clear MAMBA_MCP_GITLAB_TOKEN to use OAuth instead.
Read-Only Mode¶
Setting MAMBA_MCP_GITLAB_READ_ONLY=true disables all write operations at runtime. The 5 affected tools are:
| Tool | Category |
|---|---|
create_mr |
Merge Requests |
update_mr |
Merge Requests |
create_issue |
Issues |
update_issue |
Issues |
add_issue_comment |
Issues |
When a write tool is called in read-only mode, the server returns a structured error:
{
"error": true,
"code": "READ_ONLY",
"message": "Tool 'create_mr' is disabled in read-only mode",
"tool_name": "create_mr",
"suggestion": "This server is running in read-only mode. Set MAMBA_MCP_GITLAB_READ_ONLY=false to enable write operations."
}
Read-only with read_api scope
When running in read-only mode, a token with only the read_api scope is sufficient. This reduces the blast radius of a compromised token.
All 13 read tools (list, get, and search operations) remain fully functional in read-only mode.
Rate Limiting¶
The server implements client-side rate limiting using a sliding window algorithm to prevent overwhelming the GitLab API.
How it works¶
- Each API request records a timestamp in a sliding window
- Before each request, timestamps older than
WINDOW_SECONDSare pruned - If the number of timestamps in the window reaches
MAX_REQUESTS, the request is rejected - The rejection includes a
retry_aftervalue indicating when the next request will be allowed
Configuration¶
# 100 requests per 60-second window (default)
MAMBA_MCP_GITLAB_RATE_LIMIT_MAX_REQUESTS=100
MAMBA_MCP_GITLAB_RATE_LIMIT_WINDOW_SECONDS=60
# Disable rate limiting
MAMBA_MCP_GITLAB_RATE_LIMIT_MAX_REQUESTS=0
Error response¶
When the rate limit is exceeded, the tool returns:
{
"error": true,
"code": "RATE_LIMITED",
"message": "Rate limit exceeded: 100 requests per 60s window. Try again in 12.3 seconds.",
"tool_name": "list_mrs",
"suggestion": "GitLab API rate limit reached. Wait before retrying."
}
Client-side only
This is a client-side limiter protecting the GitLab instance. GitLab's own server-side rate limits still apply. If the GitLab API returns HTTP 429, the server maps it to a RATE_LIMITED error with the same structured format.
Service Architecture¶
The server follows a layered architecture separating tool handlers from API communication.
graph TD
subgraph "MCP Tool Handlers"
MR["mr_tools.py<br/>(7 tools)"]
IS["issue_tools.py<br/>(6 tools)"]
PL["pipeline_tools.py<br/>(4 tools)"]
SE["search_tools.py<br/>(1 tool)"]
end
subgraph "Service Layer"
MRS["MergeRequestService"]
ISS["IssueService"]
PLS["PipelineService"]
SES["SearchService"]
end
BASE["GitLabService (base)"]
MR --> MRS
IS --> ISS
PL --> PLS
SE --> SES
MRS --> BASE
ISS --> BASE
PLS --> BASE
SES --> BASE
BASE -->|httpx| GL["GitLab API v4"]
subgraph "Shared Infrastructure"
RL["RateLimiter"]
AUTH["AuthStrategy"]
CFG["Settings"]
end
BASE --> RL
BASE --> CFG
GitLabService (base class)¶
All four service classes inherit from GitLabService, which provides:
- HTTP methods:
_get(),_post(),_put()for GitLab API calls - Pagination:
_get_paginated()extractsx-totalandx-total-pagesheaders - URL construction:
_build_project_url()handles both integer IDs and URL-encoded paths - Project ID resolution:
_resolve_project_id()falls back to the configured default - Error mapping: HTTP status codes are mapped to
ErrorCodeconstants (401 toAUTH_FAILED, 403 toFORBIDDEN, 404 toNOT_FOUND, 429 toRATE_LIMITED, 5xx toAPI_ERROR) - Rate limiting: Every request checks the rate limiter before execution
Tool handler pattern¶
Every tool handler follows the same 7-step skeleton:
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, ...))
async def tool_name(params, ctx=None):
# 1. Start timer
start_time = time.perf_counter()
# 2. Null-check context
if ctx is None:
return create_tool_error(...)
# 3. Extract AppContext from lifespan
app_ctx = ctx.request_context.lifespan_context
# 4. (Write tools only) Check read-only mode
# 5. Create service, delegate to service method
# 6. Convert result to Pydantic output model
# 7. Catch GitLabAPIError → structured error; catch Exception → CONNECTION_ERROR
Error codes¶
| Code | HTTP Status | Description |
|---|---|---|
AUTH_FAILED |
401 | Token is invalid or expired |
FORBIDDEN |
403 | Token lacks required scopes |
NOT_FOUND |
404 | Resource not found |
PROJECT_NOT_FOUND |
-- | Project does not exist |
MERGE_REQUEST_NOT_FOUND |
-- | MR IID not found in project |
ISSUE_NOT_FOUND |
-- | Issue IID not found in project |
PIPELINE_NOT_FOUND |
-- | Pipeline ID not found |
BRANCH_NOT_FOUND |
-- | Branch does not exist |
RATE_LIMITED |
429 | Rate limit exceeded (client or server) |
VALIDATION_ERROR |
400 | Invalid input parameters |
CONNECTION_ERROR |
-- | Cannot reach GitLab |
API_ERROR |
5xx | GitLab server error |
READ_ONLY |
-- | Write tool blocked in read-only mode |
Each error code has a default suggestion message. Errors involving resource names (projects, branches) include fuzzy matching via Levenshtein distance to suggest similar names.
CLI Reference¶
# Start server (default: stdio transport)
mamba-mcp-gitlab [--env-file PATH]
# Test connectivity and token validity
mamba-mcp-gitlab [--env-file PATH] test
The CLI auto-discovers mamba.env in the current directory or ~/mamba.env if --env-file is not specified.