Skip to content

Command Line

gitlab-mcp-server [flags]
gitlab-mcp-server --probe [flags] [target]

Run without flags, the server serves stdio, reading its configuration from the environment, from ~/.gitlab-mcp-server.env and from the file --env-file or GITLAB_MCP_ENV_FILE names. Started by hand in an interactive terminal with GITLAB_URL or GITLAB_TOKEN still unset once those files are read, it prints what it needs and waits instead; see First run without configuration.

The flags are parsed by Go’s standard flag package, which decides a few things worth knowing:

  • A flag takes one dash or two: -http and --http are the same flag. This page writes two.
  • A value follows = or a space (--http-addr=:9090 or --http-addr :9090). A boolean flag takes a value only after =: --stateless=false turns stateless off, while --stateless false sets it to true and ends the flags at the word false.
  • Parsing stops at the first argument that is not a flag, so a --probe target goes last: nothing after it is read as a flag.
  • A duration uses Go’s syntax: 90s, 30m, 1h30m. 0 needs no unit, and any other bare number is refused.
  • A flag the binary does not define, or a value its type cannot parse, stops the program before it does anything: the parser prints the error and its own alphabetical list of every flag, and the exit code is 2. The curated help is --help.

A stdio server builds its configuration from the environment. Of the flags, it reads the general, environment-backed and telemetry ones; the HTTP mode flags are read only by a server serving HTTP. (--tool-search reads --tool-surface and --tier, and --probe reads --tls-cert, but both exit before a transport is chosen.)

A stdio run given an HTTP-mode flag ignores it and says so once at startup, at WARN, naming each flag with the variable stdio reads instead where there is one: --max-request-body-bytes, for instance, names GITLAB_MCP_STDIO_MAX_LINE_BYTES, which bounds the same message on stdio. When --transport=auto chose stdio, a flag stdio has no setting for at all (the listener, the pool, the authentication gate) is named at INFO instead, since that command line was written for either transport; a flag stdio has a variable for stays at WARN.

Where ignoring the flag would cost more than a setting, a stdio run refuses to start, exits 1 and names the setting to use instead, under --transport=auto too and even when that variable already asks for the same thing:

  • --read-only or --safe-mode set to true, or an --exclude-tools that names anything: ignoring it would serve what it asks to withhold. Set GITLAB_MCP_READ_ONLY, GITLAB_MCP_SAFE_MODE or GITLAB_MCP_EXCLUDE_TOOLS instead.
  • --gitlab-url naming no instance the stdio run connects to: stdio connects to the one GITLAB_URL names, https://gitlab.com when it is unset, and would send GITLAB_TOKEN there. Set GITLAB_URL to the instance instead. A --gitlab-url naming that same instance, however it is spelled, is only named: the two are compared in canonical form, so https://GitLab.example.com:443/ names https://gitlab.example.com.

A value that asks for nothing (--read-only=false, an empty --exclude-tools) is only named, like any other ignored flag.

In HTTP mode, settings resolve in three layers, highest first: a flag passed explicitly, then its environment variable, then the built-in default. A flag passed with the same value as its default still counts as passed, so a stray variable cannot displace a deliberate command line.

In the tables below, the Variable column names the environment variable a flag writes (the environment-backed flags) or falls back to when it is not passed (every other flag that has one); none means the flag has no environment counterpart. The Default column is the value the flag is registered with; where that is empty and stands for something, the description says what.

