GitLab MCP Server

José M. Requena Plens·jmrplens.gitlab-mcp-server

GitLab MCP server exposing the REST v4 and GraphQL APIs as tools for AI assistants.

A Model Context Protocol (MCP) server that connects AI assistants (Claude, Cursor, VS Code + Copilot, and any MCP client) to GitLab.com or self-managed GitLab instances. Covers the full REST API v4 and GraphQL with automatic edition gating — roughly 850 to 1070 tools depending on the GitLab tier — plus 45 MCP resources, 37 prompts, argument completions, and progress notifications. Single static binary, stdio and HTTP transports, read-only and safe (mutation-preview) modes, and an interactive setup wizard (gitlab-mcp-server --setup) that auto-configures most MCP clients.

winget install --id jmrplens.gitlab-mcp-server --exact --source winget

Latest 2.7.5·August 27, 2026

Release Notes

Yes, another one. In my defence, 2.7.4 shipped a flag whose entire purpose was to let browsers talk to this server, and it did not let browsers talk to this server. The flag was fine. The preflight it depended on was refused, so the browser never got as far as sending the request the flag would have allowed. curl said 200 throughout, which is how it shipped.

So:2.7.5 is the one where the browser path is actually exercised by something other than optimism.


The browser path

--trusted-origins now works from a browser. A preflight OPTIONS from a trusted origin is answered 204 with the CORS headers, and Mcp-Session-Id / Mcp-Protocol-Version are exposed so a client can read them back.

⚠️ If you run behind a reverse proxy that adds its own CORS headers, remove that block in the same deploy as this upgrade. This is not tidiness — upgrading without it makes your deployment worse than it was:

Access to fetch at .../gitlab has been blocked by CORS policy:The 'Access-Control-Allow-Origin' header contains multiple values 'http://127.0.0.1:18090, *', but only one is allowed.

Fetch treats two Access-Control-Allow-Origin headers as a failure rather than merging them. Before the upgrade your proxy's lone * at least worked for uncredentialed requests; after it, the server sends its own and the pair cancel out. curl reports 200 either way, so nothing but a real browser will tell you. See Security — The browser preflight.

What happens to a request that does not get through

A public deployment was an amplifier. Unauthenticated traffic — which anyone can generate — was relayed to GitLab one request for one, and on gitlab.com that became rate-limit pressure charged to this server's address, landing on the legitimate users who share it. Three layers now sit in front:a per-address failure budget checked before anything else, a five-minute rejected-token cache keyed by SHA-256 digest (never the raw credential, bounded to 4096 entries because callers choose the keys), and the existing verified-identity cache. Six retries with the same bad token now reach GitLab once.

A throttled GitLab was reported as an invalid token. A 429 was wrapped in ErrInvalidToken, so a well-behaved MCP client did the worst possible thing:discarded a perfectly good credential and started a fresh authorization flow — more upstream traffic at the exact moment the instance asked for less, and a user asked to re-approve an application that was never at fault. Upstream conditions now answer 503, carry GitLab's own Retry-After, and are never cached as a rejection.

Rejections say what they are. RFC 6750 error codes distinguish reauthorize from ask for more scope from just retry, in the JSON-RPC shape the rest of the endpoint uses rather than plain text — which a client is told by the specification to read as an initialization-era server, turning a missing header into a false protocol diagnosis.

Least-privilege scope. A read-only deployment asks for read_api instead of api, and api is accepted where read_api is required, since it is a strict superset. Real introspection had turned the old hardcoded api into a lockout for read_api tokens.

Fixes you would only notice in the logs

Every HTTP legacy request logged no user at all. Identity was read from a field only the SDK's bearer middleware can populate, and legacy mode does not mount it, so the zero identity resolved every time. The pool now resolves the user once when it builds an entry — one call per entry, not per request.

Bearer is matched case-insensitively, per RFC 9110. The SDK verified the token, spending an API call, and the gate then refused it.

The SEP-2243 header annotation was doubled. The SDK prepends Mcp-Param- on both the sending and validating side, so declaring the full name put Mcp-Param-Mcp-Param-Action on the wire and made the documented header a mismatch.

PUBLIC_URL and TRUSTED_ORIGINS are read in HTTP mode. Both were documented and neither was consulted, so a compose deployment configured entirely through the environment could never enable OAuth.

Concurrent clients sharing one credential build one entry, not one each. A normal startup burst of twelve connections cost twenty-four upstream probes for a single token; the waiters now block exactly as long as they would have anyway, and the upstream cost is one.

A pooled entry is never cached for a server that was not built. A factory reporting success while returning nothing was inserted, then rejected on the way out — so the caller who triggered it got an error and every later caller for that credential got a nil server with a nil error. Not reachable with the current factory; the contract was wrong regardless.

Tests

