Troubleshooting
Connection and authentication
Section titled “Connection and authentication”| Symptom | Cause | Solution |
|---|---|---|
GITLAB_TOKEN is required at startup | Token not set | Set GITLAB_TOKEN in the environment or in ~/.gitlab-mcp-server.env; a working-directory .env is not read, and one that exists is named at WARN on startup |
GITLAB_URL is not a valid URL at startup | URL has invalid syntax | Fix GITLAB_URL or omit it to use https://gitlab.com in stdio mode |
401 Unauthorized from GitLab API | Invalid or expired PAT | Generate a new token with api scope at GitLab → Preferences → Access Tokens |
403 Forbidden on specific operations | Token lacks required scope | Ensure the token has api scope (not just read_api) |
| Connection refused or timeout | GitLab instance unreachable | Verify GITLAB_URL is reachable: curl -s $GITLAB_URL/api/v4/version |
TLS and certificates
Section titled “TLS and certificates”| Symptom | Cause | Solution |
|---|---|---|
x509: certificate signed by unknown authority | Self-signed certificate | First try adding the CA certificate to your system trust store. If that’s not possible, set GITLAB_MCP_SKIP_TLS_VERIFY=true in the environment or in ~/.gitlab-mcp-server.env, or --skip-tls-verify in HTTP mode; --auth-mode=oauth refuses it for a non-loopback instance, so install the CA (or point SSL_CERT_FILE at a bundle) there |
x509: certificate has expired | Expired TLS certificate | Renew the certificate on the GitLab server, or use GITLAB_MCP_SKIP_TLS_VERIFY=true temporarily |
Network and proxy
Section titled “Network and proxy”DNS resolution
Section titled “DNS resolution”If the server cannot resolve your GitLab hostname:
# Verify DNS from the machine running the MCP servernslookup gitlab.example.com# ordig gitlab.example.com +shortIn Docker containers, ensure your compose file or docker run command uses --dns or a custom network with proper DNS configuration. Inside Kubernetes, check CoreDNS logs and resolv.conf in the pod.
Corporate proxies
Section titled “Corporate proxies”Go’s net/http honours the standard proxy environment variables. Set them before launching the server:
export HTTPS_PROXY=http://proxy.corp.example.com:8080export HTTP_PROXY=http://proxy.corp.example.com:8080export NO_PROXY=localhost,127.0.0.1,.internal.corp| Symptom | Cause | Solution |
|---|---|---|
| Connection timeout behind corporate network | Proxy not configured | Set HTTPS_PROXY / HTTP_PROXY |
Proxy works for curl but not for the server | Env vars not exported into the server process | Pass them in the client’s env block, ~/.gitlab-mcp-server.env, Docker environment:, or your platform’s secrets |
| Internal GitLab routed through external proxy | NO_PROXY missing | Add your GitLab hostname to NO_PROXY |
Reverse proxy (HTTP mode)
Section titled “Reverse proxy (HTTP mode)”When running the MCP server behind nginx, Caddy, or a cloud load balancer:
| Symptom | Cause | Solution |
|---|---|---|
429 after a few failed logins from different people | The authentication-failure limit (10 per address per minute) sees the proxy’s address, not the client’s | Set --trusted-proxy-header to the header your proxy sets (e.g. X-Forwarded-For, CF-Connecting-IP) and --trusted-proxies to the proxy’s address; the per-call rate limiter is keyed by token and unaffected |
| SSE stream disconnected | Proxy read or idle timeout shorter than the stream’s silence | Raise the proxy read timeout; the server’s 25-second SSE keep-alive holds an idle stream open across nginx’s 60-second default, so a tighter edge is what cuts it |
502 Bad Gateway | Server not listening yet | Add a startup probe or retry; the server needs a few seconds to initialize on first request |
Tool discovery
Section titled “Tool discovery”| Symptom | Cause | Solution |
|---|---|---|
Only two tools appear: gitlab_find_action and gitlab_execute_action | Expected — dynamic is the default surface | Not a failure. Ask for what you want in plain language; the assistant finds the action and its schema, then executes it. Set GITLAB_MCP_TOOL_SURFACE=meta or GITLAB_MCP_TOOL_SURFACE=individual if you want a browsable tool list |
| MCP client shows hundreds of individual tools instead of 34 | Individual surface selected | Set GITLAB_MCP_TOOL_SURFACE=meta to consolidate into domain meta-tools |
Tool not found in tools/list | Tool surface mismatch | Individual mode uses gitlab_issue_create, meta mode uses gitlab_issue with action: create, and dynamic mode exposes gitlab_find_action and gitlab_execute_action |
unknown action in meta-tool call | Invalid action parameter | Check valid actions in the Tools Overview |
json: unknown field "<name>" from a meta-tool | Misspelled or stale parameter in params | Meta-tools reject unknown keys. Use the exact parameter names for the chosen action (e.g. merge_request_iid, issue_iid, epic_iid, work_item_iid, snippet_id) |
| Enterprise tools missing | Enterprise catalog disabled | In stdio mode, set GITLAB_MCP_TIER=premium or GITLAB_MCP_TIER=ultimate. In HTTP mode, set --tier=premium/--tier=ultimate to force the catalog or let license detection enable it when the instance reports a Premium or Ultimate plan. The deprecated GITLAB_ENTERPRISE=true variable still works when GITLAB_MCP_TIER is unset; there is no --enterprise flag — use --tier |
npm / npx installs
Section titled “npm / npx installs”| Symptom | Cause | Solution |
|---|---|---|
the @jmrp.io/gitlab-mcp-server-linux-x64 package is not installed | On Alpine this is deliberate: the linux packages declare libc: glibc and the released binaries need the glibc dynamic loader, so npm skips them on musl. On a glibc distro it means the install skipped optional dependencies (--no-optional, or a lockfile resolved on another OS) | On musl, use the container image ghcr.io/jmrplens/gitlab-mcp-server (built against musl) or build from source — the publish is not broken. On glibc, reinstall without --no-optional, or delete node_modules and the lockfile and install again |
Updates never arrive under npx | The server never updates itself, and npm owns the binary | Expected. Upgrade with npm, or add @latest to the npx spec so it resolves afresh instead of reusing its cache |
Upgrading
Section titled “Upgrading”The server never updates itself, so an upgrade is whatever your install channel does plus a restart.
| Symptom | Cause | Solution |
|---|---|---|
| Still running the old version after upgrading | The old process was never restarted | Restart the server, or run gitlab-mcp-server --shutdown to terminate every running instance first |
| Cannot replace the binary (file locked) | Running instances hold the file open | Run gitlab-mcp-server --shutdown to terminate every instance, then replace it |
HTTP server mode
Section titled “HTTP server mode”| Symptom | Cause | Solution |
|---|---|---|
401 Unauthorized with WWW-Authenticate: Bearer | Missing or empty token header | Send PRIVATE-TOKEN or Authorization: Bearer <token> header with every request. The JSON body names both accepted headers |
400 Bad Request with no server available in plain text | A build older than 2.6.6 received a request it could not route | Not a crash and not emitted by this project — it comes from the MCP SDK when server selection finds no server. Upgrade: those cases are now 401, 400, 429 and 503 with a JSON-RPC body |
400 Bad Request naming GITLAB-URL | The per-request GITLAB-URL header is not a parseable URL | Send an absolute URL such as https://gitlab.example.com, or omit the header when the server runs with --gitlab-url |
400/403 saying the deployment does not serve that GitLab instance | --gitlab-url names more than one instance, so it publishes an allow-list | Send a GITLAB-URL the server publishes; omitting the header is refused too (400), never resolved to the first instance. In OAuth mode the error names the published instances — the same list its RFC 9728 metadata already serves unauthenticated — and answers 403 before the token is used anywhere. Legacy mode answers 400 and does not name them: it publishes no metadata document and reaches this rejection before the credential is validated, so ask the operator |
429 Too Many Requests with Retry-After | More than 10 failed authentications from one client IP within a minute | Wait for the window to pass and retry with a valid token. Behind a reverse proxy, set --trusted-proxy-header and --trusted-proxies so the limit counts real client IPs |
503 Service Unavailable | The GitLab instance was unreachable while building the session for that token | Check GITLAB-URL / --gitlab-url and instance reachability; the server logs the underlying error |
| Pool eviction too frequent | Too many unique tokens | Increase --max-http-clients (default: 100) |
| Sessions expiring unexpectedly | MCP idle timeout too short | Increase --session-timeout (default: 30m); it applies to --stateless=false only, the default stateless transport has no session to expire |
MCP sessions drop every ~2 min / keepalive ping failed; closing session | A low --http-idle-timeout (or a proxy timeout) is closing long-lived SSE streams | Default --http-idle-timeout=0 disables HTTP-layer idle closure; if you set a low value, raise it or use 0. Behind a reverse proxy, also raise its read/idle timeout |
OAuth mode (--auth-mode=oauth)
Section titled “OAuth mode (--auth-mode=oauth)”| Symptom | Cause | Solution |
|---|---|---|
401 Unauthorized with valid OAuth token | Token expired or GitLab rejected it | Re-authorize through the OAuth flow; check that the GitLab OAuth app is still active |
| High latency on first request after cache expiry | Token re-validation against GitLab API | Expected behavior — increase --oauth-cache-ttl (default: 15m, max: 2h) to reduce validation frequency |
404 on /.well-known/oauth-protected-resource | OAuth mode not enabled, or --public-url carries a path | Start the server with --auth-mode=oauth. When --public-url has a path, the document moves to the derived path (see HTTP Server Mode) and the bare path answers 404 on purpose |
| Client doesn’t start OAuth flow | Client lacks OAuth 2.1 support | Send a personal access token as Authorization: Bearer <glpat-...> — verified the same way as an OAuth token |
401 when sending PRIVATE-TOKEN in OAuth mode | OAuth mode is Bearer-only, by design | Move the token to Authorization: Bearer <token>; PRIVATE-TOKEN is accepted only in --auth-mode=legacy |
Operations fail with insufficient mcp scope | DCR fallback assigned mcp instead of api | Configure clientId explicitly in the MCP client config. See HTTP Server Mode |
Pagination
Section titled “Pagination”| Symptom | Cause | Solution |
|---|---|---|
| List results truncated | Default per_page limit | Pass per_page (max 100) and page parameters to paginate |
nextPage missing in response | Last page reached | No more results — this is expected behavior |
IDE-specific issues
Section titled “IDE-specific issues”| Symptom | Solution |
|---|---|
| “Tool not found” in Copilot Chat | Check Output panel → MCP Logs for errors. Verify .vscode/mcp.json path |
| Server not appearing in MCP status | Run Ctrl+Shift+P → MCP: List Servers to verify configuration |
| “Permission denied” on startup | Run chmod +x /path/to/gitlab-mcp-server (Linux/macOS) |
| Server restarts repeatedly | Check MCP Logs for missing GITLAB_URL or GITLAB_TOKEN |
Waiting for initialize with Docker logs showing HTTP mode | Add -i to the docker run args so the container gets a pipe to speak on and infers stdio (JSON-RPC over stdin/stdout, no port needed), and docker pull the image if it predates the release that made --transport auto its command. Alternatively, run the container in HTTP mode with -p 8080:8080 and an instance to serve (-e GITLAB_URL=https://gitlab.com, without which it exits with --gitlab-url is required in HTTP mode), then configure the MCP client as HTTP pointing to http://host:8080/mcp — this only works if the server is running and the port is reachable. |
| Symptom | Solution |
|---|---|
| Tools not listed | Verify .cursor/mcp.json exists and uses mcpServers key (not servers) |
${input:...} not working | Not supported by Cursor — use environment variables instead |
Debug mode
Section titled “Debug mode”Enable verbose logging to diagnose issues:
-
Set the log level to
debug:Terminal window # Stdio modeGITLAB_MCP_LOG_LEVEL=debug ./gitlab-mcp-server 2>debug.log# HTTP mode (logs interleaved with server output)GITLAB_MCP_LOG_LEVEL=debug ./gitlab-mcp-server --http --gitlab-url=https://gitlab.com 2>debug.log -
Reproduce the issue by running the same operation that failed.
-
Examine the logs — debug logs include:
- Every tool call with input parameters
- GitLab API request/response details
- Token validation events (last 4 characters only)
- Session pool operations (HTTP mode)
Getting help
Section titled “Getting help”If you cannot resolve an issue:
- Enable debug logging (
GITLAB_MCP_LOG_LEVEL=debug) and capture the output - Check the GitHub Issues for known problems
- Open a new issue with:
- Server version (
gitlab-mcp-server --versionor check startup logs) - Operating system and architecture
- MCP client name and version
- Redacted debug logs (remove any tokens or sensitive data)
- Steps to reproduce the issue
- Server version (
Frequently asked questions
Why does my MCP client show hundreds of tools?
The client is using the individual tool surface, which registers one tool per GitLab operation. Set GITLAB_MCP_TOOL_SURFACE=meta to consolidate them into 34 domain-level meta-tools, or use the default dynamic surface, which exposes only gitlab_find_action and gitlab_execute_action. Tool names also differ by surface: individual mode uses gitlab_issue_create, meta mode uses gitlab_issue with action: create, and dynamic mode uses find and execute.
How do I fix 401 or 403 errors from GitLab?
A 401 Unauthorized means the Personal Access Token is invalid or expired — generate a new token with the api scope at GitLab → Preferences → Access Tokens. A 403 Forbidden on specific operations means the token lacks the required scope; ensure it has the api scope, not just read_api. If the server will not start with GITLAB_TOKEN is required, set GITLAB_TOKEN in the client's environment or in ~/.gitlab-mcp-server.env (a .env in the working directory is not read).
Why are Enterprise tools missing?
The Enterprise/Premium catalog is disabled. In stdio mode, set GITLAB_MCP_TIER=premium or GITLAB_MCP_TIER=ultimate. In HTTP mode, set --tier=premium or --tier=ultimate to force the catalog, or let license detection enable it when the instance reports a Premium or Ultimate plan. The deprecated GITLAB_ENTERPRISE=true variable still works when GITLAB_MCP_TIER is unset; there is no --enterprise flag — use --tier. Enterprise tools also require a Premium or Ultimate license on the connected instance.
How do I enable debug logging?
Set GITLAB_MCP_LOG_LEVEL=debug and redirect stderr to a file — for example GITLAB_MCP_LOG_LEVEL=debug ./gitlab-mcp-server 2>debug.log — then reproduce the failing operation. Debug logs include every tool call with input parameters, GitLab API request and response details, token validation events (last 4 characters only), and HTTP session pool operations. Logs go to stderr and JSON-RPC messages go to stdout, so always redirect stderr to avoid mixing the two.