FlagTypeDefaultVariableDescription
-h, --helpboolfalsenonePrint the curated help: every flag grouped by what it configures, the environment variables, and JSON configuration examples. Both spellings print the same text
--versionboolfalsenonePrint gitlab-mcp-server <version> (commit: <commit>) and exit
--shutdownboolfalsenoneTerminate every other running instance of this binary and exit; see Shutdown mode
--probeboolfalsenoneAsk the running instance’s /health and exit 0 when it answers 200: the container image’s HEALTHCHECK. An optional target after the flags (a URL, unix:<path> or host:port) is probed instead of the discovered listener; see Probe mode
--tool-searchstring(empty)noneSearch the canonical action catalog and exit; see Tool search. Reads --tool-surface and --tier when passed, and otherwise GITLAB_MCP_TOOL_SURFACE and GITLAB_MCP_TIER. Needs no GitLab credentials
--env-filestring(empty)Sets GITLAB_MCP_ENV_FILEOne dotenv file to load besides ~/.gitlab-mcp-server.env. The same setting as GITLAB_MCP_ENV_FILE, and wins over it. Give an absolute path: a relative one follows the client into whatever directory it starts the server in, and startup says so
--transportstring(empty)noneTransport to serve: stdio, http or auto. Empty defers to --http; given both, --transport wins and says so at startup. auto reads file descriptor 0 and serves HTTP only when stdin is the null device, which is what a container started without -i gives, and stdio otherwise. Any other value exits 2
--httpboolfalsenoneServe HTTP instead of stdio

A run given more than one of the one-shot flags runs the first of -h/--help, --version, --shutdown, --probe and --tool-search, in that order, and ignores the rest. An invalid --transport is refused before any of them, --help included.

Seven settings whose only home used to be an environment variable have a flag each, so that one command line can configure the whole server. A passed flag writes its variable before anything reads configuration, so each setting keeps exactly one reader and an explicitly passed flag beats an exported variable. A flag that is not passed writes nothing, which is why all seven are registered with an empty default: the variable, or the built-in default named below, decides. All seven are read by both transports.

FlagTypeDefaultVariableDescription
--log-levelstring(empty)Sets GITLAB_MCP_LOG_LEVELLogging verbosity: debug, info, warn (or warning) or error. Unset, or any other value, logs at info
--client-compatstring(empty)Sets GITLAB_MCP_CLIENT_COMPATPer-client response compatibility: off turns it off, and anything else, unset included, means auto. See Client Compatibility
--upload-max-file-sizestring(empty)Sets GITLAB_MCP_UPLOAD_MAX_FILE_SIZELargest local file an upload or file-read tool accepts: a byte count, or a number with a KB, MB or GB suffix in either case (multiples of 1024). Unset means 2GB, and the ceiling is 1 TB. On stdio a value that does not parse, or one above the ceiling, refuses startup; in HTTP mode it is logged at WARN and the server uses 2GB or the ceiling instead
--yolo-modestring(empty)Sets GITLAB_MCP_YOLO_MODE1, true or yes skips the confirmation of destructive actions on every surface: no prompt on the meta and individual surfaces, and no confirm: true needed on the default dynamic one (see Destructive actions); any other value keeps it. When the variable is set it decides, and AUTOPILOT is consulted only when it is not, so --yolo-mode=false overrides an inherited AUTOPILOT=true
--description-substitutionsstring(empty)Sets GITLAB_MCP_DESCRIPTION_SUBSTITUTIONSComma-separated old=new pairs applied in order to every listed description and title, for strict MCP gateway validators (backslash escapes \,, \= and \\). A malformed value refuses startup. See Client Compatibility
--allow-private-instancesstring(empty)Sets GITLAB_MCP_ALLOW_PRIVATE_INSTANCEStrue permits a destination this server’s operator did not choose to be a private, loopback, CGNAT, link-local, unique-local or unspecified address: an instance a caller named in the GITLAB-URL header 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 it says. See Outbound destinations
--pprof-addrstring(empty)Sets GITLAB_MCP_PPROF_ADDRServe Go’s profiling handlers (net/http/pprof) on this address, on a listener of their own started before the transport, so a CPU profile of startup can be taken. Only localhost or a loopback IP is accepted (127.0.0.1:6060, [::1]:6060, localhost:6060); any other host is refused at startup, because a heap profile is a copy of the process’s memory and the handlers take no credential. Empty serves nothing

--allow-private-instances reads its value as a Go boolean (true, 1, t), and anything else means false; the decision behind the exemption is ADR-0022.

GITLAB_TOKEN has no flag. A token on a command line is visible to every user on the machine through ps, is captured by process accounting and lands in shell history, so the environment is the only way to give one to a stdio server; in HTTP mode each client sends its own in a request header. The telemetry pseudonymisation key, GITLAB_MCP_TELEMETRY_IDENTITY_KEY, has no flag for the same reason.

