Skip to content

Configuration

  • One required variable
  • Read-only aware
  • Safe-mode previews
  • Tier detected from the licence or plan

GitLab MCP Server needs almost nothing to start. In stdio mode, the default used by IDE integrations, you set just two things: which GitLab instance to talk to (GITLAB_URL) and a personal access token to talk with (GITLAB_TOKEN). On GitLab.com even the URL is optional, because GITLAB_URL defaults to https://gitlab.com, so a single GITLAB_TOKEN is enough; point GITLAB_URL at your own host for self-managed instances. In HTTP mode the server holds no credentials at all: each client sends its own token with every request, and, when the deployment publishes several GitLab instances, the one it selects. Everything else on this page is optional and ships with a safe default.

Stdio mode reads its configuration from environment variables, falling back to ~/.gitlab-mcp-server.env and to any file named in GITLAB_MCP_ENV_FILE; HTTP mode reads CLI flags. This page covers the options most users need; the environment variable reference lists every variable and the CLI reference every flag.

Settings this project defines are read as GITLAB_MCP_<NAME> from 2.8.0.

A stdio MCP server runs in whatever shell its client was started from, next to every other tool that person uses. Names as generic as LOG_LEVEL, AUTH_MODE or RATE_LIMIT_RPS may already be owned by something else there, and the collision is silent: the server reads a value nobody gave it and behaves in a way nobody configured.

The spellings used before 2.8.0 were removed in 3.1.0 and are read by nothing: the bare generic names (TOOL_SURFACE, LOG_LEVEL and the rest), the switches that already began with GITLAB_ (GITLAB_TIER, GITLAB_READ_ONLY, GITLAB_SAFE_MODE, GITLAB_IGNORE_SCOPES, GITLAB_SKIP_TLS_VERIFY) and YOLO_MODE. One left set, in the environment or in a dotenv file the server loads, is named at startup with the variable to rename it to.

They were to have gone in 3.0.0 and were held back one release, because 2.7.5 carries a self-updater and 3.0.0 does not: a 2.7.5 deployment updates itself into 3.0.0 with nobody reading a release note, so removing them there would have broken those deployments in silence. 3.1.0 is the first version nobody is carried into. GITLAB_ENTERPRISE, retired earlier in favour of the tier, was removed in 3.0.0 and is ignored.

Three of them refuse the start instead: GITLAB_READ_ONLY, GITLAB_SAFE_MODE and EXCLUDE_TOOLS. They withhold part of what a deployment serves, so a version that ignored one in silence would serve writes, or the actions the operator removed. The refusal reads the name and never its value. EXCLUDE_TOOLS is generic enough that another tool in the same shell could own it: rename it to GITLAB_MCP_EXCLUDE_TOOLS, or unset it in this server’s environment when it is somebody else’s.

Some names stay bare on purpose:

NamesWhy they were not renamed
GITLAB_URL, GITLAB_TOKENGitLab’s own convention. Every existing configuration sets them, and they are the two most likely to be written into a client configuration from memory
OTEL_*Owned by the OpenTelemetry specification. The exporters read those names themselves and would never see a prefixed spelling
AUTOPILOTA convention other agent tooling sets, honored as an alias of GITLAB_MCP_YOLO_MODE and never warned about. The setting itself is ours and carries the prefix; its old spelling YOLO_MODE was removed in 3.1.0
MODELEVAL_*The model evaluation’s own variables, set by make targets in this repository. They configure a test harness that cmd/server never links, so they never appear beside another tool’s variables in a user’s shell

The tables below always give the name to set, so read the name rather than deriving it.


GitLab MCP Server requires exactly one variable to start in stdio mode — the rest are optional and default to safe values:

VariableDescriptionExample
GITLAB_TOKENPersonal access token: a classic one with the api or read_api scope, or a fine-grained one whose grant decides what it is served. Which token says what each kind is servedglpat-xxxxxxxxxxxxxxxxxxxx
TokenWhat the server serves it
Classic, api scopeEvery action the tier allows. The five administration groups also need admin_mode (Scope-based tool filtering)
Classic, read_api scopeThe read actions only: the server reads the token’s scopes at startup (per pooled credential in HTTP mode) and builds a read-only surface
Fine-grained (scopes read granular)What its grant reaches, judged per action against the permissions GitLab 19.4.1 declares, when the token grants Personal Access Token: Read and Metadata: Read and the instance runs the recorded 19.4 release (GitLab.com’s prerelease of the next minor is listed by the grant); otherwise everything but the actions no fine-grained token can reach. See Fine-grained Tokens

A token whose scopes cannot be read is served as if it could write, and GitLab’s own 403 answers a call it cannot make. A token GitLab accepts that carries neither read_api nor api reaches no tool, so it is refused (Token scopes).

