Skip to content

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.

  • 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.
KindWhat is accepted
BooleanGo’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
DurationGo 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)
SizeA byte count, or a number with a KB, MB or GB suffix in any case. The suffixes are powers of 1024
ListComma-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
VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_URLhttps://gitlab.com on stdio; none in HTTP modeAn http:// or https:// URL with a host. In HTTP mode, a comma-separated listBoth--gitlab-urlOn 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_TOKENNone: required on stdioA GitLab personal access token (glpat-...); see Which tokenstdioNone, on purposeThe 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_VERIFYfalseBooleanBoth--skip-tls-verifySkips 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_INSTANCESfalseBoolean; anything unparseable is falseBoth--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)
TokenWhat the server serves it
Classic, api scopeEvery action the tier allows
Classic, read_api scopeThe 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 apiNothing. 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.

VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_TOOL_SURFACEdynamicdynamic, meta, individualBoth--tool-surfaceWhich tool catalog is registered: two find and execute tools (dynamic), one dispatcher per domain (meta), or one tool per action
GITLAB_MCP_CAPABILITY_SURFACEfullfull, minimalBoth--capability-surfaceminimal keeps the gitlab://tools manifest and drops the other resources, the prompts, the workflow guides and resource subscriptions
GITLAB_MCP_META_PARAM_SCHEMAopaqueopaque, compact, full, in any caseBoth--meta-param-schemaHow 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_TIERDetectedfree (or ce), premium, ultimate, in any caseBoth--tierSet, 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_ONLYfalseBooleanBoth--read-onlyRemoves 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_MODEfalseBooleanBoth--safe-modeAnswers 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_RESOURCEStrueBooleanBoth--embedded-resourcesAdds 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_TOOLSEmptyList of tool names, group names or canonical action IDs, such as gitlab_admin,gitlab_runner,project.deleteBoth--exclude-toolsRemoves 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_SCOPESfalseBooleanBoth--ignore-scopesRegisters 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_COMPATautooff (any case) disables; any other value means autoBoth--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_SUBSTITUTIONSEmptyold=new pairs, comma-separated, applied in order; a backslash escapes \, \= \\. At most 32 pairs, 256 bytes per halfBoth--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_MODEfalse1, true, yes (any case) are true; anything else is falseBoth--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
AUTOPILOTUnsetAs GITLAB_MCP_YOLO_MODEBothNoneAn 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_SIZE2GBSize, positive, at most 1 TBBoth--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_TIMEOUT65mDuration; 0 disables; at most 24hBoth--action-timeoutCancels 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

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.

VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_ALLOWED_UPLOAD_DIRSEmptyList of directoriesstdioNoneMore directories a tool may read a local file from: every file_path and directory_path input
GITLAB_MCP_ALLOWED_DOWNLOAD_DIRSEmptyList of directoriesstdioNoneMore 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_DIRSEmptyList of directoriesstdioNoneMore 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
VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_STDIO_MAX_LINE_BYTES4194304 (4 MiB)A positive byte count, no maximumstdioNoneThe 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_STREAMS64A non-negative integer; 0 removes the ceilingBothNoneHow 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_LEVELinfodebug, info, warn (or warning), error, in any case; anything else means infoBoth--log-level (both)Verbosity of the JSON log the server writes to stderr
GITLAB_MCP_PPROF_ADDREmptyhost: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_FILEEmptyA path to a dotenv file, absolute by preferenceBoth--env-file (both)One dotenv file to load besides the home file. See Dotenv files
VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_MAX_HTTP_CLIENTS100A positive integer, at most 10000HTTP; stdio validates--max-http-clientsHow 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_TIMEOUT30mA positive duration, at most 24h. The variable refuses 0, which the flag acceptsHTTP; stdio validates--session-timeoutIdle 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_TIMEOUT1hDuration; 0 disables; at most 24hHTTP; stdio validates--pool-idle-timeoutReclaims 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_INTERVAL15mDuration; 0 stops the periodic check; at most 24hHTTP; stdio validates--revalidate-intervalHow 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_DELAY0Duration, at most 5mHTTP; stdio validates--drain-delayAfter 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
VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_AUTH_MODElegacylegacy, oauth, in lower caseHTTP; stdio validates--auth-modelegacy 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_URLEmptyAn absolute https://host[:port][/path] URL with no trailing slash and no fragment; http only for localhost, 127.0.0.1 or ::1HTTP; stdio validates it under oauth--public-urlThe 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_ORIGINSEmptyList of absolute origins (scheme://host[:port]), or *HTTP--trusted-originsOrigins 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_TTL15mDuration from 1m to 2hHTTP; stdio validates--oauth-cache-ttlHow 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_UIDEmptyList of GitLab OAuth application uidsHTTP--oauth-client-uidAdmits 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)
VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_RATE_LIMIT_RPS0 on stdio, 10 in HTTP modeA non-negative number, at most 1000; 0 disablesBoth--rate-limit-rpsPer-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_BURST40An integer from 0 to 10000Both--rate-limit-burstThe 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_LIMIT10An integer from 0 to 100000; 0 disablesHTTP; stdio validates--auth-failure-limitFailed 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_WINDOW1mDuration; 0 disables; at most 24hHTTP; stdio validates--auth-failure-windowThe 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_LIMIT50An integer from 0 to 100000; 0 disablesHTTP; stdio validates--auth-distinct-token-limitDistinct 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_WINDOW10mDuration; 0 disables; at most 24hHTTP; stdio validates--auth-distinct-token-windowThe window the distinct-credential budget counts in

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.

