Environment variables
This page lists every variable the server reads: the 45 GITLAB_MCP_* settings, GITLAB_URL, GITLAB_TOKEN, AUTOPILOT, the OTEL_* variables of the OpenTelemetry specification, and the few the Go runtime reads on its own. Configuration explains the options most deployments set and how to put them in a client; the command-line reference covers every flag.
Reading the tables
Section titled “Reading the tables”- Default is what an unset or empty variable means. An empty value counts as unset everywhere.
- Transport says which mode reads the variable. HTTP; stdio validates means a stdio server parses it and refuses to start on an invalid value, although it never uses it.
- Flag is the command-line spelling. A flag is read in HTTP mode only unless the cell says both: a stdio server takes everything else from the environment, names an HTTP-only flag it was given at startup, and ignores it (four of them can refuse the start instead; see the command-line reference).
- An invalid value refuses the start unless the row says otherwise. The rows that warn and keep a default do so on purpose, and say why.
Value syntax
Section titled “Value syntax”| Kind | What is accepted |
|---|---|
| Boolean | Go’s boolean spellings: true, false, 1, 0, t, f, in lower, upper or title case. Three exceptions: GITLAB_MCP_YOLO_MODE and AUTOPILOT read 1, true and yes (any case) as true and anything else as false; GITLAB_MCP_TELEMETRY and OTEL_SDK_DISABLED read only true and false (any case), and warn about anything else and treat it as false; GITLAB_MCP_ALLOW_PRIVATE_INSTANCES treats anything unparseable as false without a word, since it switches a guard off |
| Duration | Go duration syntax: 90s, 15m, 1h30m. 0 is accepted only where the row says it disables something, and there only as the bare digit: 0s refuses the start, although the flag of the same name takes it (#1171). GITLAB_MCP_TELEMETRY_IDENTITY_ROTATION takes either. The OTEL_* timeouts are integer milliseconds instead (a trap worth knowing) |
| Size | A byte count, or a number with a KB, MB or GB suffix in any case. The suffixes are powers of 1024 |
| List | Comma-separated, with the blanks around each entry trimmed. A list of directories uses the operating system’s path-list separator instead: : on Linux and macOS, ; on Windows |
Connection and credentials
Section titled “Connection and credentials”| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_URL | https://gitlab.com on stdio; none in HTTP mode | An http:// or https:// URL with a host. In HTTP mode, a comma-separated list | Both | --gitlab-url | On stdio, the instance the token is sent to. In HTTP mode, the instance or instances the deployment publishes: required unless --allow-any-gitlab-url is passed, and with several the GITLAB-URL request header is required and must name one of them. With GITLAB_MCP_AUTH_MODE=oauth every instance must be https, http being accepted only for a loopback host. A stdio server started by hand in a terminal with this or GITLAB_TOKEN unset once the dotenv files are read shows a first-run screen and waits for Enter instead of serving |
GITLAB_TOKEN | None: required on stdio | A GitLab personal access token (glpat-...); see Which token | stdio | None, on purpose | The credential every call is made with. It has no flag because a token on a command line is visible to every user of the machine through ps, is captured by process accounting and lands in shell history. HTTP mode never reads it: each request carries its own token |
GITLAB_MCP_SKIP_TLS_VERIFY | false | Boolean | Both | --skip-tls-verify | Skips certificate verification for the GitLab instance. HTTP mode refuses it with GITLAB_MCP_AUTH_MODE=oauth for any instance that is not loopback, since bearer tokens would go to whichever host answers. A private CA in the system trust store, or SSL_CERT_FILE (see Read by the Go runtime), needs no flag |
GITLAB_MCP_ALLOW_PRIVATE_INSTANCES | false | Boolean; anything unparseable is false | Both | --allow-private-instances (both) | Lets a destination the operator did not choose resolve to a loopback, private, CGNAT 100.64.0.0/10, link-local, unique-local or unspecified address: an instance a caller named in GITLAB-URL under --allow-any-gitlab-url, or a redirect hop that left the configured instance’s host. An address GITLAB_URL or --gitlab-url named is never checked, and the cloud metadata addresses stay refused whatever this says (Outbound connections) |
Which token
Section titled “Which token”| Token | What the server serves it |
|---|---|
Classic, api scope | Every action the tier allows |
Classic, read_api scope | The read actions only: the server reads the token’s scopes at startup and builds a read-only surface for it |
Fine-grained (its scope list reads granular) | What its grant reaches, decided per action against the permissions GitLab declares, when the token grants Personal Access Token: Read and Metadata: Read and the instance runs the release the permission table was recorded from; otherwise everything but the actions no fine-grained token can reach (Fine-grained tokens) |
Neither read_api nor api | Nothing. A stdio server still answers the MCP handshake and refuses every catalog method (tools/list, tools/call, the resource and prompt methods, completions and subscriptions/listen) with -40300 and a message saying to create a token with read_api, or api to write as well, and restart with it. HTTP mode refuses such a token with 403 |
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. GITLAB_MCP_IGNORE_SCOPES skips the read-only narrowing and the scope filter, never the read_api minimum.
Tool surface and behavior
Section titled “Tool surface and behavior”| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_TOOL_SURFACE | dynamic | dynamic, meta, individual | Both | --tool-surface | Which tool catalog is registered: two find and execute tools (dynamic), one dispatcher per domain (meta), or one tool per action |
GITLAB_MCP_CAPABILITY_SURFACE | full | full, minimal | Both | --capability-surface | minimal keeps the gitlab://tools manifest and drops the other resources, the prompts, the workflow guides and resource subscriptions |
GITLAB_MCP_META_PARAM_SCHEMA | opaque | opaque, compact, full, in any case | Both | --meta-param-schema | How the meta tools describe their params in tools/list: an opaque envelope, or one schema per action with property names only (compact) or in full. Measured at about 8.7 and 18.3 times the opaque schema. It changes the meta surface’s listing only: no handler, no find result and no gitlab://tools entry |
GITLAB_MCP_TIER | Detected | free (or ce), premium, ultimate, in any case | Both | --tier | Set, the tier is used as written with no license check. Unset, it is detected from GET /license (readable only by an administrator on self-managed, and by nobody on GitLab.com), then from the plans of the namespaces the token administers (GET /namespaces, the real plan on GitLab.com and always default on self-managed), read 100 at a time for at most 10 pages and stopping at the first ultimate. When neither answers the tier is free, and an enterprise instance says so at WARN naming this variable. HTTP mode detects it per credential unless it is pinned |
GITLAB_MCP_READ_ONLY | false | Boolean | Both | --read-only | Removes every mutating action, per action rather than per tool, so reads keep working on every surface: a meta tool keeps its read actions, and the dynamic surface’s gitlab_execute_action stays, annotated read-only, for the reads it can route (Read-only mode) |
GITLAB_MCP_SAFE_MODE | false | Boolean | Both | --safe-mode | Answers a mutating action with a preview card naming the action and echoing its arguments instead of running it; reads still run. Read-only mode wins when both are set (Safe mode) |
GITLAB_MCP_EMBEDDED_RESOURCES | true | Boolean | Both | --embedded-resources | Adds the canonical gitlab:// resource as an embedded resource block to the get results that have one (Output format). Set false for a client that does not tolerate the duplicate content block |
GITLAB_MCP_EXCLUDE_TOOLS | Empty | List of tool names, group names or canonical action IDs, such as gitlab_admin,gitlab_runner,project.delete | Both | --exclude-tools | Removes what it names on every surface, and from the resources, subscriptions, prompts and completions that return the same objects. The standalone utilities answer to the same three spellings: gitlab_interactive removes the four guided flows, interactive.issue_create (or its meta and individual tool, gitlab_interactive_issue_create) one of them, and discover_project.resolve (or gitlab_discover_project) project discovery. An entry that names nothing is logged at WARN rather than refused, because one configuration is often reused across tiers |
GITLAB_MCP_IGNORE_SCOPES | false | Boolean | Both | --ignore-scopes | Registers every tool the tier allows whatever the token’s scopes. The scopes are still read, so a token below read_api is still refused, and a fine-grained token’s grant still decides what it is shown |
GITLAB_MCP_CLIENT_COMPAT | auto | off (any case) disables; any other value means auto | Both | --client-compat (both) | On auto, a session identifying itself as OpenAI Codex has the fractional priority in its annotations rounded to 0 or 1, since the builds bundled with ChatGPT.app reject a fraction (Per-client compatibility profiles). A deliberate deviation from the specification |
GITLAB_MCP_DESCRIPTION_SUBSTITUTIONS | Empty | old=new pairs, comma-separated, applied in order; a backslash escapes \, \= \\. At most 32 pairs, 256 bytes per half | Both | --description-substitutions (both) | Rewrites listed descriptions and titles of tools, prompts, resources and resource templates for strict MCP gateway validators, never names, URIs, enum values or tool results. An empty old or an unknown escape refuses the start. A rewrite that would grow a string past twice its length, or 512 bytes more when that is larger, leaves that string as written. An active list is announced at WARN once |
GITLAB_MCP_YOLO_MODE | false | 1, true, yes (any case) are true; anything else is false | Both | --yolo-mode (both) | On every surface, lets a destructive action, such as a delete, run without confirmation: no question to a client that supports elicitation, no refusal for one that cannot prompt, and no confirm: true needed on the default dynamic surface (Destructive actions). When set at all it decides, so --yolo-mode=false overrides an inherited AUTOPILOT=true |
AUTOPILOT | Unset | As GITLAB_MCP_YOLO_MODE | Both | None | An alias other agent tooling sets, consulted only when GITLAB_MCP_YOLO_MODE is empty. It stays bare on purpose and is never warned about |
GITLAB_MCP_UPLOAD_MAX_FILE_SIZE | 2GB | Size, positive, at most 1 TB | Both | --upload-max-file-size (both) | The largest file an upload or file-read tool accepts. A stdio server refuses an invalid or oversized value at startup; HTTP mode warns and uses the default, or the 1 TB maximum |
GITLAB_MCP_ACTION_TIMEOUT | 65m | Duration; 0 disables; at most 24h | Both | --action-timeout | Cancels an action still running after this long. The default sits above the longest wait any action offers (a pipeline wait caps itself at an hour), and it ends a file upload or download still running at the limit too, so raise it for files too large to move in that time |
Local files
Section titled “Local files”These three widen where a tool may touch the machine the server runs on. The working directory, unless it is a filesystem root or exactly your home directory, and the operating system’s temporary directory are always allowed. A path is resolved through every symlink before it is checked, and a listed directory that does not exist is skipped with a WARN. They apply to a stdio server only: HTTP mode refuses every local path.
| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_ALLOWED_UPLOAD_DIRS | Empty | List of directories | stdio | None | More directories a tool may read a local file from: every file_path and directory_path input |
GITLAB_MCP_ALLOWED_DOWNLOAD_DIRS | Empty | List of directories | stdio | None | More directories a tool may write a download into (output_path). The destination is checked again once its parent directories exist, and the file is written beside it under a temporary name and renamed over it only when complete, so a failed or cancelled download leaves output_path as it was |
GITLAB_MCP_ALLOWED_IMPORT_DIRS | Empty | List of directories | stdio | None | More directories a project or group import archive may be read from. The archive must be a regular .tar.gz file, and outside Windows one that is not group- or world-writable |
Transport limits and diagnostics
Section titled “Transport limits and diagnostics”| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_STDIO_MAX_LINE_BYTES | 4194304 (4 MiB) | A positive byte count, no maximum | stdio | None | The longest stdio message assembled. A longer line is refused and answered rather than buffered, so a client cannot grow the process by sending one. The default matches the SDK’s own HTTP body limit, so both transports refuse the same messages; raise it only for a client that inlines large base64 payloads. An unparseable or non-positive value warns and keeps the default, since a mistyped number should not take the client down with the server |
GITLAB_MCP_MAX_LISTEN_STREAMS | 64 | A non-negative integer; 0 removes the ceiling | Both | None | How many subscriptions/listen streams one credential may hold open. A listen is a request the client leaves open, measured at about one file descriptor and 55 to 100 KB of memory a stream, and a real client opens a handful. 0 removes that ceiling and nothing else: a credential holding a stream is still counted as busy and kept in the pool. A second ceiling of 512 per process is not configurable, because the first multiplies by however many tokens a caller holds. An invalid value warns and keeps 64 |
GITLAB_MCP_LOG_LEVEL | info | debug, info, warn (or warning), error, in any case; anything else means info | Both | --log-level (both) | Verbosity of the JSON log the server writes to stderr |
GITLAB_MCP_PPROF_ADDR | Empty | host:port where the host is localhost or a loopback address (127.0.0.1:6060, [::1]:6060) | Both | --pprof-addr (both) | Serves Go’s profiling handlers under /debug/pprof/ on a listener of their own, which starts before the transport and stops with the process. Any other host, or none, refuses the start: a heap profile is a copy of the process’s memory and the handlers take no credential. Empty serves nothing |
GITLAB_MCP_ENV_FILE | Empty | A path to a dotenv file, absolute by preference | Both | --env-file (both) | One dotenv file to load besides the home file. See Dotenv files |
HTTP pool and sessions
Section titled “HTTP pool and sessions”| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_MAX_HTTP_CLIENTS | 100 | A positive integer, at most 10000 | HTTP; stdio validates | --max-http-clients | How many (token, GitLab URL) entries the pool keeps. It bounds entries, not the calls or sessions they hold: the process bounds those by its descriptor limit (192 held calls and 96 stateful sessions under a hard limit of 1024), and nothing configures that (Sizing) |
GITLAB_MCP_SESSION_TIMEOUT | 30m | A positive duration, at most 24h. The variable refuses 0, which the flag accepts | HTTP; stdio validates | --session-timeout | Idle timeout of a stateful MCP session, so --stateless=false only: under the default stateless transport each POST’s session ends with its response. A session no client deletes holds one of the process’s session slots until it expires |
GITLAB_MCP_POOL_IDLE_TIMEOUT | 1h | Duration; 0 disables; at most 24h | HTTP; stdio validates | --pool-idle-timeout | Reclaims a credential’s pool entry after this long unused. An entry with a live subscription is never idle by this measure |
GITLAB_MCP_SESSION_REVALIDATE_INTERVAL | 15m | Duration; 0 stops the periodic check; at most 24h | HTTP; stdio validates | --revalidate-interval | How often each pooled credential is checked again. With 0, an entry whose credential is older than an hour is still rebuilt, which checks it again. Note the flag’s shorter name |
GITLAB_MCP_DRAIN_DELAY | 0 | Duration, at most 5m | HTTP; stdio validates | --drain-delay | After SIGTERM, keeps the listener open and answers /health with 503 draining for this long before closing it, so a balancer that polls /health takes the instance out first. Set it to at least one probe interval; 0 closes at once |
HTTP authentication
Section titled “HTTP authentication”| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_AUTH_MODE | legacy | legacy, oauth, in lower case | HTTP; stdio validates | --auth-mode | legacy takes a token per request in PRIVATE-TOKEN or Authorization: Bearer. oauth takes only a bearer token, verifies it against GitLab and publishes RFC 9728 protected-resource metadata; it requires GITLAB_MCP_PUBLIC_URL and an https instance (HTTP server) |
GITLAB_MCP_PUBLIC_URL | Empty | An absolute https://host[:port][/path] URL with no trailing slash and no fragment; http only for localhost, 127.0.0.1 or ::1 | HTTP; stdio validates it under oauth | --public-url | The externally reachable address of the deployment. Required with oauth, where it is the RFC 9728 resource identifier; in legacy mode it is optional, and its origin is trusted for cross-origin browser requests |
GITLAB_MCP_TRUSTED_ORIGINS | Empty | List of absolute origins (scheme://host[:port]), or * | HTTP | --trusted-origins | Origins allowed to make cross-origin browser requests. Empty refuses every cross-origin browser POST except from the GITLAB_MCP_PUBLIC_URL origin, which is trusted regardless; * accepts any origin and turns the protection off (Cross-origin protection) |
GITLAB_MCP_OAUTH_CACHE_TTL | 15m | Duration from 1m to 2h | HTTP; stdio validates | --oauth-cache-ttl | How long a verified OAuth identity is reused. The cache holds at most 10000 identities whatever the lifetime, dropping an expired one or else the least recently used, and at most 16 verifications run at once; neither figure is configurable (The server’s identity cache) |
GITLAB_MCP_OAUTH_CLIENT_UID | Empty | List of GitLab OAuth application uids | HTTP | --oauth-client-uid | Admits only tokens minted for these applications. Empty admits any credential the instance accepts; setting it refuses personal access tokens, which belong to no application. GitLab publishes no audience binding, so this is the recipient check available (ADR-0019, Admitting only your own application) |
Rate limits and authentication budgets
Section titled “Rate limits and authentication budgets”| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_RATE_LIMIT_RPS | 0 on stdio, 10 in HTTP mode | A non-negative number, at most 1000; 0 disables | Both | --rate-limit-rps | Per-credential limit, in requests a second, on every call that reaches GitLab (tools/call, resources/read, resources/subscribe, subscriptions/listen, prompts/get), plus completion/complete on a bucket of its own with ten times the rate and the burst, and tools/list on a bucket of its own refilled a tenth as fast. A listing is also charged, in the tools it carries, to one bucket the whole process shares (3000 a second, 48000 in hand, not configurable). 0 turns all of them off. On stdio this variable is the only switch (Rate limiting tool invocations) |
GITLAB_MCP_RATE_LIMIT_BURST | 40 | An integer from 0 to 10000 | Both | --rate-limit-burst | The bucket size while the limit is on. With the limiter on, a burst below 1 refuses the start in both transports |
GITLAB_MCP_AUTH_FAILURE_LIMIT | 10 | An integer from 0 to 100000; 0 disables | HTTP; stdio validates | --auth-failure-limit | Failed authentications one address may produce inside the failure window before it is blocked. 0 turns this budget off rather than blocking on the first failure (Authentication budgets) |
GITLAB_MCP_AUTH_FAILURE_WINDOW | 1m | Duration; 0 disables; at most 24h | HTTP; stdio validates | --auth-failure-window | The window the failure budget counts in, and the step the distinct-credential escalation is built from: one window, then ten, then sixty. 0 turns the distinct-credential budget off with it |
GITLAB_MCP_AUTH_DISTINCT_TOKEN_LIMIT | 50 | An integer from 0 to 100000; 0 disables | HTTP; stdio validates | --auth-distinct-token-limit | Distinct credentials one address may have refused inside the distinct window before it is blocked, for longer each time. A person has one token and a fleet behind a NAT has one each, so this count is what only an attack produces |
GITLAB_MCP_AUTH_DISTINCT_TOKEN_WINDOW | 10m | Duration; 0 disables; at most 24h | HTTP; stdio validates | --auth-distinct-token-window | The window the distinct-credential budget counts in |
Telemetry
Section titled “Telemetry”Telemetry is off by default and goes only to a collector you configure; the OpenTelemetry page covers what it records and how to send it.
| Variable | Default | Accepts | Transport | Flag | What it does |
|---|---|---|---|---|---|
GITLAB_MCP_TELEMETRY | false | true or false, in any case; anything else warns and counts as false | Both | --telemetry (both) | Exports traces, metrics and logs over OTLP. OTEL_SDK_DISABLED=true vetoes it |
GITLAB_MCP_TELEMETRY_IDENTITY | none | none, pseudonymous, full, in any case | Both | --telemetry-identity (both) | What telemetry records about who made a call: nobody, a keyed digest that correlates one caller’s calls without naming them, or the GitLab user id and username. Identity never reaches a metric. An unrecognized value is logged at ERROR, and nothing about callers is recorded (Recording who made a call) |
GITLAB_MCP_TELEMETRY_IDENTITY_KEY | Empty | Any secret | Both | None, on purpose | The secret the pseudonymous policy derives its keys from (HKDF-SHA256). Empty generates one per process, so a digest means something within one process only; set it when replicas must agree or a count has to survive a restart, and keep it away from wherever the telemetry lands. It has no flag because process arguments are readable through /proc |
GITLAB_MCP_TELEMETRY_IDENTITY_ROTATION | Empty | Duration; empty or 0 keeps the key for the life of the process; at most 30 days (720h) | Both | --telemetry-identity-rotation (both) | How long a generated key lives. Ignored, with a WARN, when GITLAB_MCP_TELEMETRY_IDENTITY_KEY is set. An unparseable or out-of-range value is logged at ERROR, and nothing about callers is recorded |
GITLAB_MCP_TELEMETRY_TOOL_NAME | auto | auto, on, off, in any case; anything else is logged at ERROR and read as auto | Both | --telemetry-tool-name (both) | Whether gen_ai.tool.name is a metric dimension. auto keeps it on the dynamic and meta surfaces and drops it on individual, where about a thousand tools would exhaust the SDK’s cardinality limit and fold the long tail into one overflow series |
The OTEL_* variables
Section titled “The OTEL_* variables”The standard variables keep their names, and most of them are read by the OpenTelemetry exporters and SDK themselves, never by this server’s code. A few this server reads too, for the reason in the last column.
| Variable | Read by | What this server does with it |
|---|---|---|
OTEL_SDK_DISABLED | This server only | true (any case) vetoes telemetry even when GITLAB_MCP_TELEMETRY or --telemetry asks for it. Nothing in the Go SDK implements the variable, so the server does |
OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, ..._METRICS_PROTOCOL, ..._LOGS_PROTOCOL | This server | Chooses each signal’s exporter, the signal’s own variable first: http/protobuf (the default, also spelled http) or grpc. http/json and anything else stop telemetry at startup with an ERROR line, and the server goes on serving without it |
OTEL_EXPORTER_OTLP_ENDPOINT and its per-signal forms | The exporters; this server reads it too | Named in the startup line and the server card, with any user and password removed, and checked against the headers for the plaintext warning |
OTEL_EXPORTER_OTLP_HEADERS and its per-signal forms | The exporters; this server reads it too | A collector credential: the server warns once when one would cross the network in the clear to another host, and removes it from its own log lines (Authenticating to your collector) |
OTEL_EXPORTER_OTLP_INSECURE and its per-signal forms | The exporters; this server reads it too | Read only to decide which signals that warning names, since the Go trace and metric exporters let it override an https endpoint |
OTEL_EXPORTER_OTLP_CERTIFICATE, ..._CLIENT_CERTIFICATE, ..._CLIENT_KEY and their per-signal forms | The exporters; this server checks them first | A CA file that cannot be read or holds no certificate, or a client certificate without its key under the same prefix, stops telemetry at startup with an ERROR naming the variable, rather than leaving the exporters to fall back in silence |
OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES | The SDK; this server checks them | When neither names the service, the server calls itself gitlab-mcp-server; when either does, that name stands. Every other resource attribute passes through untouched |
OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT | The SDK; this server checks whether it is set | Unset, the server bounds every span attribute value at 4096 characters rather than leaving it unlimited |
OTEL_TRACES_SAMPLER | The SDK; this server checks whether it is set | Unset, the HTTP span ignores a caller’s request not to be sampled, so an anonymous caller cannot switch off the record of its own refusal. Set, the sampler you chose decides |
Every other OTEL_* variable (timeouts, compression, batching, export intervals) | The exporters and the SDK only | Nothing. Their durations are integer milliseconds |
Read by the Go runtime
Section titled “Read by the Go runtime”Two more groups of variables are read by Go’s standard library rather than by anything this project wrote:
- The proxy variables
HTTPS_PROXY,HTTP_PROXYandNO_PROXY(and their lower-case spellings), read once per process for the connections to GitLab. Behind a proxy the connection is to the proxy, and the destination behind it is judged as Outbound connections describes; hosts that should be reached directly belong inNO_PROXY. SSL_CERT_FILEon Linux and the other Unix systems except macOS, which points Go at a bundle of root certificates. It replaces the default roots for the whole process, so give it a bundle rather than a single certificate. It is the container answer to a private CA, and needs noGITLAB_MCP_SKIP_TLS_VERIFY.
Retired names
Section titled “Retired names”Settings this project defines gained the GITLAB_MCP_ prefix in 2.8.0, because a stdio server runs in whatever shell its client started in, next to every other tool there, and a name as generic as LOG_LEVEL or AUTH_MODE may already belong to one of them. The old spellings answered beside the new ones until 3.1.0, which removed them. They were held back one release on purpose: 2.7.5 carries a self-updater and 3.0.0 does not, so a 2.7.5 deployment updated itself into 3.0.0 with nobody reading a release note, and 3.1.0 is the first version nobody is carried into.
Nothing reads an old spelling now. The server knows the old spelling of 32 settings, and at startup, after the dotenv files are loaded, it looks for each: the bare suffix (LOG_LEVEL for GITLAB_MCP_LOG_LEVEL), or for five of them the GITLAB_-prefixed name they had (GITLAB_TIER, GITLAB_READ_ONLY, GITLAB_SAFE_MODE, GITLAB_IGNORE_SCOPES, GITLAB_SKIP_TLS_VERIFY). YOLO_MODE is the old spelling of GITLAB_MCP_YOLO_MODE. The other 26 bare names are ACTION_TIMEOUT, AUTH_DISTINCT_TOKEN_LIMIT, AUTH_DISTINCT_TOKEN_WINDOW, AUTH_FAILURE_LIMIT, AUTH_FAILURE_WINDOW, AUTH_MODE, CAPABILITY_SURFACE, CLIENT_COMPAT, DRAIN_DELAY, EMBEDDED_RESOURCES, EXCLUDE_TOOLS, LOG_LEVEL, MAX_HTTP_CLIENTS, META_PARAM_SCHEMA, OAUTH_CACHE_TTL, OAUTH_CLIENT_UID, POOL_IDLE_TIMEOUT, PPROF_ADDR, PUBLIC_URL, RATE_LIMIT_BURST, RATE_LIMIT_RPS, SESSION_REVALIDATE_INTERVAL, SESSION_TIMEOUT, TOOL_SURFACE, TRUSTED_ORIGINS and UPLOAD_MAX_FILE_SIZE. Each one found is reported in both transports:
GITLAB_TIER is no longer read (removed in 3.1.0): rename it to GITLAB_MCP_TIER| Old name | What happens now |
|---|---|
GITLAB_READ_ONLY | Refuses the start: the line above at ERROR, followed by “and this deployment will not be started under a capability it did not ask for”, and exit status 1 |
GITLAB_SAFE_MODE | Refuses the start, the same way |
EXCLUDE_TOOLS | Refuses the start, the same way. It is the one bare name of the three and may belong to another tool in the same shell; the server cannot tell, so rename it to GITLAB_MCP_EXCLUDE_TOOLS, or unset it in this server’s environment |
| The other 29 | A WARN line, and the server starts with the setting at its default |
The three that refuse are how an operator withholds part of what a deployment serves, so a version that ignored one in silence would serve writes, or the actions the operator removed, on a deployment that asked for neither. The check reads the name and never its value, so GITLAB_READ_ONLY=false refuses too, and a name set in a dotenv file the server loads counts as much as one exported in the shell. Because only names are read, a bare LOG_LEVEL that belongs to another tool in the same shell is reported as well; it only warns.
Two names were removed earlier, in 3.0.0, and are ignored without a word: META_TOOLS (with its GITLAB_MCP_META_TOOLS spelling and the --meta-tools flag, which is now an unknown flag), replaced by GITLAB_MCP_TOOL_SURFACE; and GITLAB_ENTERPRISE, replaced by GITLAB_MCP_TIER. A deployment that still sets GITLAB_ENTERPRISE has its tier detected, as one that sets nothing does.
Some names stay bare on purpose: GITLAB_URL and GITLAB_TOKEN, GitLab’s own convention, which every existing configuration sets; AUTOPILOT, a convention of other agent tooling; and OTEL_*, which belong to the OpenTelemetry specification and are read by the exporters themselves, so a prefixed spelling would never be seen. The MODELEVAL_* variables and GITLAB_MCP_TEST_INVENTORY_DIR and GITLAB_MCP_TEST_SNAPSHOT_PARITY configure this repository’s own test and evaluation harnesses; the server never reads them.
Dotenv files
Section titled “Dotenv files”Besides its environment, the server reads two dotenv files, in both transports, before anything reads configuration:
~/.gitlab-mcp-server.envin your home directory, when it exists.- The one file
GITLAB_MCP_ENV_FILEnames, or--env-file, which sets the same thing and wins over the variable.
Precedence, highest first:
- A flag passed explicitly. In both transports that is
--env-file, the seven flags that write their variable (--log-level,--client-compat,--upload-max-file-size,--yolo-mode,--description-substitutions,--allow-private-instances,--pprof-addr) and the four telemetry flags; in HTTP mode it is every flag. - The process environment, which is what the MCP client passed or the shell exported.
- The file
GITLAB_MCP_ENV_FILEnames. - The home file.
A file never overwrites a variable that is already set. That includes a variable exported empty: it hides the file’s value, and the server then reads it as unset.
GITLAB_MCP_ENV_FILE is resolved once, from the process environment, before any file is loaded, so a file the server loads cannot nominate another. Give it an absolute path. A relative one is resolved against the working directory, which the client changes with every workspace it opens, so one relative line in a user-level client configuration names a different file in every repository; the server warns when it sees one. Loading the named file is announced at WARN with its path, and a file that cannot be read is a WARN too, not a refusal.
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxxGITLAB_URL=https://gitlab.example.comGITLAB_MCP_TOOL_SURFACE=dynamicGITLAB_MCP_UPLOAD_MAX_FILE_SIZE=500MBGITLAB_MCP_LOG_LEVEL=infoNever commit a file that holds a token, and restrict it to its owner (chmod 600 on Linux and macOS).
The working directory is not a configuration source
Section titled “The working directory is not a configuration source”A .env in the working directory is not loaded. A stdio server inherits its working directory from the client, and every client that opens a workspace sets it to that workspace, so the file arrives with a cloned repository or an unpacked archive, written by whoever wrote them. When that file used to be loaded first, two lines in a repository could send your token to another host, switch off certificate verification so nothing complained, turn telemetry on toward a collector of their choosing and rewrite the tool descriptions the model reads, all before any tool call: the startup check delivered the token. The variables that make this work are exactly the ones no client sets, so they were always free for the first file found.
A dotenv file now configures the server only when someone put it where the server looks or named it. Being in the working directory is not a decision anyone made, which is the conclusion Git’s safe.directory, direnv’s direnv allow and VS Code Workspace Trust reached too.
The file is still looked for. One that exists and is not empty is named at WARN with its absolute path, the number of keys it wanted to set and up to ten of their names, never their values, so a file that stopped taking effect shows up in the log rather than in an afternoon of debugging. To keep using it, name it by absolute path in GITLAB_MCP_ENV_FILE: the file it names is loaded on purpose, even when it is that same .env.
HTTP mode equivalents
Section titled “HTTP mode equivalents”In HTTP mode, configuration resolves in three layers, highest first:
- A flag passed explicitly on the command line. Passing a flag whose value happens to equal the default still counts as choosing it:
--rate-limit-rps=10beatsGITLAB_MCP_RATE_LIMIT_RPS=2. - The variable with the same meaning, when its flag was not passed.
- The built-in default.
A variable is honored only when it is set and not empty, and it goes through the same parser and bounds as on stdio, so an invalid value stops the start with loading environment configuration: ... rather than falling back in silence. The rows above that warn instead say so.
A stdio server reads the variables, and of the flags only --http, --transport, --env-file, the seven that write a variable and the four telemetry flags. Nothing in this table is reachable from a request: a client controls only its own token and, where the deployment allows it, the GITLAB-URL header, and a request header named after any other setting is ignored and logged (HTTP server).
| Variable | Flag | Notes |
|---|---|---|
GITLAB_MCP_ENV_FILE | --env-file | Both transports; the flag wins |
| None | --http, --transport | The transport is chosen on the command line only |
GITLAB_URL | --gitlab-url | A comma-separated value spells the list a repeated flag does |
| None | --allow-any-gitlab-url | No variable on purpose: it lets a caller choose the host its token is sent to, so it belongs in the command line that started the process |
GITLAB_TOKEN | None | stdio only. In HTTP mode each request brings its own token |
GITLAB_MCP_SKIP_TLS_VERIFY | --skip-tls-verify | |
GITLAB_MCP_ALLOW_PRIVATE_INSTANCES | --allow-private-instances | Both transports; the flag writes the variable |
GITLAB_MCP_TOOL_SURFACE | --tool-surface | |
GITLAB_MCP_CAPABILITY_SURFACE | --capability-surface | |
GITLAB_MCP_META_PARAM_SCHEMA | --meta-param-schema | |
GITLAB_MCP_TIER | --tier | Neither set, the tier is detected per credential |
GITLAB_MCP_READ_ONLY | --read-only | |
GITLAB_MCP_SAFE_MODE | --safe-mode | |
GITLAB_MCP_EMBEDDED_RESOURCES | --embedded-resources | |
GITLAB_MCP_EXCLUDE_TOOLS | --exclude-tools | |
GITLAB_MCP_IGNORE_SCOPES | --ignore-scopes | |
GITLAB_MCP_CLIENT_COMPAT | --client-compat | Both transports; the flag writes the variable |
GITLAB_MCP_DESCRIPTION_SUBSTITUTIONS | --description-substitutions | Both transports; the flag writes the variable |
GITLAB_MCP_YOLO_MODE | --yolo-mode | Both transports; the flag writes the variable |
AUTOPILOT | None | Both transports; loses to a non-empty GITLAB_MCP_YOLO_MODE |
GITLAB_MCP_UPLOAD_MAX_FILE_SIZE | --upload-max-file-size | Both transports; the flag writes the variable |
GITLAB_MCP_ACTION_TIMEOUT | --action-timeout | Both transports; stdio reads the variable only |
GITLAB_MCP_ALLOWED_UPLOAD_DIRS, GITLAB_MCP_ALLOWED_DOWNLOAD_DIRS, GITLAB_MCP_ALLOWED_IMPORT_DIRS | None | stdio only: HTTP mode refuses every local path whatever they say |
GITLAB_MCP_STDIO_MAX_LINE_BYTES | None | stdio only. The HTTP counterpart is --max-request-body-bytes, which has no variable |
GITLAB_MCP_MAX_LISTEN_STREAMS | None | Both transports; environment only |
GITLAB_MCP_LOG_LEVEL | --log-level | Both transports; the flag writes the variable |
GITLAB_MCP_PPROF_ADDR | --pprof-addr | Both transports; the flag writes the variable |
GITLAB_MCP_MAX_HTTP_CLIENTS | --max-http-clients | |
GITLAB_MCP_SESSION_TIMEOUT | --session-timeout | Only the flag accepts 0 |
GITLAB_MCP_POOL_IDLE_TIMEOUT | --pool-idle-timeout | |
GITLAB_MCP_SESSION_REVALIDATE_INTERVAL | --revalidate-interval | The one pair whose names differ |
GITLAB_MCP_DRAIN_DELAY | --drain-delay | |
GITLAB_MCP_AUTH_MODE | --auth-mode | |
GITLAB_MCP_PUBLIC_URL | --public-url | |
GITLAB_MCP_TRUSTED_ORIGINS | --trusted-origins | |
GITLAB_MCP_OAUTH_CACHE_TTL | --oauth-cache-ttl | |
GITLAB_MCP_OAUTH_CLIENT_UID | --oauth-client-uid | |
| None | --resource-documentation, --resource-policy-uri, --resource-tos-uri | The RFC 9728 metadata links; flags only |
GITLAB_MCP_RATE_LIMIT_RPS | --rate-limit-rps | Defaults differ: 10 in HTTP mode, 0 on stdio, which ignores the flag |
GITLAB_MCP_RATE_LIMIT_BURST | --rate-limit-burst | |
GITLAB_MCP_AUTH_FAILURE_LIMIT | --auth-failure-limit | |
GITLAB_MCP_AUTH_FAILURE_WINDOW | --auth-failure-window | |
GITLAB_MCP_AUTH_DISTINCT_TOKEN_LIMIT | --auth-distinct-token-limit | |
GITLAB_MCP_AUTH_DISTINCT_TOKEN_WINDOW | --auth-distinct-token-window | |
| None | --trusted-proxies, --trusted-proxy-header | Flags only, and each requires the other |
| None | --http-addr, --http-socket-mode, --tls-cert, --tls-key | The listener; flags only |
| None | --stateless, --json-response, --max-request-body-bytes, --http-idle-timeout | The streamable HTTP transport; flags only |
GITLAB_MCP_TELEMETRY | --telemetry | Both transports |
GITLAB_MCP_TELEMETRY_IDENTITY | --telemetry-identity | Both transports |
GITLAB_MCP_TELEMETRY_IDENTITY_KEY | None | Environment only, on purpose: process arguments are readable through /proc |
GITLAB_MCP_TELEMETRY_IDENTITY_ROTATION | --telemetry-identity-rotation | Both transports |
GITLAB_MCP_TELEMETRY_TOOL_NAME | --telemetry-tool-name | Both transports |
The flags that run once and exit (--tool-search, --probe, --shutdown, --version, -h and --help) have no variable; --tool-search reads GITLAB_MCP_TOOL_SURFACE and GITLAB_MCP_TIER and the dotenv files, so a stdio deployment searches what it serves. The command-line reference covers each flag.
Local paths in HTTP mode
Section titled “Local paths in HTTP mode”A server reached over HTTP refuses every local path a caller names: a file_path or directory_path to read, an output_path to write and a local import archive. The caller has no files on the machine the server runs on, so any path they can name belongs to someone else. The refusal does not depend on the three GITLAB_MCP_ALLOWED_*_DIRS variables, which have no effect in HTTP mode, and follows the transport the process actually serves, --transport=auto included.
The error says so, and for file_path it names the input that carries the bytes in the request instead:
file_path is disabled when the server is reached over HTTP: the file would be read from the server's own disk, not yours; send the bytes with content_base64 insteadcontent_base64 is the remote form of an upload, and one reason the HTTP request body limit stays at the SDK’s 4 MiB by default.