make test-e2e-http — 125 cases, about three minutes, no GitLab and no credentials. The existing e2e suite drives tools through an in-memory transport, which answers "does this tool work against GitLab" and none of "does this endpoint behave". Cross-origin decisions, preflight, auth modes, rate limiting, rejection shapes and every limiting flag live in a handler chain no test could import. Four bugs shipped through that gap in a row, each found by hand after the previous fix looked complete. It now runs on every pull request, and four of the fixes above are its findings.

It includes a real nginx in Docker, because that is where a whole class of failure lives — a proxy that answers OPTIONS itself hides a server that cannot, which is exactly how the broken preflight shipped.

Unit coverage of internal/ is at 99.85%, up from 99.65%. What was missing were whole behaviours, not stray lines:the entire multi round-trip branch of URL elicitation, the guard between a work-item update and the two lists GitLab replaces wholesale, and approver-filter validation on three of the four merge-request list surfaces — where GitLab silently drops a filter it cannot parse and returns a confidently unfiltered result.

Upgrading

Drop-in for stdio. For HTTP behind a proxy, read the warning at the top — it is the one change this release requires you to make yourself.

Installer type: portable

x640E8B5E5AC1343CB68AC565A56538654EE59541A827BB3F20A25C04AD525B4814
arm647EB27DD48CC55911D8E30CC4D633157AE850B1805496EE73648232A645547C2A

Details

Homepage
https://github.com/jmrplens/gitlab-mcp-server
License
MIT
Publisher
José M. Requena Plens
Support
https://github.com/jmrplens/gitlab-mcp-server/issues
Privacy Policy
https://github.com/jmrplens/gitlab-mcp-server/blob/main/PRIVACY.md
Moniker
gitlab-mcp-server

Tags

aici-cdclaudedevopsgitlabgraphqlllmmcpmodel-context-protocolrest-api

Older versions (12)

2.7.4
x647097CDE23FEEA6C81961EB71CA7FD957522A61AF5116006BCBC277FD8A7DC8EB
arm6406633810A809C7144680F3D8FBA9F2598383E42A8091F05D2D9C91D8F5A43E08
2.7.3
x6407734C2578924CE407CCB72996CBE58534A106C2427FDE08E049E86AEB964AE8
arm642CC6BE9F5C8E429C1181BC5931E4E2C2D7F62EE3D5A4E8E24BE082E9659F7BF5
2.7.1
x6475FE9602437DF6CA7F029D1ABBF33F6035E3E9FA806748AFFE7CF835F2410C62
arm64C753537E2420AD164E6C1D3A6D601D72BE431D7964C63DE8E2BE51D678EA7286
2.7.0
x646F1715E8EA5B3E96A5896F5A8A340242704218B777711C8E42DC7797ACAD6B28
arm644A03D2B91824FAF07519CC97243B7371AADB994E036FF4EE2BBF8518BB8DE425
2.6.6
x64444B6ED7C159BC4CC6AF0007F1A96C4BF254BBBF5CCCCE4E56421FBCC6F447D7
arm64AF74ACC8164A8D60834A323E3A7F2870063FCDFD51D2E237F8A330E312BB01BE
2.6.5
x648FCB712A93C04948F87C6CBFC73F1BE70117CC38D34B18E4866B4C27DBD60989
arm64FEF73AC3532CEE3A6D2DE6F0409620DDBC480BD79F2A404F5A794FCB62FDFB5D
2.6.4
x64339B0EC72A4AB48FAF0899757846345BCFDD695610BEA6D9CFEC5D886F7B808B
arm648D1DE3C10631113F555E6F6AF81775D0D235A364704B228C420AFD2D528F57EA
2.6.1
x64C45CE18FF81ECD7D71AFF1B63CDF27FBA9628EEBEB9D31E74B257F470A1C4025
arm646651FCADFCAC14681FD360B930D6FD06C53157EE810A59C81D9867517632B095
2.6.0
x6495E04E5CCCE5A8577164275AB0FA08C97FB402CFB18939A69B8A824917F1A9D7
arm6475CEF96419383DE22DC6FA8534DA220E96334F0F20E4562C58B5D062A59F8C0A
2.5.3
x640B05DD3DB77380039EDB10D02F6A7F890E656EB2EF2897F1473DD1477BC3DDF3
arm64CE028B412ACEFD29A7BB340B1130DB099CBE7531A98C5153CF402D9CB1E3E8C6
2.5.2
x6464795B71C4749F40CAA513DC1D040CFF97119C55592FBF4C12AA484050D808AF
arm64301A75FADDE1D63EF2837E97FB769C51EA31B3EE0BC94D0A5FC52E2A533C0832
2.5.0
x64F802F8197535782F1A7357F5DF42117766ADCFF29EC94973D3E9B70389ECB042
arm646DA31DA26F9BD95A42B0AFA2C214903342447794A1226F5F77133A18C8FB12A2