VariableDefaultAcceptsTransportFlagWhat it does
GITLAB_MCP_TELEMETRYfalsetrue or false, in any case; anything else warns and counts as falseBoth--telemetry (both)Exports traces, metrics and logs over OTLP. OTEL_SDK_DISABLED=true vetoes it
GITLAB_MCP_TELEMETRY_IDENTITYnonenone, pseudonymous, full, in any caseBoth--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_KEYEmptyAny secretBothNone, on purposeThe 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_ROTATIONEmptyDuration; 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_NAMEautoauto, on, off, in any case; anything else is logged at ERROR and read as autoBoth--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 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.

VariableRead byWhat this server does with it
OTEL_SDK_DISABLEDThis server onlytrue (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_PROTOCOLThis serverChooses 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 formsThe exporters; this server reads it tooNamed 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 formsThe exporters; this server reads it tooA 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 formsThe exporters; this server reads it tooRead 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 formsThe exporters; this server checks them firstA 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_ATTRIBUTESThe SDK; this server checks themWhen 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_LIMITThe SDK; this server checks whether it is setUnset, the server bounds every span attribute value at 4096 characters rather than leaving it unlimited
OTEL_TRACES_SAMPLERThe SDK; this server checks whether it is setUnset, 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 onlyNothing. Their durations are integer milliseconds

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_PROXY and NO_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 in NO_PROXY.
  • SSL_CERT_FILE on 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 no GITLAB_MCP_SKIP_TLS_VERIFY.

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 nameWhat happens now
GITLAB_READ_ONLYRefuses 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_MODERefuses the start, the same way
EXCLUDE_TOOLSRefuses 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 29A 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.

Besides its environment, the server reads two dotenv files, in both transports, before anything reads configuration:

  1. ~/.gitlab-mcp-server.env in your home directory, when it exists.
  2. The one file GITLAB_MCP_ENV_FILE names, or --env-file, which sets the same thing and wins over the variable.

Precedence, highest first:

  1. 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.
  2. The process environment, which is what the MCP client passed or the shell exported.
  3. The file GITLAB_MCP_ENV_FILE names.
  4. 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-mcp-server.env
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
GITLAB_URL=https://gitlab.example.com
GITLAB_MCP_TOOL_SURFACE=dynamic
GITLAB_MCP_UPLOAD_MAX_FILE_SIZE=500MB
GITLAB_MCP_LOG_LEVEL=info

Never 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.

In HTTP mode, configuration resolves in three layers, highest first:

  1. 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=10 beats GITLAB_MCP_RATE_LIMIT_RPS=2.
  2. The variable with the same meaning, when its flag was not passed.
  3. 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).

VariableFlagNotes
GITLAB_MCP_ENV_FILE--env-fileBoth transports; the flag wins
None--http, --transportThe transport is chosen on the command line only
GITLAB_URL--gitlab-urlA comma-separated value spells the list a repeated flag does
None--allow-any-gitlab-urlNo 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_TOKENNonestdio 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-instancesBoth 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--tierNeither 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-compatBoth transports; the flag writes the variable
GITLAB_MCP_DESCRIPTION_SUBSTITUTIONS--description-substitutionsBoth transports; the flag writes the variable
GITLAB_MCP_YOLO_MODE--yolo-modeBoth transports; the flag writes the variable
AUTOPILOTNoneBoth transports; loses to a non-empty GITLAB_MCP_YOLO_MODE
GITLAB_MCP_UPLOAD_MAX_FILE_SIZE--upload-max-file-sizeBoth transports; the flag writes the variable
GITLAB_MCP_ACTION_TIMEOUT--action-timeoutBoth transports; stdio reads the variable only
GITLAB_MCP_ALLOWED_UPLOAD_DIRS, GITLAB_MCP_ALLOWED_DOWNLOAD_DIRS, GITLAB_MCP_ALLOWED_IMPORT_DIRSNonestdio only: HTTP mode refuses every local path whatever they say
GITLAB_MCP_STDIO_MAX_LINE_BYTESNonestdio only. The HTTP counterpart is --max-request-body-bytes, which has no variable
GITLAB_MCP_MAX_LISTEN_STREAMSNoneBoth transports; environment only
GITLAB_MCP_LOG_LEVEL--log-levelBoth transports; the flag writes the variable
GITLAB_MCP_PPROF_ADDR--pprof-addrBoth transports; the flag writes the variable
GITLAB_MCP_MAX_HTTP_CLIENTS--max-http-clients
GITLAB_MCP_SESSION_TIMEOUT--session-timeoutOnly the flag accepts 0
GITLAB_MCP_POOL_IDLE_TIMEOUT--pool-idle-timeout
GITLAB_MCP_SESSION_REVALIDATE_INTERVAL--revalidate-intervalThe 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-uriThe RFC 9728 metadata links; flags only
GITLAB_MCP_RATE_LIMIT_RPS--rate-limit-rpsDefaults 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-headerFlags only, and each requires the other
None--http-addr, --http-socket-mode, --tls-cert, --tls-keyThe listener; flags only
None--stateless, --json-response, --max-request-body-bytes, --http-idle-timeoutThe streamable HTTP transport; flags only
GITLAB_MCP_TELEMETRY--telemetryBoth transports
GITLAB_MCP_TELEMETRY_IDENTITY--telemetry-identityBoth transports
GITLAB_MCP_TELEMETRY_IDENTITY_KEYNoneEnvironment only, on purpose: process arguments are readable through /proc
GITLAB_MCP_TELEMETRY_IDENTITY_ROTATION--telemetry-identity-rotationBoth transports
GITLAB_MCP_TELEMETRY_TOOL_NAME--telemetry-tool-nameBoth 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.

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 instead

content_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.