VariableDefaultDescription
GITLAB_URLhttps://gitlab.comGitLab instance base URL, with an http:// or https:// scheme. Set this for self-managed instances
GITLAB_MCP_SKIP_TLS_VERIFYfalseSkip TLS certificate verification for self-signed certs
GITLAB_MCP_TOOL_SURFACEdynamicCanonical tool catalog selector: dynamic, meta, or individual
GITLAB_MCP_CAPABILITY_SURFACEfullResource and prompt catalog selector: full keeps the complete catalog; minimal keeps the gitlab://tools manifest, and disables optional resources, prompts, workflow guides, and resource subscriptions. See Capability surface
GITLAB_MCP_META_PARAM_SCHEMAopaqueMeta-tool input-schema strategy: opaque (default), compact (8.7x the opaque schema), or full (18.3x). Applies to meta-tool tools/list schemas only; see Meta parameter schema
GITLAB_MCP_TIER(auto-detect)GitLab edition selector: free/ce, premium, or ultimate. When set, used verbatim with no license check. When unset, detected in two steps: GET /license, the instance plan, which only an administrator can read on self-managed and nobody on GitLab.com; then the plans of the namespaces the token administers (GET /namespaces), which are the real subscription on GitLab.com and always default on self-managed. That listing is read 100 at a time, stopping at the first ultimate and after 10 pages either way, so an account administering more than 1,000 namespaces whose only paid one sits past that bound resolves lower than it holds. Where neither step answers, the tier is free, and an enterprise instance logs a warning naming this setting. The tier decides which Premium and Ultimate actions are served, and removes the input schema fields above it; output schemas are pruned leniently, so data a response carries still reaches the client
GITLAB_MCP_READ_ONLYfalseRemove every mutating action (create, update, delete). The filter is per action, so reads keep working on every surface, and a meta-tool or gitlab_execute_action that serves both keeps serving its reads
GITLAB_MCP_SAFE_MODEfalseIntercept mutating actions and answer with a preview card instead of executing them: a Markdown card naming the action, with the arguments it would have sent in a JSON fence. Reads keep working; GITLAB_MCP_READ_ONLY takes precedence
GITLAB_MCP_EMBEDDED_RESOURCEStrueEmbed the canonical gitlab:// MCP resource URI in the get results that carry one (twenty-two get actions, from projects and groups to snippets and wiki pages); set false for clients that do not tolerate duplicate content blocks
GITLAB_MCP_EXCLUDE_TOOLS(empty)Comma-separated tool names, group names or action IDs (e.g., gitlab_project_delete,gitlab_admin), excluded from the tool surface and from the resources, subscriptions, prompts and argument completions that return the same objects, so the removal holds on every request path. The same spellings reach the standalone utilities on every surface (gitlab_interactive, interactive.issue_create, discover_project.resolve), and an entry that names nothing is logged as a warning rather than refused. The warning is written when a catalog is first built: at startup on stdio, and in HTTP mode the first time each configuration shape’s catalog is built. The shape includes the tier, detected per credential unless pinned, and the credential’s scope narrowing, so the warning can be written more than once, and an entry reported as naming nothing for one caller’s tier may still remove actions for another’s. It also names the dynamic surface’s own gitlab_find_action and gitlab_execute_action, although that surface removes them by name
GITLAB_MCP_IGNORE_SCOPESfalseSkip the scope filter and the read-only narrowing and register all tools regardless of token scopes. The token’s scopes are still read, so one carrying neither read_api nor api is still refused, and a fine-grained token’s grant still decides what it is shown
GITLAB_MCP_LOG_LEVELinfoLog verbosity: debug, info, warn, error. The --log-level flag sets the same variable and wins over it
GITLAB_MCP_PPROF_ADDR(empty)Serve Go’s profiling handlers (net/http/pprof) on this loopback address (127.0.0.1:6060), on a listener of their own that starts before the transport; a host that is not loopback is refused at startup, since a heap profile is a copy of the process’s memory. Empty serves nothing. The --pprof-addr flag sets the same variable
GITLAB_MCP_ALLOW_PRIVATE_INSTANCESfalsetrue permits a private, loopback or CGNAT address as a destination the operator did not choose: an instance a caller named in GITLAB-URL under --allow-any-gitlab-url, or a redirect hop that left the configured instance. An address GITLAB_URL or --gitlab-url named is never checked, and cloud metadata addresses stay refused whatever this says. The --allow-private-instances flag sets the same variable. See Outbound connections
ModeVariableTools exposedBest for
Dynamic toolset (default)GITLAB_MCP_TOOL_SURFACE=dynamicgitlab_find_action, gitlab_execute_actionMost users: the lowest startup context, with every catalog action still reachable
Meta-toolsGITLAB_MCP_TOOL_SURFACE=meta34 on Free/CE, 40 on self-managed Premium, 51 on self-managed Ultimate, 52 on GitLab.com UltimateClients that prefer consolidated domain dispatchers with an action parameter
Individual toolsGITLAB_MCP_TOOL_SURFACE=individual868 on Free/CE, 1022 on self-managed Premium, 1088 on self-managed Ultimate, 1094 on GitLab.com Ultimate with OrbitClients that need granular tool selection