These four are read by both transports: a stdio deployment is exactly the case an operator monitoring their own machine cares about. Telemetry is off by default, and the endpoint, credentials, sampling and batching come from the standard OTEL_EXPORTER_OTLP_* variables, which the exporters read themselves. See OpenTelemetry.

FlagTypeDefaultVariableDescription
--telemetryboolfalseGITLAB_MCP_TELEMETRYExport OpenTelemetry traces, metrics and logs over OTLP. OTEL_SDK_DISABLED=true vetoes it whatever the flag says. A telemetry pipeline that fails to start is logged, and the server keeps serving
--telemetry-identitystringnoneGITLAB_MCP_TELEMETRY_IDENTITYWhat telemetry records about who made a call: none records nobody, pseudonymous a per-process HMAC digest that correlates one caller’s calls without naming them, full the GitLab user id and username. A value it does not recognise is logged at ERROR and nothing is recorded about callers. See Recording who made a call
--telemetry-identity-rotationstring(empty)GITLAB_MCP_TELEMETRY_IDENTITY_ROTATIONHow long a generated pseudonymisation key lives, e.g. 24h. Empty or 0 keeps it for the life of the process, and 30 days (720h) is the ceiling; a value that does not parse or exceeds it is logged at ERROR and nothing is recorded about callers. Ignored, with a warning at startup, when GITLAB_MCP_TELEMETRY_IDENTITY_KEY is set
--telemetry-tool-namestringautoGITLAB_MCP_TELEMETRY_TOOL_NAMEWhether 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; on and off force it. A value it does not recognise is logged and read as auto

The 42 flags below are read only by a server serving HTTP; what a stdio run does with them is described in Which flags each transport reads. They are grouped as --help groups them.