Keep the default dynamic surface for normal low-token deployments, and set GITLAB_MCP_TOOL_SURFACE=meta only when a client or workflow prefers domain meta-tools. Dynamic toolset explains the find/execute workflow and Meta-tools the domain-action mapping.

GITLAB_MCP_META_PARAM_SCHEMA (--meta-param-schema in HTTP mode) decides only what the inputSchema of each meta-tool dispatcher shows in tools/list. Handlers validate and run the same parameters under all three modes, and neither what gitlab_find_action returns nor what the gitlab://tools manifest holds changes with it.

ModeWhat a meta-tool’s inputSchema showsSize of the served meta-tool input schemas
opaque (default)The compact envelope {action, params}: the action names, and params as an open objectThe baseline
compactA discriminated oneOf with one branch per action, its property names and types, descriptions and $defs stripped8.7x the opaque total
fullA discriminated oneOf with each action’s complete schema, descriptions included18.3x the opaque total

The sizes are serialized bytes of the meta-tool input schemas, summed, as the repository’s token audit measures them. They are not a startup-token measurement: resources and prompts are counted in none of them. On the dynamic surface the setting changes nothing, since the two dynamic tools keep their own schemas and gitlab_find_action returns an action’s schema inline; on the individual surface it is ignored, since each tool already carries its own typed schema. Keep opaque and read gitlab://tools/{id} for the exact parameters of one action.

GITLAB_MCP_CAPABILITY_SURFACE (--capability-surface in HTTP mode) decides which MCP resources and prompts are registered:

  • full (the default) registers every resource, the workflow guides, the prompts and the gitlab://tools manifest, and advertises resource subscriptions.
  • minimal keeps only the gitlab://tools manifest and its gitlab://tools/{id} template. Its handshake declares no prompts capability, so prompts/list and prompts/get answer JSON-RPC -32601 (method not found) rather than an empty page, and resources/subscribe is not advertised.

logging/setLevel answers -32601 on both, because the server logs to stderr and never declares the logging capability.

Minimal removes no action schema: gitlab_find_action returns exact schemas inline before gitlab_execute_action is called, and gitlab://tools/{id} serves the manifest entry of one action on every surface. What it saves is the shared startup context, measured at 9,498 tokens for the resources and prompts of full against 170 for minimal (cl100k_base tokenizer; the dynamic toolset page has the per-surface totals). There are only these two modes: an intermediate one, such as schemas or resources without prompts, would add another configuration axis without beating the low-token workflows that already exist.

VariableDefaultDescription
GITLAB_MCP_CLIENT_COMPATautoPer-client response compatibility: Codex sessions get fractional annotation priorities rounded to 0/1; off disables. Both transports; the --client-compat flag sets the same variable. Choosing a response from clientInfo is a deliberate deviation from the MCP specification
GITLAB_MCP_DESCRIPTION_SUBSTITUTIONSemptyRewrite listed descriptions and titles for strict MCP gateway validators: comma-separated old=new pairs applied in order (backslash escapes \, \= \\); malformed values refuse startup. Both transports; the --description-substitutions flag sets the same variable
GITLAB_MCP_UPLOAD_MAX_FILE_SIZE2GBSize cap for upload and file tools, including raw file reads (streamed and stopped at the limit); supports KB/MB/GB suffixes, 1 TB ceiling. Both transports; the --upload-max-file-size flag sets the same variable
GITLAB_MCP_YOLO_MODEfalseSkip destructive action confirmations on every surface (not recommended): no prompt on the meta and individual surfaces, and no confirm: true needed on the default dynamic one; see Destructive actions. A non-empty value wins over AUTOPILOT; the --yolo-mode flag sets the same variable
AUTOPILOTfalseSame as GITLAB_MCP_YOLO_MODE, and read only while that is unset or empty: skip destructive confirmations on every surface. No flag of its own
GITLAB_MCP_ALLOWED_IMPORT_DIRS(empty)Additional OS path-list-separated directories allowed for local project/group import archives
GITLAB_MCP_ALLOWED_UPLOAD_DIRS(empty)Additional OS path-list-separated directories a tool may read a local file from (every file_path and directory_path input). The working directory (unless it is the filesystem root or the user’s home directory, which are dropped as implicit roots) and the OS temp directory are always allowed, and a path is resolved through symlinks first
GITLAB_MCP_ALLOWED_DOWNLOAD_DIRSemptyAdditional OS path-list-separated directories a tool may write a downloaded file into (output_path). Same syntax and same always-allowed roots. The file appears at output_path only once it is complete, so a failed or cancelled download leaves it as it was
GITLAB_MCP_ENV_FILE(empty)One dotenv file to load besides ~/.gitlab-mcp-server.env. Read from the process environment only, so a file the server loads cannot nominate another. Give an absolute path; a relative one follows the client into every workspace it opens, and the server warns when it sees one
GITLAB_MCP_STDIO_MAX_LINE_BYTES4 MiBLongest stdio message accepted, in bytes. A longer line is refused and answered, not buffered. It matches the SDK’s own HTTP body default, so both transports refuse the same messages; raise it only for a client that inlines large base64 payloads
GITLAB_MCP_MAX_LISTEN_STREAMS64Concurrent subscriptions/listen streams one credential may hold open; 0 removes that ceiling. A further 512 per process is not configurable, since the per-credential figure multiplies by however many tokens a caller holds. Applies to both transports
GITLAB_MCP_ACTION_TIMEOUT65mCancel an action still running after this long; 0 disables it (upper bound 24h). Above the longest wait any action offers. It ends a file upload or download still running at the limit too. Both transports; HTTP mode also has --action-timeout
GITLAB_MCP_DRAIN_DELAY0HTTP mode: after SIGTERM, keep the listener open and answer /health with 503 draining for this long before closing it, so a balancer that polls /health removes the instance before the close (upper bound 5m); 0 closes at once. Also --drain-delay
GITLAB_MCP_AUTH_MODElegacyHTTP mode authentication: legacy (a token sent per request in PRIVATE-TOKEN or Authorization: Bearer) or oauth (RFC 9728 Bearer token verification). Also --auth-mode
GITLAB_MCP_OAUTH_CACHE_TTL15mHTTP mode, OAuth: how long a verified token identity is reused, from 1m to 2h. Also --oauth-cache-ttl
GITLAB_MCP_OAUTH_CLIENT_UID(empty)HTTP mode, OAuth: comma-separated GitLab OAuth application uids whose tokens are admitted. Empty admits any credential the instance accepts; setting it also refuses personal access tokens, which belong to no application. Also --oauth-client-uid
GITLAB_MCP_RATE_LIMIT_RPS0Per-credential rate limit, in req/s, on every call that reaches GitLab: tools/call, resources/read, resources/subscribe, subscriptions/listen, prompts/get (0 disables it), plus completion/complete on a bucket of its own ten times looser in rate and burst, and tools/list on a bucket of its own refilled a tenth as fast and holding the same burst, charged because it spends the processor every tenant shares rather than because it reaches GitLab, and first on one bucket the whole process shares, 3000 tools a second and not configurable. Both transports: the default is 0 in stdio and 10 in HTTP mode, where --rate-limit-rps overrides it. On stdio this variable is the only switch, since stdio ignores the flag and says so at startup (why stdio is off by default). At most 1000
GITLAB_MCP_RATE_LIMIT_BURST40Token-bucket burst size when GITLAB_MCP_RATE_LIMIT_RPS > 0, at most 10000

Write it to ~/.gitlab-mcp-server.env, or to any path you then name in GITLAB_MCP_ENV_FILE. A .env in the working directory is not loaded.

~/.gitlab-mcp-server.env
# Required
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
# Optional
GITLAB_MCP_SKIP_TLS_VERIFY=false
GITLAB_MCP_TOOL_SURFACE=dynamic
GITLAB_MCP_READ_ONLY=false
GITLAB_MCP_SAFE_MODE=false
GITLAB_MCP_EXCLUDE_TOOLS=
GITLAB_MCP_IGNORE_SCOPES=false
GITLAB_MCP_EMBEDDED_RESOURCES=true
GITLAB_MCP_UPLOAD_MAX_FILE_SIZE=2GB
GITLAB_MCP_LOG_LEVEL=info

For self-managed GitLab, add GITLAB_URL=https://gitlab.example.com. Leave GITLAB_MCP_TIER out unless you mean to pin it: set, it is used with no license check, so free on a licensed instance hides its Premium and Ultimate actions.

Create .vscode/mcp.json in your workspace:

{
"servers": {
"gitlab": {
"type": "stdio",
"command": "/path/to/gitlab-mcp-server",
"env": {
"GITLAB_TOKEN": "glpat-xxxxxxxxxxxxxxxxxxxx"
}
}
}
}

Secure token configuration using VS Code input variables:

{
"inputs": [
{
"id": "gitlab-token",
"type": "promptString",
"description": "GitLab Personal Access Token",
"password": true
}
],
"servers": {
"gitlab": {
"type": "stdio",
"command": "/path/to/gitlab-mcp-server",
"env": {
"GITLAB_TOKEN": "${input:gitlab-token}"
}
}
}
}

Loading the variables from a file with envFile:

{
"servers": {
"gitlab": {
"type": "stdio",
"command": "/path/to/gitlab-mcp-server",
"envFile": "${userHome}/.config/gitlab-mcp-server.env"
}
}
}

VS Code loads the variables in that file into the server’s environment, so the JSON carries no secret. Name a file outside the workspace, since one inside it travels with the repository. The server reads ~/.gitlab-mcp-server.env on its own, so envFile is only needed for a file kept somewhere else.

The examples above that write the token into the client’s JSON are the shortest to read, not the safest to keep: a project’s .vscode/mcp.json or .cursor/mcp.json is easily committed. Client configuration has the entry for every other client.

  • The home env file. The server reads ~/.gitlab-mcp-server.env by itself, so an entry can leave GITLAB_TOKEN out entirely, as the Claude Code and JetBrains tabs do. A variable the entry sets, even to an empty value, wins over the file. Recommended: Environment file shows how to write it without the token reaching shell history.
  • The client’s own secret handling. VS Code prompts for the token with an input variable, or loads a file with envFile (the VS Code tab above).
  • A variable the client substitutes. Cursor expands ${env:GITLAB_TOKEN} and OpenCode {env:GITLAB_TOKEN} from the environment they were started in (Cursor, OpenCode), so the token is exported once and the file names only the variable.

To export the variable on Linux or macOS, add it to your shell profile; a client started from that shell inherits it:

~/.bashrc or ~/.zshrc
export GITLAB_TOKEN="glpat-xxxxxxxxxxxxxxxxxxxx"

On Windows, set it for your user in PowerShell 7.1 or later, which prompts for the value without echoing it, then restart the client:

Terminal window
[Environment]::SetEnvironmentVariable('GITLAB_TOKEN', (Read-Host 'GitLab token' -MaskInput), 'User')

Not every client passes its environment on. GitHub documents that Copilot CLI hands a local server PATH and the variables in that server’s env block and nothing else, so an exported GITLAB_TOKEN does not reach the server there: keep it in the env block of ~/.copilot/mcp-config.json, which lives in your home directory rather than in a project.

A stdio server reads none of the flags in the first table below but --http itself, since its configuration comes from the environment. Given one, it ignores it and says so once at startup, naming each flag with the variable to set instead, at WARN (or at INFO when --transport=auto chose stdio and stdio has no such setting at all, like the listener). Where ignoring the flag would cost more than a setting, a stdio server refuses to start and names the variable to set instead, even when that variable already asks for the same thing: --read-only, --safe-mode or --exclude-tools asking to withhold something it would then serve, and a --gitlab-url naming no instance it connects to, since it would send GITLAB_TOKEN to the one GITLAB_URL names, https://gitlab.com when that is unset.

In HTTP mode (--http), settings resolve in three layers, highest first: a CLI flag passed explicitly, then the matching environment variable, then the built-in default. A variable exported without its flag is therefore honored, and one whose value cannot be parsed fails startup rather than being dropped in silence. A few flags, the listener ones below among them, have no environment counterpart:

FlagDefaultDescription
--httpfalseEnable HTTP transport mode
--http-addr:8080Listen address. host:port binds TCP; a value containing a path separator (e.g. /run/gitlab-mcp.sock) binds a unix socket instead. No environment counterpart
--http-socket-mode0660Octal permission mode for a unix socket named by --http-addr: owner and group may connect, nobody else. No environment counterpart
--tls-cert / --tls-key(empty)PEM certificate and key. Serves HTTPS on the listener itself (TLS 1.2 floor, 1.3 negotiated). Both or neither. A pair that does not load stops startup; afterwards a changed pair is read again at the next handshake, so rotating the certificate needs no restart. No environment counterpart
--gitlab-url(required)GitLab instance URL. Required unless --allow-any-gitlab-url is passed; GITLAB_URL supplies it when the flag is not given. Repeat it (or comma-separate) to publish several instances, among which the GITLAB-URL header is then required
--allow-any-gitlab-urlfalseStart with no instance published and let GITLAB-URL name any host. Single-user local deployments only: it is refused unless --http-addr binds a loopback address or a unix socket, and warns at startup even there. No environment counterpart
--skip-tls-verifyfalseSkip TLS verification
--tool-surface(empty)Canonical tool catalog selector: dynamic, meta, or individual; empty serves dynamic
--meta-param-schemaopaqueMeta-tool params schema mode: opaque, compact, or full; applies to meta-tool schemas only
--capability-surfacefullResource and prompt catalog selector: full or minimal; minimal keeps the gitlab://tools manifest, and omits optional resources, workflow guides, and prompts
--tier(auto-detect)Force GitLab edition: free/ce, premium, or ultimate; omit to auto-detect CE/EE per token+URL entry. Replaces GITLAB_ENTERPRISE, which was removed in 3.0.0 and is ignored; there is no --enterprise flag
--read-onlyfalseRead-only mode: removes every mutating action, per action, while reads keep working on every surface
--safe-modefalseIntercept mutating actions and answer with a preview card instead of executing them: a Markdown card naming the action, with the arguments in a JSON fence
--embedded-resourcestrueEmbed the canonical MCP resource URI in the get results that carry one (twenty-two get actions)
--exclude-tools(empty)Comma-separated tool names, group names or canonical action IDs, excluded from the tool surface and from the resources, subscriptions, prompts and argument completions that return the same objects. The same spellings reach the standalone utilities on every surface, and an entry that names nothing is logged as a warning, as GITLAB_MCP_EXCLUDE_TOOLS describes
--ignore-scopesfalseSkip the scope filter and the read-only narrowing. A token carrying neither read_api nor api is still refused, and a fine-grained token’s grant still decides what it is shown
--max-http-clients100Maximum unique (token, GitLab URL) server entries kept in the pool, from 1 to 10000; bounds pooled entries, not the sessions or requests they hold, which the process bounds by its descriptor limit (192 held calls and 96 stateful sessions at once under a hard limit of 1024), not configurable
--session-timeout30mIdle MCP session timeout; applies to --stateless=false only; under the default stateless transport each POST’s session ends with its response. A session a client never deletes holds one of the process’s session slots until it expires, and with 0 until the pool evicts its credential
--http-idle-timeout0 (disabled)HTTP server idle connection timeout. Default 0 disables idle closure, so --session-timeout is the effective lifetime; set a positive duration to recycle idle connections sooner
--statelesstrueSessionless streamable HTTP (protocol 2026-07-28): no Mcp-Session-Id tracking, every POST is self-contained, GET and DELETE answer 405. --stateless=false restores legacy stateful sessions. No environment counterpart
--json-responsefalseReturn application/json response bodies instead of text/event-stream (SSE). No environment counterpart
--max-request-body-bytes0Maximum streamable HTTP request body size in bytes; 0 uses the SDK default (4 MiB). Oversized bodies are rejected with 413. No environment counterpart
--auth-modelegacyAuthentication mode: legacy or oauth
--oauth-cache-ttl15mOAuth token identity cache TTL (1m–2h)
--oauth-client-uid(empty)Comma-separated GitLab OAuth application uids whose tokens are admitted. Empty admits any credential the instance accepts; setting it also refuses personal access tokens, which belong to no application
--public-url(empty)Externally reachable origin (https://host[:port][/path], http only for a loopback host, no trailing slash). Required with --auth-mode=oauth; its origin is also trusted for cross-origin browser requests. Env: GITLAB_MCP_PUBLIC_URL
--resource-documentation(empty)https URL published as RFC 9728 resource_documentation; point it at a page describing your own OAuth application (its client ID and registered redirect URIs). Empty publishes this project’s OAuth application page
--resource-policy-uri(empty)https URL published as RFC 9728 resource_policy_uri; your own page on what this deployment does with the data reached through it. Empty omits the field
--resource-tos-uri(empty)https URL published as RFC 9728 resource_tos_uri; your own terms of service. Empty omits the field
--trusted-origins(empty)Comma-separated origins allowed to make cross-origin browser requests; * accepts any origin. Empty adds none, though a configured --public-url origin is trusted regardless. Env: GITLAB_MCP_TRUSTED_ORIGINS
--action-timeout65mCancel an action still running after this long; 0 disables it (upper bound: 24h). Falls back to GITLAB_MCP_ACTION_TIMEOUT
--drain-delay0After SIGTERM, keep the listener open and answer /health with 503 draining for this long before closing it (upper bound: 5m); 0 closes at once. Falls back to GITLAB_MCP_DRAIN_DELAY
--pool-idle-timeout1hReclaim a pooled per-token-and-URL credential entry after this long unused; 0 keeps entries until the pool size bound evicts them (upper bound: 24h). An entry with a live subscription is never idle by this measure: its watcher polls GitLab directly and its listen is one request the client never repeats, so nothing would refresh it
--revalidate-interval15mToken re-validation interval (upper bound: 24h). 0 stops the periodic check, but an entry whose credential is older than 1h is still rebuilt, which re-runs the probe
--rate-limit-rps10Per-credential rate limit, in req/s, on every call that reaches GitLab: tools/call, resources/read, resources/subscribe, subscriptions/listen, prompts/get (0 disables it), plus completion/complete on a bucket of its own ten times looser in rate and burst, and tools/list on a bucket of its own refilled a tenth as fast and holding the same burst, charged for spending the shared processor rather than for reaching GitLab, and first on one bucket the whole process shares, 3000 tools a second and not configurable; on by default because an HTTP deployment is shared. At most 1000
--rate-limit-burst40Token-bucket burst size when --rate-limit-rps > 0, at most 10000
--trusted-proxies(empty)Addresses or CIDR ranges of the reverse proxies whose --trusted-proxy-header is believed (e.g. 127.0.0.1,10.0.0.0/8); from any other peer the header is ignored. Required with --trusted-proxy-header
--trusted-proxy-header(empty)HTTP header with the real client IP (e.g. CF-Connecting-IP, X-Forwarded-For), so the authentication-failure budgets charge callers rather than the proxy; believed only from --trusted-proxies

The four authentication budget flags (--auth-failure-limit, --auth-failure-window, --auth-distinct-token-limit, --auth-distinct-token-window) are explained under Authentication budgets; each limit takes at most 100000 and each window at most 24h.

General flags (both stdio and HTTP modes):

FlagDefaultDescription
--transport(empty)stdio, http or auto. Empty defers to --http; auto serves HTTP only when stdin is the null device (a container started without -i) and stdio for the pipe an MCP client connects
--env-file(empty)Dotenv file to load besides ~/.gitlab-mcp-server.env; the same setting as GITLAB_MCP_ENV_FILE, and wins over it
--log-level(empty)debug, info (the effective default), warn or error. Sets GITLAB_MCP_LOG_LEVEL
--client-compat(empty)auto (the effective default) or off. Sets GITLAB_MCP_CLIENT_COMPAT
--upload-max-file-size(empty)Size cap for upload and file-read tools, KB/MB/GB suffixes accepted (2GB when unset). Sets GITLAB_MCP_UPLOAD_MAX_FILE_SIZE
--yolo-mode(empty)true skips the confirmation of destructive actions on every surface, the default dynamic one included. Sets GITLAB_MCP_YOLO_MODE
--description-substitutions(empty)Comma-separated old=new pairs applied to every listed description and title. Sets GITLAB_MCP_DESCRIPTION_SUBSTITUTIONS; a malformed value refuses startup
--allow-private-instances(empty)true permits a destination the operator did not choose (a GITLAB-URL header under --allow-any-gitlab-url, or a redirect hop that left the instance) to be a private, loopback or CGNAT address; cloud metadata addresses stay refused. Sets GITLAB_MCP_ALLOW_PRIVATE_INSTANCES
--pprof-addr(empty)Serve Go’s profiling handlers on this loopback address (127.0.0.1:6060), on a listener of their own; any other host is refused at startup. Sets GITLAB_MCP_PPROF_ADDR
--tool-search(empty)Search the action catalog by name, alias, tag or description, then exit; prints each canonical action ID beside the tool the configured surface names it. Reads --tool-surface and --tier, and otherwise GITLAB_MCP_TOOL_SURFACE and GITLAB_MCP_TIER
--versionfalsePrint the version and commit, then exit
--shutdownfalseTerminate all running instances and exit
--probefalseAsk the running instance’s /health and exit 0 when it answers; the image’s HEALTHCHECK. Reads the listener off the instance’s own flags, or probes the URL, unix:<path> or host:port given after the flag

The seven flags that set a variable (--log-level through --pprof-addr) exist so one command line can configure the whole server; an explicitly passed flag beats an exported variable. GITLAB_TOKEN deliberately has no flag, since a token on a command line is visible to every user on the machine through ps and lands in shell history. The four telemetry flags (--telemetry, --telemetry-identity, --telemetry-identity-rotation, --telemetry-tool-name) are read by both transports too, and Telemetry explains them. -h or --help prints the server’s own help, and the CLI reference lists every flag with its type.

Example:

Terminal window
# Single GitLab.com instance (fixed URL for all clients; replace for self-managed GitLab)
./gitlab-mcp-server \
--http \
--http-addr=0.0.0.0:8080 \
--gitlab-url=https://gitlab.com \
--max-http-clients=200 \
--session-timeout=1h
# Several instances (the GITLAB-URL header is required, and must name one of them)
./gitlab-mcp-server \
--http \
--http-addr=0.0.0.0:8080 \
--gitlab-url=https://gitlab.com,https://gitlab.example.com

The server loads configuration in the following order (later sources override earlier ones):

  1. ~/.gitlab-mcp-server.env: user-level defaults (home directory)
  2. The file GITLAB_MCP_ENV_FILE names: one extra dotenv file, if the environment names one
  3. System environment variables: what the MCP client passed, or the shell exported
  4. CLI flags (highest priority): in HTTP mode the whole HTTP table; on stdio only the general and telemetry flags, the ones that set a variable among them, since a stdio server names every HTTP flag it was given and ignores it, or refuses to start (see HTTP mode flags)

A dotenv file never overwrites a variable that is already set, which is what makes an earlier source lose to a later one in this list.

For GitLab instances with self-signed TLS certificates:

Terminal window
GITLAB_MCP_SKIP_TLS_VERIFY=true

Enable GITLAB_MCP_READ_ONLY=true to restrict the server to read-only operations. Every action that creates, updates or deletes is removed, per action, and reads keep working on every surface. This is useful for:

  • Audit and compliance environments
  • Shared servers where users should only query data
  • Tokens with read_api scope
What it exposes

every read operation, on every surface: list, get and search keep working exactly as before.

Requires
GITLAB_MCP_READ_ONLY=true in stdio mode, --read-only in HTTP mode.
What it does not do

anything that writes: create, update and delete actions are removed from the catalog per action, so on the dynamic and meta surfaces they stop being reachable rather than merely erroring. When safe mode is also set, read-only wins — mutations disappear instead of previewing.

Enable GITLAB_MCP_SAFE_MODE=true to intercept mutating actions and answer with a preview of what would be executed, without performing the operation. The preview is a Markdown card: a ⛔ Safe mode blocked heading naming the action, Status and Mode rows reading blocked and safe, a Tool row, the arguments it would have sent in a JSON fence, and the hint Set GITLAB_MCP_SAFE_MODE=false to execute this operation. On the dynamic and meta surfaces the card names the canonical action and the structured result carries the same fields; on the individual surface it names the tool and comes back as an error result, since the tool produced none of the output its schema describes. Reads work normally. This is useful for:

  • Reviewing operations before execution (dry-run)
  • Training environments where you want to see tool behavior
  • Debugging tool parameters without side effects
What it exposes

every operation’s shape: mutating actions stay callable and answer with a preview card naming the canonical action (for example issue.create), with the parameters it would send in a JSON fence; reads execute normally.

Requires
GITLAB_MCP_SAFE_MODE=true in stdio mode, --safe-mode in HTTP mode.
What it does not do

actual writes: nothing mutating reaches GitLab. The preview is not a validation either — GitLab-side errors (permissions, conflicts) only surface on a real execution.

Both safe mode and read-only act per action, not per tool. On the dynamic and meta surfaces a single tool serves many actions — gitlab_execute_action routes everything, and gitlab_issue covers list and create alike — so the policy is applied to the underlying action catalog. Reads keep working on every surface, and safe-mode previews name the canonical action (for example issue.create) rather than the dispatcher tool.

These are always on and take no configuration:

FeatureWhat it does
Content annotationsEvery Markdown block of a tool result is annotated for the assistant audience, with a priority of 0.7 unless the action declares a list, detail or mutation kind (see Content annotations). The Markdown is for the model to reason over and the JSON structuredContent for programs, so a client that shows both does not display the same answer twice. An image block goes to the user audience instead
Clickable linksList results link each GitLab object they name (merge requests, issues, pipelines and the rest) as [text](url), and lead their hints with one asking the model to keep the links
Next-step hintsA result that suggests a follow-up ends with a 💡 Next steps block, and the structured output carries the same hints as a next_steps array where the action’s output type declares it
Formatted datesMarkdown shows a timestamp in UTC as 15 Jan 2025 10:30 UTC, and a date alone as 15 Jan 2025; the structured output keeps RFC 3339 in UTC, so a client can parse it

The output format reference describes a result in full.

Frequently asked questions

What is the minimum configuration?

In stdio mode, a single GITLAB_TOKEN with the api scope is enough to start — every other variable is optional and defaults to a safe value. GITLAB_URL defaults to https://gitlab.com, so you set it only when connecting to a self-managed instance. For a read-only setup, use a read_api token: the server serves it a read-only surface on its own, and GITLAB_MCP_READ_ONLY=true does the same for an api token. In HTTP mode, no token is configured on the server at all; each client sends its own token with every request.

stdio vs HTTP — which mode do I use?

Use stdio mode (the default) for local, single-user setups such as IDE integrations like VS Code, Cursor, and Claude Desktop; it is configured through environment variables, plus ~/.gitlab-mcp-server.env or a file named in GITLAB_MCP_ENV_FILE for what the environment does not carry. Use HTTP mode (--http) for shared or remote deployments such as Docker or Kubernetes, where configuration uses CLI flags and each client authenticates with its own GitLab token per request.

How do I point at a self-managed GitLab instance?

Set GITLAB_URL to your instance base URL, for example GITLAB_URL=https://gitlab.example.com (in HTTP mode, use the --gitlab-url flag instead). If the instance uses a self-signed or internal CA certificate, also set GITLAB_MCP_SKIP_TLS_VERIFY=true (or --skip-tls-verify in HTTP mode), but only on a trusted network — it disables certificate verification for all connections to GitLab.

How do I pick a tool surface (dynamic, meta, or individual)?

Set the GITLAB_MCP_TOOL_SURFACE variable (or the --tool-surface flag in HTTP mode). dynamic is the default and lowest-token catalog: it exposes gitlab_find_action and gitlab_execute_action while keeping every GitLab action reachable through the canonical action catalog. Choose meta when a client prefers consolidated domain dispatchers with an action parameter, and individual to register one MCP tool per GitLab operation.