FlagTypeDefaultVariableDescription
--http-addrstring:8080noneListen address. host:port binds TCP (localhost:8080, :9090, 127.0.0.1:8080); a value containing a path separator (/run/gitlab-mcp.sock) binds a unix socket instead, which removes the network hop to a same-machine proxy rather than encrypting it. See Listening on a unix socket, or on TLS
--http-socket-modestring(empty)nonePermission mode, in octal, for a unix socket named by --http-addr: 0001 to 0777, with or without a 0o prefix. Empty means 0660, which lets owner and group connect and nobody else, so a reverse proxy reaches the server by sharing a group with it
--tls-certstring(empty)nonePEM certificate file. Serves HTTPS on the listener itself, for a deployment whose proxy does not share the machine. Requires --tls-key, and each is refused without the other. The pair is loaded at startup, so a typo fails there; after that both files are checked on every handshake and re-read when either changed, so a rotation needs no restart, and a pair that does not load keeps the previous certificate with a warning. TLS 1.2 is the floor
--tls-keystring(empty)nonePEM private key file matching --tls-cert
--statelessbooltruenoneSessionless streamable HTTP (SEP-2567), the only way protocol 2026-07-28 is served over HTTP: no Mcp-Session-Id, every POST self-contained, GET and DELETE answered 405. --stateless=false restores the legacy stateful sessions and warns at startup. See Stateless mode
--json-responseboolfalsenoneAnswer with application/json bodies instead of text/event-stream (SSE). A JSON body carries one response and no other frame, so progress notifications are dropped under --stateless and reach a stateful client only on a GET stream it holds open; startup warns whenever the flag is set
--max-request-body-bytesint640noneLargest streamable HTTP request body, in bytes; 0 uses the SDK default (4 MiB). A larger body is refused with 413, and a negative value at startup. On stdio the same message is bounded by GITLAB_MCP_STDIO_MAX_LINE_BYTES
--session-timeoutduration30mGITLAB_MCP_SESSION_TIMEOUTIdle MCP session timeout, at most 24h. Applies to --stateless=false only, since 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, and with 0 until the pool evicts its credential, which startup warns about
--http-idle-timeoutduration0noneHTTP server idle connection timeout. 0 disables idle closure, so --session-timeout is the effective lifetime; a positive value recycles idle connections sooner, and a negative one is refused
FlagTypeDefaultVariableDescription
--gitlab-urllist(empty)GITLAB_URLGitLab instance URL. Required in HTTP mode, through the flag or GITLAB_URL, unless --allow-any-gitlab-url is passed: a deployment that has not said which GitLab it serves would make its requests to whatever host a caller names in GITLAB-URL, with whatever token that caller supplied. Repeatable, or comma-separated (the variable takes the same list): publishing several lists them all in the RFC 9728 authorization_servers field and makes GITLAB-URL a required choice among them, since picking for the caller would send their token to an instance they never named; a value naming anything else is refused, not ignored. See Publishing more than one instance
--allow-any-gitlab-urlboolfalsenoneStart with no instance published and let GITLAB-URL name any host. The response comes back to the caller, so this makes the server a proxy for whoever can reach the listener: it is for the single-user local deployment where the operator is the caller, and it is refused unless --http-addr binds a loopback address or a unix socket. It warns at startup even there, and has no variable on purpose, so that a deployment running with it says so on its own command line. It does not widen what those hosts may resolve to: a caller-named instance on a private, loopback or link-local address still needs --allow-private-instances. A --gitlab-url passed beside it wins, and it then changes nothing
--skip-tls-verifyboolfalseGITLAB_MCP_SKIP_TLS_VERIFYSkip TLS certificate verification when calling GitLab (outbound; unrelated to --tls-cert). --auth-mode=oauth refuses it for any instance that is not loopback, since bearer tokens are forwarded there on every call: install the CA in the system trust store, or point SSL_CERT_FILE at a bundle, instead
--tierstring(empty)GITLAB_MCP_TIERForce the licensing tier: free (or ce), premium or ultimate, used as given with no licence check. Empty detects it per token and URL pool entry, from the instance licence and then from the namespace plans, falling back to free, with a warning when the instance is an Enterprise Edition build. The variable can only pin a tier, never un-pin one
--ignore-scopesboolfalseGITLAB_MCP_IGNORE_SCOPESSkip the scope filter and the read-only narrowing and register every tool the tier allows. 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
FlagTypeDefaultVariableDescription
--tool-surfacestring(empty)GITLAB_MCP_TOOL_SURFACETool catalog: dynamic, which is what empty serves, meta or individual. See Tools Overview
--capability-surfacestringfullGITLAB_MCP_CAPABILITY_SURFACEResources and prompts: full, or minimal, which keeps the gitlab://tools manifest and leaves out the optional GitLab data resources, the workflow guides and the prompts
--meta-param-schemastringopaqueGITLAB_MCP_META_PARAM_SCHEMAMeta-tool input schema strategy: opaque, compact (about 8.7 times the size of opaque) or full (about 18.3 times). Applies to meta-tool schemas only; every action’s exact call shape stays readable through gitlab://tools/{id}
--embedded-resourcesbooltrueGITLAB_MCP_EMBEDDED_RESOURCESEmbed the canonical gitlab:// resource URI in get-style tool results; false leaves it out
--exclude-toolsstring(empty)GITLAB_MCP_EXCLUDE_TOOLSComma-separated tool names, group names or canonical action IDs to remove, on every surface and from the resources, subscriptions, prompts and argument completions that return the same objects. The same spellings reach the standalone utilities (gitlab_interactive, interactive.issue_create, discover_project.resolve). An entry naming nothing is a warning, not a refusal, written the first time each catalog is built, and the tier and the token’s scope narrowing are part of what a catalog is, so it can repeat
--read-onlyboolfalseGITLAB_MCP_READ_ONLYRemove every mutating action; reads keep working on every surface. Wins over --safe-mode when both are set
--safe-modeboolfalseGITLAB_MCP_SAFE_MODEIntercept every mutating action and answer with a preview card, naming the action (the tool on the individual surface) and showing as JSON the arguments it would have sent, instead of running it; reads keep working
FlagTypeDefaultVariableDescription
--auth-modestringlegacyGITLAB_MCP_AUTH_MODElegacy (the PRIVATE-TOKEN header, or Authorization: Bearer) or oauth (RFC 9728 bearer verification), which requires an https --gitlab-url for every published instance (http only on loopback) and a valid --public-url. See OAuth mode
--public-urlstring(empty)GITLAB_MCP_PUBLIC_URLExternally reachable origin of this deployment (scheme://host[:port][/path]): https (http only for a loopback host), no fragment, no trailing slash. Required with --auth-mode=oauth, where it is the RFC 9728 protected-resource identifier and the metadata URL is derived from it. Optional in legacy mode, where its origin is added to the trusted origins
--oauth-cache-ttlduration15mGITLAB_MCP_OAUTH_CACHE_TTLHow long a verified OAuth token identity is cached, from 1m to 2h; used only with --auth-mode=oauth. The cache holds at most 10,000 identities whatever the TTL, dropping an expired one, or else the one used least recently, when full
--oauth-client-uidstring(empty)GITLAB_MCP_OAUTH_CLIENT_UIDComma-separated GitLab OAuth application uids whose tokens this deployment admits. Empty admits any credential the instance accepts; setting it also refuses personal access tokens, which belong to no application. It stands in for audience binding, which GitLab does not offer; see ADR-0019
--revalidate-intervalduration15mGITLAB_MCP_SESSION_REVALIDATE_INTERVALHow often a pooled credential is re-validated against GitLab, at most 24h. 0 stops the periodic check, but an entry whose credential is older than 1h is still rebuilt, which re-runs the probe and ends any stateful session on it
--resource-documentationstring(empty)nonehttps URL published as RFC 9728 resource_documentation. Point it at a page describing your own OAuth application (its client ID and registered redirect URIs), so that a client arriving from a 401 challenge finds what it needs: RFC 9728 defines no field for a client identifier, so this is the only sanctioned way to lead a client to one. Empty publishes this project’s OAuth application page
--resource-policy-uristring(empty)nonehttps URL published as RFC 9728 resource_policy_uri: your page on what this deployment does with the data reached through the tokens it accepts. Empty omits the field, the right default for a deployment with no such page, since an absent optional field is better than a dead link on a consent screen
--resource-tos-uristring(empty)nonehttps URL published as RFC 9728 resource_tos_uri: your terms of service. Empty omits the field

The three --resource-* URLs are checked at startup in either mode, since they are published the moment OAuth is turned on: each must be an absolute https URL, or http on a loopback host.

FlagTypeDefaultVariableDescription
--max-http-clientsint100GITLAB_MCP_MAX_HTTP_CLIENTSMost (token, GitLab URL) entries the pool keeps, from 1 to 10000. It bounds entries, not the sessions or calls they hold: the process bounds those by its descriptor limit, 192 held calls and 96 stateful sessions at once under a hard limit of 1024, and no flag moves either. See Requests held open at once
--pool-idle-timeoutduration1hGITLAB_MCP_POOL_IDLE_TIMEOUTReclaim a pooled credential entry after this long unused, at most 24h; 0 keeps entries until the size bound evicts them. An entry with a live subscription is never idle by this measure
--action-timeoutduration65mGITLAB_MCP_ACTION_TIMEOUTCancel an action still running after this long, at most 24h; 0 disables it. The timeout applies on stdio too, where the variable is the only spelling. It sits above the longest wait any action offers (a pipeline wait caps itself at 3600 s), so it ends a handler nothing else bounds rather than a legitimate wait, and it bounds a file transfer as well
--drain-delayduration0GITLAB_MCP_DRAIN_DELAYAfter SIGTERM, keep the listener open and answer /health with 503 draining for this long before closing it, so a balancer that polls /health takes the instance out of rotation first. 0 closes at once, and the most is 5m. Set it to at least one probe interval; see Health check
--rate-limit-rpsfloat10GITLAB_MCP_RATE_LIMIT_RPSPer-credential rate 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, with the same burst. A listing is also charged first, in the tools it carries, to one bucket the whole process shares: 3000 tools a second with 48000 in hand, not configurable. 0 turns all of them off, and the most is 1000. Stdio ignores the flag and says so: there GITLAB_MCP_RATE_LIMIT_RPS is the switch, and it defaults to 0. See Rate limiting
--rate-limit-burstint40GITLAB_MCP_RATE_LIMIT_BURSTToken-bucket burst size, at most 10000. While --rate-limit-rps is above 0 it must be at least 1
--auth-failure-limitint10GITLAB_MCP_AUTH_FAILURE_LIMITFailed authentications one address may produce inside --auth-failure-window before it is blocked for the rest of it, from 0 to 100000. 0 turns this budget off rather than blocking on the first failure. See Authentication budgets
--auth-failure-windowduration1mGITLAB_MCP_AUTH_FAILURE_WINDOWWindow the failure budget counts in, and the step the distinct-credential escalation is built from: one window, then ten, then sixty. At most 24h; 0 turns off both budgets, since the escalation has no step without it
--auth-distinct-token-limitint50GITLAB_MCP_AUTH_DISTINCT_TOKEN_LIMITDistinct credentials one address may have refused inside --auth-distinct-token-window before it is blocked, for longer each time, from 0 to 100000; 0 turns this budget off. A person has one token and a fleet behind a NAT has one each, so only a spray reaches this count. A credential the pool already holds is still served from a blocked address
--auth-distinct-token-windowduration10mGITLAB_MCP_AUTH_DISTINCT_TOKEN_WINDOWWindow the distinct-credential budget counts in, at most 24h; 0 turns that budget off
--trusted-originsstring(empty)GITLAB_MCP_TRUSTED_ORIGINSComma-separated absolute origins (scheme://host[:port]; an IP is fine for a local deployment) allowed to make cross-origin browser requests. * accepts any origin and disables the protection. Empty adds none, though the --public-url origin is trusted regardless. A malformed entry refuses startup. See Cross-origin protection
--trusted-proxy-headerstring(empty)noneHTTP header carrying the real client address behind a reverse proxy (e.g. CF-Connecting-IP, X-Forwarded-For, X-Real-IP), so that the authentication budgets charge callers rather than the proxy. Believed only on a connection from an address in --trusted-proxies, which it requires
--trusted-proxiesstring(empty)noneComma-separated 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 and the peer itself is charged, so a caller who reaches the listener directly cannot choose the address its failures count against. For X-Forwarded-For the value is read from the right, skipping hops that are themselves listed, so the first unlisted hop is the client; a hop that is not an address charges the peer. Required with --trusted-proxy-header, and refused without it

The server reads its configuration from the environment and speaks JSON-RPC over stdin and stdout, which is how MCP clients such as VS Code, Claude Desktop and Cursor run it. Settings this project defines are named GITLAB_MCP_<NAME>; GITLAB_URL, GITLAB_TOKEN and every OTEL_* variable keep their bare names, and GITLAB_URL defaults to https://gitlab.com, so it is needed only for a self-managed instance.

Terminal window
# Configuration from the environment
export GITLAB_TOKEN="glpat-xxxxxxxxxxxxxxxxxxxx"
gitlab-mcp-server
# Configuration from ~/.gitlab-mcp-server.env, or from a file named in GITLAB_MCP_ENV_FILE
gitlab-mcp-server

A .env in the current directory is not read: that directory belongs to whatever repository the client opened, not to the operator. One that exists is named at WARN on startup. The spellings variables had before 2.8.0 are read by nothing since 3.1.0: one left set is named at startup with the variable to rename it to, and GITLAB_READ_ONLY, GITLAB_SAFE_MODE and EXCLUDE_TOOLS refuse the start instead, since ignoring one would serve what it withheld. The environment variable reference has the full rule.

The server listens on an HTTP endpoint, and each client sends its own GitLab token with every request, in the PRIVATE-TOKEN header or as Authorization: Bearer, so no GITLAB_TOKEN is needed at startup. The instance is not optional: name the one this deployment serves, or the several, with --gitlab-url or GITLAB_URL. Publishing several makes the GITLAB-URL header a required choice among them, and publishing none is possible only with --allow-any-gitlab-url. See HTTP Server Mode for the deployment itself.

Terminal window
# One instance (all clients use the fixed URL; replace for self-managed GitLab)
gitlab-mcp-server --http --gitlab-url=https://gitlab.com
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --http-addr=localhost:9090
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --max-http-clients=50 --session-timeout=1h
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --auth-mode=oauth --public-url=https://mcp.example.com --oauth-cache-ttl=15m
# Stateless streamable HTTP (the default) with plain JSON responses
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --json-response
# Legacy stateful sessions with a 1 MiB request body cap
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --stateless=false --max-request-body-bytes=1048576
# Several instances (the GITLAB-URL header is then required, and must name one of them)
gitlab-mcp-server --http --gitlab-url=https://gitlab.com,https://gitlab.example.com

Started by hand in a terminal, or double-clicked, in stdio mode with GITLAB_TOKEN or GITLAB_URL still unset once the dotenv files are read (either one missing is enough), the server prints to stderr what it is and what it needs, then waits for Enter and exits 0. The wait matters on Windows, where a double-clicked console program closes its window the instant it returns, so a message printed and returned from is a message nobody reads.

An MCP client never reaches that screen: a client connects pipes rather than a terminal, which is the test the server uses. It replaced an interactive setup wizard that wrote ~/.gitlab-mcp-server.env; MCP configuration lives in the client’s own JSON, which is where Getting Started puts it.

--tool-search searches the canonical action catalog, not a server’s registered tools, and exits. The catalog is the same on every surface (the default dynamic surface registers only two tools), so the actions found are too; the surface decides only how each row names the call. It needs no GitLab credentials and makes no request.

  • The query is split on whitespace, and every term must appear, case-insensitively, in an action’s canonical ID, its individual tool name, its meta tool name, its description, its aliases or its tags.
  • The surface and the tier come from --tool-surface and --tier when passed, and otherwise from GITLAB_MCP_TOOL_SURFACE and GITLAB_MCP_TIER, read from the same dotenv files the server reads, so a stdio deployment searches what it serves. With neither, the tier is free, so an action only a higher tier serves needs --tier.
  • A surface or tier that does not parse exits 1.

The results go to stdout, sorted by canonical ID, under a line saying how to call a listed action on that surface:

Found <n> action(s) matching "issue list" (tier free, dynamic surface):
Call gitlab_execute_action with {"action": "<ACTION>", "params": {...}}. TOOL is the name the individual surface would register.
ACTION TOOL DESCRIPTION

ACTION is the canonical ID, which is what gitlab_execute_action takes on the dynamic surface. TOOL is the individual tool name on the dynamic and individual surfaces, or the meta group tool followed by action=<name> on the meta surface; - marks an action the individual surface does not register. DESCRIPTION is cut at 80 characters. A query nothing matches prints No actions found matching with the query, the tier and the surface.

--shutdown terminates every other running instance of this binary and exits. It is meant for external updaters, such as pe-agnostic-store, that stop running servers before replacing the binary on disk.

Terminal window
gitlab-mcp-server --shutdown
  1. Lists the processes on the machine and keeps those whose name matches its own, once a platform suffix such as -linux-amd64 and a .exe extension are stripped from both, leaving itself out.
  2. With none found, exits 0 without writing anything.
  3. Sends each a graceful termination: SIGTERM on Unix, TerminateProcess on Windows.
  4. Checks every 200 ms, for up to five seconds, whether they have exited.
  5. Force-kills any still running when the five seconds are up.
  6. Exits 0. Only a failure to list the processes exits 1.

It writes these lines to stderr:

  • shutdown: found N running instance(s), once they are found
  • shutdown: all instances terminated, when none is left running
  • shutdown: force-killed M instance(s), when some had to be killed
  • shutdown: error listing processes: <error>, when the listing failed

--probe is the container image’s HEALTHCHECK (every 30 s, with a five-second timeout). It asks the running instance’s /health and exits 0 when it answers 200, without being told where that instance listens.

Terminal window
# What the image runs: find the server in this container and probe its listener
gitlab-mcp-server --probe
# Probe a known listener instead: a URL, a unix socket, or host:port
gitlab-mcp-server --probe --tls-cert=/etc/ssl/mcp.crt https://127.0.0.1:8443
gitlab-mcp-server --probe unix:/run/gitlab-mcp/server.sock
gitlab-mcp-server --probe 127.0.0.1:9090

Without a target:

  1. Finds the other instances of this binary with the lookup --shutdown uses, and skips probe, shutdown, version and help invocations and any process whose command line it cannot read.
  2. Reads --http-addr, --tls-cert, --transport and --http off each instance’s command line, lowest pid first. An argument after a bare -- is not read, since it was not a flag to that process either.
  3. Settles --transport auto the way the server did, by reading the instance’s file descriptor 0 from procfs: /dev/null means HTTP, anything else stdio. Where procfs cannot be read, HTTP is assumed and the connection decides.
  4. An instance serving stdio has nothing to probe and is reported healthy while it runs.
  5. An HTTP instance is probed where it listens. An unspecified host such as :8080, 0.0.0.0:8080 or [::]:8080 is reached on 127.0.0.1, a path is dialed as a unix socket, and --tls-cert means HTTPS. A TLS listener is verified by pinning: it must present the very certificate its --tls-cert names, which the probe reads from the same file and trusts as its only root, so a self-signed certificate on a loopback address is probeable without trusting whatever answers there.
  6. The first instance, in pid order, that serves stdio or answers 200 makes the probe healthy. If none does, or no instance is running, it exits 1.

A target given after the flags is probed instead: an http:// or https:// URL (its path, or /health when it names none), unix:<path> or a bare path for a unix socket, or host:port for plain HTTP. An https:// target is pinned to the probe’s own --tls-cert, which must come before the target, and gets the standard verification against the system roots without one. A target that is none of these exits 2.

Each attempt is bounded to three seconds, and the whole run to four, inside the image’s five-second timeout: attempts run one after another, so two unreachable listeners at three seconds each would outlast it and be killed without a verdict. The probe never goes through a proxy, and writes one line to stderr, prefixed probe:, saying which instance answered or why none did. A listener bound to port 0 cannot be discovered, because the command line says 0; give the probe the address the server’s log reports.

Terminal window
# Print the version
gitlab-mcp-server --version
# Show the curated help with every flag, the variables and JSON configuration examples
gitlab-mcp-server --help
# Start the stdio server (reads ~/.gitlab-mcp-server.env for what the environment lacks)
gitlab-mcp-server
# Start the HTTP server on another port
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --http-addr=:9090
# Several instances (clients pick one with the GITLAB-URL header)
gitlab-mcp-server --http --gitlab-url=https://gitlab.com,https://gitlab.example.com --http-addr=:8080
# Single-user local deployment: no instance published, GITLAB-URL may name any host
gitlab-mcp-server --http --allow-any-gitlab-url --http-addr=127.0.0.1:8080
# Self-managed GitLab with a self-signed certificate and a longer session timeout
gitlab-mcp-server --http --gitlab-url=https://gitlab.example.com --skip-tls-verify --session-timeout=2h
# One tool per action
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --tool-surface=individual
# The dynamic surface with the reduced resource and prompt surface
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --tool-surface=dynamic --capability-surface=minimal
# Find the actions that list issues, on whatever surface is configured
gitlab-mcp-server --tool-search "issue list"
# Search the Ultimate catalog from a Free deployment
gitlab-mcp-server --tier=ultimate --tool-search "epic"
# Terminate all running instances (used by external updaters)
gitlab-mcp-server --shutdown
# Container health check: probe the running instance where it listens
gitlab-mcp-server --probe

See Dynamic Tools for what the default surface registers and how gitlab_find_action and gitlab_execute_action reach the catalog.

CodeMeaning
0A normal end: a shutdown by signal (SIGINT or SIGTERM), --version, -h/--help, a completed --tool-search, --shutdown whether or not it found anything to stop, a --probe that was answered or found an instance serving stdio, and the first-run screen once Enter is pressed
1A configuration error (among them a stdio run given --read-only, --safe-mode or --exclude-tools asking to withhold something or a --gitlab-url naming another instance, and an HTTP run naming no instance without --allow-any-gitlab-url), an error the server stops on, a --tool-search whose surface or tier does not parse, a --shutdown that could not list processes, or a --probe nothing answered
2A flag the binary does not define or a value its type cannot parse, --transport given a value other than stdio, http or auto, or --probe given a target that is not a URL, a socket path or host:port