{"id":2262,"date":"2026-11-02T00:55:54","date_gmt":"2026-11-01T22:55:54","guid":{"rendered":"https:\/\/cln.io\/blog\/?p=2262"},"modified":"2026-11-02T00:55:56","modified_gmt":"2026-11-01T22:55:56","slug":"gitlab-mcp-server-in-librechat-gitlab-18-and-19-via-litellm-or-direct-with-authentik-sso","status":"publish","type":"post","link":"https:\/\/cln.io\/blog\/gitlab-mcp-server-in-librechat-gitlab-18-and-19-via-litellm-or-direct-with-authentik-sso\/","title":{"rendered":"GitLab MCP server in LibreChat: GitLab 18 and 19, via LiteLLM or direct, with Authentik SSO"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">GitLab ships its own MCP server. No sidecar container, no shared service token: each user connects with their own GitLab account, so the model only sees what that user can see in GitLab anyway. This guide wires it into <a href=\"https:\/\/www.librechat.ai\/\" rel=\"noopener\">LibreChat<\/a>, either through LiteLLM as an MCP gateway or straight to GitLab, and puts Authentik in front so users log in once.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Versions used:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>GitLab EE 18.11.7 (unlicensed, local) and 18.11.11 (licensed, prod)<\/li>\n<li>GitLab EE 19.5.1 (fresh install, unlicensed = Free, local)<\/li>\n<li>LiteLLM v1.93.0 (local) and 1.105.0 (prod)<\/li>\n<li>LibreChat v0.8.7 \/ v0.8.8<\/li>\n<li>Authentik (local; prod uses the same setup)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Everything below was run on those versions unless a section says otherwise.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"1-what-you-get\">1. What you get<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Users open the MCP panel in LibreChat, click GitLab, approve a GitLab consent page once, and their chats get GitLab tools: issues, merge requests, pipelines, search, work item notes. The token is the user&#8217;s own GitLab OAuth token with scope <code>mcp<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Via LiteLLM (the setup I run):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Browser \u2500\u2500&gt; LibreChat \u2500\u2500&gt; LiteLLM \/gitlab\/mcp \u2500\u2500&gt; GitLab \/api\/v4\/mcp\n               \u2502              \u2502\n               \u2502              \u2514\u2500 checks x-litellm-api-key (LibreChat's key),\n               \u2502                 forwards \"Authorization: Bearer &lt;user token&gt;\" untouched\n               \u2502\n               \u2514\u2500 OAuth popup \u2500\u2500&gt; GitLab \/oauth\/authorize \u2500\u2500&gt; consent \u2500\u2500&gt; token (scope mcp)\n                                       \u2502\n                                       \u2514\u2500 GitLab login via Authentik (SSO)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Direct, without LiteLLM:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Browser \u2500\u2500&gt; LibreChat \u2500\u2500&gt; GitLab \/api\/v4\/mcp\n               \u2502\n               \u2514\u2500 OAuth popup \u2500\u2500&gt; GitLab \/oauth\/authorize \u2500\u2500&gt; consent \u2500\u2500&gt; token (scope mcp)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The OAuth part is identical in both. LiteLLM only adds a gateway in the middle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"2-gitlab-18-x\">2. GitLab 18.x<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"2-1-enable-the-mcp-server\">2.1 Enable the MCP server<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The endpoint is <code>POST https:\/\/git.example\/api\/v4\/mcp<\/code> (streamable HTTP, JSON-RPC). GET isn&#8217;t supported.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Out of the box, authenticated calls get <code>404 Not Found<\/code>. Unauthenticated calls get <code>401<\/code> whether MCP is on or not (and 18.x sends no <code>WWW-Authenticate<\/code> header), so you need a token to tell the two states apart.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two instance settings have to be on:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>duo_features_enabled<\/code> (already <code>true<\/code> by default)<\/li>\n<li><code>instance_level_ai_beta_features_enabled<\/code> (<code>false<\/code> by default)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s no feature flag to flip on 18.6 and later. (18.3 to 18.5 had <code>mcp_server<\/code> and <code>oauth_dynamic_client_registration<\/code> flags, those are gone since 18.6.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Licensed (Premium\/Ultimate):<\/strong> Admin area \u2192 <strong>GitLab Duo<\/strong> \u2192 <strong>Change configuration<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>GitLab Duo availability<\/strong>: On by default<\/li>\n<li><strong>Feature preview<\/strong> \u2192 tick <strong>Turn on experiment and beta GitLab Duo features<\/strong><\/li>\n<li>Save<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;Agentic Chat, agents, and flows&#8221; and &#8220;Duo Core&#8221; can stay off. MCP doesn&#8217;t need them, and it doesn&#8217;t need an AI gateway either.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Free \/ unlicensed:<\/strong> there&#8217;s no GitLab Duo admin page. <code>\/admin\/gitlab_duo<\/code> is a 404 and the sidebar has no entry:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-01b-admin-sidebar-no-duo-entry.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1440\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-01b-admin-sidebar-no-duo-entry.png\" alt=\"Admin sidebar on GitLab 18.11 Free, no GitLab Duo entry\" class=\"wp-image-2256\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-01b-admin-sidebar-no-duo-entry.png 1440w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-01b-admin-sidebar-no-duo-entry-300x188.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-01b-admin-sidebar-no-duo-entry-1024x640.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-01b-admin-sidebar-no-duo-entry-768x480.png 768w\" sizes=\"auto, (max-width: 1440px) 100vw, 1440px\" \/><\/a><figcaption class=\"wp-element-caption\">Admin sidebar on GitLab 18.11 Free, no GitLab Duo entry<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Use the application settings API instead, with an admin personal access token that has the <code>api<\/code> scope:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl --request PUT \\\n  --header \"PRIVATE-TOKEN: &lt;admin PAT&gt;\" \\\n  \"https:\/\/git.example\/api\/v4\/application\/settings?duo_features_enabled=true&amp;instance_level_ai_beta_features_enabled=true\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That returns <code>200<\/code> with the settings JSON. The docs list MCP on 18.x as Premium\/Ultimate. It worked on my unlicensed 18.11.7 EE, YMMV.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"2-2-create-the-oauth-application\">2.2 Create the OAuth application<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On 18.x the <strong>Admin area \u2192 Applications \u2192 Add new application<\/strong> form doesn&#8217;t list the <code>mcp<\/code> scope:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-06-applications-new-scopes.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1440\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-06-applications-new-scopes.png\" alt=\"Add new application form on 18.11, no mcp scope\" class=\"wp-image-2260\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-06-applications-new-scopes.png 1440w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-06-applications-new-scopes-300x188.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-06-applications-new-scopes-1024x640.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-06-applications-new-scopes-768x480.png 768w\" sizes=\"auto, (max-width: 1440px) 100vw, 1440px\" \/><\/a><figcaption class=\"wp-element-caption\">Add new application form on 18.11, no mcp scope<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The REST API accepts it though. Create the app with an admin PAT:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl --request POST \\\n  --header \"PRIVATE-TOKEN: &lt;admin PAT&gt;\" \\\n  --data \"name=LibreChat MCP&amp;redirect_uri=https:\/\/librechat.example\/api\/mcp\/GitLab\/oauth\/callback&amp;scopes=mcp&amp;confidential=true\" \\\n  \"https:\/\/git.example\/api\/v4\/applications\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The response has <code>id<\/code>, <code>application_id<\/code>, <code>secret<\/code> and <code>scopes: [\"mcp\"]<\/code>. Copy <code>application_id<\/code> and <code>secret<\/code> now, the secret is only shown once.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The redirect URI format is <code>https:\/\/&lt;librechat host&gt;\/api\/mcp\/&lt;ServerName&gt;\/oauth\/callback<\/code>. <code>&lt;ServerName&gt;<\/code> is the key you&#8217;ll use under <code>mcpServers<\/code> in <code>librechat.yaml<\/code> (here <code>GitLab<\/code>), and it&#8217;s case-sensitive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After that the app shows up in Admin area \u2192 Applications like any other, with scope <code>mcp<\/code>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-07b-application-detail-mcp.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1440\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-07b-application-detail-mcp.png\" alt=\"Application detail on 18.11 with scope mcp\" class=\"wp-image-2261\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-07b-application-detail-mcp.png 1440w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-07b-application-detail-mcp-300x188.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-07b-application-detail-mcp-1024x640.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-07b-application-detail-mcp-768x480.png 768w\" sizes=\"auto, (max-width: 1440px) 100vw, 1440px\" \/><\/a><figcaption class=\"wp-element-caption\">Application detail on 18.11 with scope mcp<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"2-3-verify\">2.3 Verify<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You need a token with the <code>mcp<\/code> scope. The UI won&#8217;t give a personal access token that scope on 18.x, but the API will. As admin, look up the user id and mint one:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># find the user id\ncurl --header \"PRIVATE-TOKEN: &lt;admin PAT&gt;\" \"https:\/\/git.example\/api\/v4\/users?username=&lt;you&gt;\"\n\n# create a PAT with scope mcp for that user\ncurl --request POST \\\n  --header \"PRIVATE-TOKEN: &lt;admin PAT&gt;\" \\\n  --data \"name=mcp-test&amp;scopes[]=mcp&amp;expires_at=2026-12-31\" \\\n  \"https:\/\/git.example\/api\/v4\/users\/&lt;user id&gt;\/personal_access_tokens\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then call the endpoint:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl -s -X POST https:\/\/git.example\/api\/v4\/mcp \\\n  -H \"Authorization: Bearer &lt;PAT with scope mcp&gt;\" \\\n  -H 'Content-Type: application\/json' \\\n  -H 'Accept: application\/json, text\/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\/list\",\"params\":{}}'<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">How to read the answer on 18.x:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Token<\/th><th>MCP off<\/th><th>MCP on<\/th><\/tr><\/thead><tbody>\n<tr><td>none<\/td><td>401<\/td><td>401<\/td><\/tr>\n<tr><td>PAT with <code>api<\/code> scope<\/td><td>404<\/td><td>403 Forbidden<\/td><\/tr>\n<tr><td>PAT with <code>mcp<\/code> scope<\/td><td>404<\/td><td>200 + tool list<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">So a <code>403<\/code> with an <code>api<\/code> token is actually good news: MCP is on, the token just lacks the <code>mcp<\/code> scope. On 18.11 the tool list has 16 tools.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"3-gitlab-19-x\">3. GitLab 19.x<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All of this was checked on a fresh 19.5.1 EE install without a license (Free).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"3-1-enable-the-mcp-server\">3.1 Enable the MCP server<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s a normal admin setting now: Admin area \u2192 <strong>Settings<\/strong> \u2192 <strong>General<\/strong> \u2192 <strong>Visibility and access controls<\/strong> \u2192 heading <strong>MCP client access<\/strong> \u2192 checkbox <strong>Allow connection to GitLab<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08b-19x-admin-mcp-setting-on.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08b-19x-admin-mcp-setting-on.png\" alt=\"MCP client access setting on 19.5.1, enabled\" class=\"wp-image-2257\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08b-19x-admin-mcp-setting-on.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08b-19x-admin-mcp-setting-on-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08b-19x-admin-mcp-setting-on-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08b-19x-admin-mcp-setting-on-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">MCP client access setting on 19.5.1, enabled<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">On a fresh install it&#8217;s on by default. I didn&#8217;t test what an upgrade from 18.x does with it, so check it after upgrading. Unticked it looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08-19x-admin-mcp-setting-off.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08-19x-admin-mcp-setting-off.png\" alt=\"MCP client access setting on 19.5.1, disabled\" class=\"wp-image-2263\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08-19x-admin-mcp-setting-off.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08-19x-admin-mcp-setting-off-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08-19x-admin-mcp-setting-off-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-08-19x-admin-mcp-setting-off-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">MCP client access setting on 19.5.1, disabled<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Related setting: Admin area \u2192 Settings \u2192 General \u2192 <strong>Account and limit<\/strong> \u2192 <strong>Allow dynamic client registration<\/strong> (default on). Turning it off deletes all dynamically registered OAuth apps and revokes their tokens. Apps you create yourself, like the one below, aren&#8217;t affected. This guide doesn&#8217;t use dynamic registration (see section 8), so turning it off is fine if you prefer that.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"3-2-create-the-oauth-application\">3.2 Create the OAuth application<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On 19 the scope is in the UI. Admin area \u2192 <strong>Applications<\/strong> \u2192 <strong>Add new application<\/strong>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-form.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-form.png\" alt=\"Add new application form on 19.5.1\" class=\"wp-image-2264\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-form.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-form-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-form-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-form-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">Add new application form on 19.5.1<\/figcaption><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Name<\/strong>: <code>LibreChat MCP<\/code><\/li>\n<li><strong>Redirect URI<\/strong>: <code>https:\/\/librechat.example\/api\/mcp\/GitLab\/oauth\/callback<\/code><\/li>\n<li><strong>Confidential<\/strong>: ticked<\/li>\n<li><strong>Scopes<\/strong>: <code>mcp<\/code> (&#8220;Grants read-write access to remote Model Context Protocol (MCP) server for tools execution.&#8221;)<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-scopes.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-scopes.png\" alt=\"mcp and mcp_orbit scopes in the 19.5.1 application form\" class=\"wp-image-2258\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-scopes.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-scopes-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-scopes-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-09-19x-applications-new-scopes-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">mcp and mcp_orbit scopes in the 19.5.1 application form<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s also <code>mcp_orbit<\/code>. You don&#8217;t need it for this. Save, then copy the <strong>Application ID<\/strong> and <strong>Secret<\/strong>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-10-19x-application-detail-mcp.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-10-19x-application-detail-mcp.png\" alt=\"Application detail on 19.5.1 with scope mcp\" class=\"wp-image-2265\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-10-19x-application-detail-mcp.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-10-19x-application-detail-mcp-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-10-19x-application-detail-mcp-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-10-19x-application-detail-mcp-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">Application detail on 19.5.1 with scope mcp<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The API call from section 2.2 still works on 19 too, if you&#8217;d rather script it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"3-3-verify\">3.3 Verify<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tokens changed in 19. User settings \u2192 <strong>Access<\/strong> \u2192 <strong>Personal access tokens<\/strong> \u2192 <strong>Generate token<\/strong> now offers <strong>Fine-grained<\/strong> or <strong>Legacy<\/strong>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-11-19x-generate-token-types.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-11-19x-generate-token-types.png\" alt=\"Generate token dropdown on 19.5.1\" class=\"wp-image-2266\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-11-19x-generate-token-types.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-11-19x-generate-token-types-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-11-19x-generate-token-types-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-11-19x-generate-token-types-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">Generate token dropdown on 19.5.1<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Legacy tokens get <code>403<\/code> on <code>\/api\/v4\/mcp<\/code> whatever scope they have, <code>api<\/code> included. Use a fine-grained token: tab <strong>User<\/strong> \u2192 category <strong>Duo<\/strong> \u2192 resource <strong>MCP tool<\/strong> \u2192 permission <strong>Execute<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-12-19x-fine-grained-token-mcp-execute.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"900\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-12-19x-fine-grained-token-mcp-execute.png\" alt=\"Fine-grained token with MCP tool Execute\" class=\"wp-image-2259\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-12-19x-fine-grained-token-mcp-execute.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-12-19x-fine-grained-token-mcp-execute-300x211.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-12-19x-fine-grained-token-mcp-execute-1024x720.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/10\/gitlab-mcp-12-19x-fine-grained-token-mcp-execute-768x540.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/a><figcaption class=\"wp-element-caption\">Fine-grained token with MCP tool Execute<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That token works for MCP and nothing else (<code>\/api\/v4\/version<\/code> answers <code>403 insufficient_granular_scope<\/code>, for example). Run the same curl as in 2.3 with it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">How to read the answer on 19.5.1:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Token<\/th><th>Setting off<\/th><th>Setting on<\/th><\/tr><\/thead><tbody>\n<tr><td>none<\/td><td>401<\/td><td>401 + <code>WWW-Authenticate: Bearer realm=\"GitLab\", scope=\"mcp\", resource_metadata=...<\/code><\/td><\/tr>\n<tr><td>legacy PAT (<code>api<\/code>)<\/td><td>403 &#8220;MCP server disabled for this instance&#8221;<\/td><td>403 Forbidden<\/td><\/tr>\n<tr><td>fine-grained, MCP tool Execute<\/td><td>403 &#8220;MCP server disabled for this instance&#8221;<\/td><td>200 + tool list<\/td><\/tr>\n<tr><td>OAuth token, scope <code>mcp<\/code><\/td><td>403 &#8220;MCP server disabled for this instance&#8221;<\/td><td>200 + tool list<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">19.5.1 returned 36 tools. The discovery documents (<code>\/.well-known\/oauth-protected-resource\/api\/v4\/mcp<\/code> and <code>\/.well-known\/oauth-authorization-server<\/code>) answer <code>200<\/code> whether the setting is on or off, so don&#8217;t use them to check whether MCP is enabled.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"4-litellm\">4. LiteLLM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skip this section if you go direct (section 7).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">LiteLLM v1.93.0 or newer is needed for <code>oauth_delegate<\/code>. Add the server to <code>config.yaml<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">mcp_servers:\n  gitlab:\n    url: \"https:\/\/git.example\/api\/v4\/mcp\"\n    transport: \"http\"\n    description: \"GitLab, user OAuth\"\n    auth_type: \"oauth_delegate\"     # forward the caller's Authorization header as-is\n    allow_all_keys: true            # any LiteLLM key may use this server<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What <code>oauth_delegate<\/code> does: LiteLLM authenticates the caller (LibreChat) with a LiteLLM key in the <code>x-litellm-api-key<\/code> header, and forwards <code>Authorization: Bearer &lt;user's GitLab token&gt;<\/code> to GitLab untouched. LiteLLM never sees a GitLab client secret.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Things to know:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>LiteLLM only forwards <code>Authorization<\/code> upstream when <code>x-litellm-api-key<\/code> is also present. Without it, LiteLLM treats <code>Authorization<\/code> as its own credential.<\/li>\n<li>Don&#8217;t use <code>auth_type: oauth2<\/code> with <code>oauth2_flow: authorization_code<\/code>. That makes LiteLLM the OAuth client, which isn&#8217;t what we want here.<\/li>\n<li>If the container has <code>HTTPS_PROXY<\/code> set, add the GitLab host to <code>NO_PROXY<\/code>.<\/li>\n<li>The gateway path is <code>\/&lt;server name&gt;\/mcp<\/code>, so the <code>gitlab<\/code> key above becomes <code>\/gitlab\/mcp<\/code>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Restart LiteLLM (Docker Swarm here):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">docker service update --force litellm_server<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify. Without a GitLab token you should get a <code>401<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl -si -X POST https:\/\/litellm.example\/gitlab\/mcp \\\n  -H 'x-litellm-api-key: Bearer sk-...' \\\n  -H 'Content-Type: application\/json' \\\n  -H 'Accept: application\/json, text\/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\/list\",\"params\":{}}'<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">HTTP\/2 401\nwww-authenticate: Bearer resource_metadata=\"https:\/\/litellm.example\/.well-known\/oauth-protected-resource\/gitlab\/mcp\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That <code>401<\/code> means the gateway is wired up. Add <code>-H \"Authorization: Bearer &lt;GitLab token with scope mcp&gt;\"<\/code> to the same command and you get the tool list back.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"5-librechat-via-litellm\">5. LibreChat (via LiteLLM)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In <code>librechat.yaml<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">mcpSettings:\n  allowedDomains:              # mandatory once any MCP\/OAuth host resolves to a private IP\n    - \"litellm_server.llm-backend\"\n    - \"litellm.example\"\n    - \"git.example\"\n\nmcpServers:\n  GitLab:                      # must match the redirect URI you registered\n    startup: false\n    type: streamable-http\n    url: \"http:\/\/litellm_server.llm-backend:4000\/gitlab\/mcp\"\n    requiresOAuth: true        # startup: false needs this explicitly, or OAuth is disabled\n    headers:\n      x-litellm-api-key: \"Bearer sk-...\"\n      X-User-ID: \"{{LIBRECHAT_USER_ID}}\"\n      X-User-Email: \"{{LIBRECHAT_USER_EMAIL}}\"\n      X-User-Role: \"{{LIBRECHAT_USER_ROLE}}\"\n      X-Email-Verified: \"{{LIBRECHAT_USER_EMAILVERIFIED}}\"\n    oauth_headers:             # headers LibreChat sends on its OAuth probe to the MCP URL\n      x-litellm-api-key: \"Bearer sk-...\"\n    oauth:\n      authorization_url: \"https:\/\/git.example\/oauth\/authorize\"\n      token_url: \"https:\/\/git.example\/oauth\/token\"\n      client_id: \"&lt;application_id&gt;\"\n      client_secret: \"&lt;secret&gt;\"\n      scope: \"mcp\"\n      redirect_uri: \"https:\/\/librechat.example\/api\/mcp\/GitLab\/oauth\/callback\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notes on that block:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>litellm_server.llm-backend<\/code> is the LiteLLM service name on the Docker network LibreChat shares with it. LibreChat talks to LiteLLM internally, the browser only ever talks to GitLab for the OAuth popup.<\/li>\n<li><code>allowedDomains<\/code> is an allow-list. Once it&#8217;s set, every MCP and OAuth host has to be in it, Docker DNS names included.<\/li>\n<li><code>startup: false<\/code> means tools only load for users who connected. It also means you have to set <code>requiresOAuth: true<\/code> yourself.<\/li>\n<li><code>oauth_headers<\/code> makes LibreChat&#8217;s OAuth probe carry the LiteLLM key, otherwise LiteLLM answers the probe for itself (section 8).<\/li>\n<li>The <code>X-User-*<\/code> headers are optional. They pass the LibreChat user&#8217;s id, email and role along to LiteLLM.<\/li>\n<li>The <code>oauth:<\/code> block uses the app from section 2.2 or 3.2.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Restart LibreChat. The startup log should show:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">[MCP][GitLab] URL: http:\/\/litellm_server.llm-backend:4000\/gitlab\/mcp\n[MCP][GitLab] OAuth Required: true<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In the UI: open the <strong>MCP<\/strong> panel \u2192 <strong>GitLab<\/strong> \u2192 <strong>Continue with OAuth<\/strong>. A GitLab consent page opens (&#8220;LibreChat MCP is requesting access\u2026 Grants read-write access to MCP server&#8221;). Click <strong>Authorize<\/strong> and LibreChat shows &#8220;Authentication Successful \u00b7 Connected to: GitLab&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the model, anything with tool calling works. LibreChat talks to LiteLLM as a custom OpenAI-compatible endpoint, and I tested with a model routed through LiteLLM. Local models (Ollama, LM Studio, llama.cpp) plug in the same way, but I haven&#8217;t tested them in this setup. Small models tend to skip <code>search<\/code> and guess ids, so tell them in the agent prompt to search first.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"6-authentik-sso\">6. Authentik SSO<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Goal: the user logs into LibreChat with Authentik, and GitLab also logs in via Authentik with auto sign-in. When LibreChat opens the GitLab OAuth popup, the user already has a GitLab session and only sees the consent page. One password, then Authorize.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Authentik isn&#8217;t involved in the MCP token itself. That&#8217;s still GitLab&#8217;s own OAuth from the previous sections. Authentik just makes sure there&#8217;s a GitLab session when the popup opens.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"6-1-authentik\">6.1 Authentik<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create two <strong>OAuth2\/OpenID providers<\/strong>, each with an <strong>application<\/strong>:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>GitLab<\/th><th>LibreChat<\/th><\/tr><\/thead><tbody>\n<tr><td>Application slug<\/td><td><code>git-lab<\/code><\/td><td><code>librechat<\/code><\/td><\/tr>\n<tr><td>Client type<\/td><td>Confidential<\/td><td>Confidential<\/td><\/tr>\n<tr><td>Redirect URI<\/td><td><code>https:\/\/git.example\/users\/auth\/openid_connect\/callback<\/code><\/td><td><code>https:\/\/librechat.example\/oauth\/openid\/callback<\/code><\/td><\/tr>\n<tr><td>Scopes<\/td><td>openid, email, profile<\/td><td>openid, email, profile<\/td><\/tr>\n<tr><td>Issuer<\/td><td><code>https:\/\/auth.example\/application\/o\/git-lab\/<\/code><\/td><td><code>https:\/\/auth.example\/application\/o\/librechat\/<\/code><\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The trailing slash on the GitLab issuer matters. Optionally add a group <code>librechat-admins<\/code> if you want to map LibreChat admins from Authentik.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"6-2-gitlab\">6.2 GitLab<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In <code>gitlab.rb<\/code> (or <code>GITLAB_OMNIBUS_CONFIG<\/code>), this is the usual GitLab OpenID Connect setup:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">gitlab_rails['omniauth_enabled'] = true\ngitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect']\ngitlab_rails['omniauth_auto_link_user'] = ['openid_connect']\ngitlab_rails['omniauth_block_auto_created_users'] = false   # true = admin must approve first-time users\ngitlab_rails['omniauth_auto_sign_in_with_provider'] = 'openid_connect'\ngitlab_rails['omniauth_providers'] = [{\n  name: 'openid_connect',\n  label: 'Authentik',\n  args: {\n    name: 'openid_connect',\n    scope: ['openid', 'profile', 'email'],\n    response_type: 'code',\n    issuer: 'https:\/\/auth.example\/application\/o\/git-lab\/',\n    discovery: true,\n    client_auth_method: 'query',\n    uid_field: 'preferred_username',\n    pkce: true,\n    client_options: {\n      identifier: '&lt;authentik client id&gt;',\n      secret: '&lt;authentik client secret&gt;',\n      redirect_uri: 'https:\/\/git.example\/users\/auth\/openid_connect\/callback'\n    }\n  }\n}]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Reconfigure\/restart GitLab. With auto sign-in on, local accounts (root) need <code>https:\/\/git.example\/users\/sign_in?auto_sign_in=false<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"6-3-librechat\">6.3 LibreChat<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In LibreChat&#8217;s <code>.env<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">ALLOW_SOCIAL_LOGIN=true\nOPENID_ISSUER=https:\/\/auth.example\/application\/o\/librechat\/.well-known\/openid-configuration\nOPENID_CLIENT_ID=&lt;client id&gt;\nOPENID_CLIENT_SECRET=&lt;client secret&gt;\nOPENID_SESSION_SECRET=&lt;random&gt;\nOPENID_CALLBACK_URL=\/oauth\/openid\/callback\nOPENID_SCOPE=openid profile email\nOPENID_BUTTON_LABEL=Login with Authentik\nOPENID_GENERATE_NONCE=true\nOPENID_USE_END_SESSION_ENDPOINT=true\n# optional admin mapping\nOPENID_ADMIN_ROLE=librechat-admins\nOPENID_ADMIN_ROLE_PARAMETER_PATH=groups\nOPENID_ADMIN_ROLE_TOKEN_KIND=id<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">LibreChat registers its OIDC strategy at boot. If Authentik is down when LibreChat starts, SSO stays broken until you restart LibreChat.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"6-4-the-flow\">6.4 The flow<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Checked end to end in a browser locally: Authentik password once \u2192 LibreChat \u2192 MCP panel \u2192 GitLab \u2192 Connect \u2192 GitLab consent page already showing the user \u2192 Authorize \u2192 Connected, 16 tools (GitLab 18.11).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One catch: with <code>omniauth_block_auto_created_users = true<\/code>, a user who never logged into GitLab still gets the consent page, but nothing works until an admin approves their account. Have users log into GitLab once first, or leave it at <code>false<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"7-direct-gitlab-librechat-without-litellm\">7. Direct GitLab \u2192 LibreChat, without LiteLLM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Same mechanism, not run by me: I&#8217;ve only run the LiteLLM path, but the OAuth part is identical.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Changes compared to section 5:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>url<\/code> points straight at GitLab<\/li>\n<li>drop <code>x-litellm-api-key<\/code> from <code>headers<\/code><\/li>\n<li>drop the whole <code>oauth_headers:<\/code> block<\/li>\n<li>keep the <code>oauth:<\/code> block and <code>requiresOAuth: true<\/code><\/li>\n<li><code>allowedDomains<\/code> needs <code>git.example<\/code><\/li>\n<\/ul>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">mcpSettings:\n  allowedDomains:\n    - \"git.example\"\n\nmcpServers:\n  GitLab:\n    startup: false\n    type: streamable-http\n    url: \"https:\/\/git.example\/api\/v4\/mcp\"\n    requiresOAuth: true\n    oauth:\n      authorization_url: \"https:\/\/git.example\/oauth\/authorize\"\n      token_url: \"https:\/\/git.example\/oauth\/token\"\n      client_id: \"&lt;application_id&gt;\"\n      client_secret: \"&lt;secret&gt;\"\n      scope: \"mcp\"\n      redirect_uri: \"https:\/\/librechat.example\/api\/mcp\/GitLab\/oauth\/callback\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>resource<\/code> array issue from the next section doesn&#8217;t bite here either, because the <code>oauth:<\/code> block skips discovery.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What you lose without LiteLLM: one gateway key for LibreChat, a central list of MCP servers, and per-key access control in LiteLLM. For a single GitLab and a single LibreChat, direct is simpler.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"8-why-auto-discovery-dcr-doesn-t-work-today\">8. Why auto-discovery (DCR) doesn&#8217;t work today<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In theory LibreChat could discover GitLab&#8217;s OAuth server and register itself (dynamic client registration), and you wouldn&#8217;t need the <code>oauth:<\/code> block. Through LiteLLM it fails with:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Incompatible auth server: does not support dynamic client registration<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Checked with LibreChat 0.8.7, LiteLLM 1.93 and GitLab 18.11. Three causes:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>LibreChat&#8217;s discovery probe doesn&#8217;t send <code>x-litellm-api-key<\/code>, so LiteLLM answers for itself instead of for GitLab. <code>oauth_headers<\/code> fixes that part.<\/li>\n<li>GitLab 18.11&#8217;s <code>\/.well-known\/oauth-protected-resource\/api\/v4\/mcp<\/code> returns <code>resource<\/code> as a JSON array (gitlab-org\/gitlab#596356 and #589197). RFC 9728 wants a string. LiteLLM&#8217;s <code>oauth_delegate<\/code> passes it through as is, the MCP TypeScript SDK (1.29) expects a string, and LibreChat also wants it to equal the URL it called.<\/li>\n<li>LiteLLM&#8217;s own root metadata has no <code>registration_endpoint<\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Hence the pre-registered app plus the <code>oauth:<\/code> block. GitLab 19.5.1 already returns <code>resource<\/code> as a string (<code>{\"resource\":\"https:\/\/git.example\/api\/v4\/mcp\",\"authorization_servers\":[\"https:\/\/git.example\"],\"scopes_supported\":[\"mcp\"]}<\/code>), so on 19 the GitLab half of cause 2 is fixed. I haven&#8217;t re-run the discovery path against 19 through LiteLLM, so the <code>oauth:<\/code> block stays in this guide until that&#8217;s tested.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"9-troubleshooting\">9. Troubleshooting<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Symptom<\/th><th>Cause<\/th><th>Fix<\/th><\/tr><\/thead><tbody>\n<tr><td><code>\/api\/v4\/mcp<\/code> returns 404 with a token (18.x)<\/td><td>MCP not enabled<\/td><td>Turn on beta features (2.1)<\/td><\/tr>\n<tr><td>401 on <code>\/api\/v4\/mcp<\/code> no matter what you change<\/td><td>No token sent; unauthenticated is always 401<\/td><td>Test with a token<\/td><\/tr>\n<tr><td>403 Forbidden with an <code>api<\/code> PAT (18.x)<\/td><td>MCP is on, token lacks <code>mcp<\/code> scope<\/td><td>Use a PAT or OAuth token with scope <code>mcp<\/code> (2.3)<\/td><\/tr>\n<tr><td>403 Forbidden with any legacy PAT (19.x)<\/td><td>Legacy tokens aren&#8217;t accepted for MCP<\/td><td>Fine-grained token, Duo \u2192 MCP tool \u2192 Execute (3.3)<\/td><\/tr>\n<tr><td>403 &#8220;MCP server disabled for this instance&#8221; (19.x)<\/td><td>&#8220;Allow connection to GitLab&#8221; is off<\/td><td>Tick it under MCP client access (3.1)<\/td><\/tr>\n<tr><td>No <code>mcp<\/code> scope in the Applications form (18.x)<\/td><td>The form doesn&#8217;t list it on 18<\/td><td>Create the app via the REST API (2.2)<\/td><\/tr>\n<tr><td>GitLab says the redirect URI is invalid<\/td><td>Redirect URI doesn&#8217;t match the server key<\/td><td><code>https:\/\/librechat.example\/api\/mcp\/GitLab\/oauth\/callback<\/code>, same case as the key under <code>mcpServers<\/code><\/td><\/tr>\n<tr><td><code>Incompatible auth server: does not support dynamic client registration<\/code><\/td><td>Auto-discovery path (section 8)<\/td><td>Add the <code>oauth:<\/code> block and <code>oauth_headers<\/code><\/td><\/tr>\n<tr><td>No &#8220;Continue with OAuth&#8221; button for GitLab<\/td><td><code>startup: false<\/code> without <code>requiresOAuth: true<\/code><\/td><td>Set <code>requiresOAuth: true<\/code><\/td><\/tr>\n<tr><td>LibreChat refuses to connect to the MCP or OAuth host<\/td><td>Host missing from <code>allowedDomains<\/code><\/td><td>Add every MCP and OAuth host, Docker names included<\/td><\/tr>\n<tr><td>LiteLLM <code>\/gitlab\/mcp<\/code> rejects the GitLab token<\/td><td><code>x-litellm-api-key<\/code> missing, so LiteLLM reads <code>Authorization<\/code> as its own key<\/td><td>Send both headers (<code>headers<\/code> and <code>oauth_headers<\/code>)<\/td><\/tr>\n<tr><td>LiteLLM can&#8217;t reach GitLab<\/td><td>Proxy env in the container<\/td><td>Add the GitLab host to <code>NO_PROXY<\/code><\/td><\/tr>\n<tr><td>Consent page works, but tools fail for a new user<\/td><td><code>omniauth_block_auto_created_users = true<\/code>, account pending approval<\/td><td>Approve the user, or have them log into GitLab once first<\/td><\/tr>\n<tr><td>&#8220;Login with Authentik&#8221; broken after a reboot<\/td><td>Authentik was down when LibreChat started<\/td><td>Restart LibreChat<\/td><\/tr>\n<tr><td>Model guesses issue\/MR ids<\/td><td>Small model skips <code>search<\/code><\/td><td>Tell it to search first in the agent prompt<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"10-the-tools-you-get\">10. The tools you get<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab 18.11 returned 16 tools:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">get_mcp_server_version\nget_issue\ncreate_issue\nget_merge_request\nget_merge_request_commits\nget_merge_request_diffs\nget_merge_request_pipelines\nget_merge_request_conflicts\ncreate_merge_request\nget_pipeline_jobs\nmanage_pipeline\nsearch\nsearch_labels\nget_workitem_notes\ncreate_workitem_note\nget_saved_view_work_items<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In LibreChat they show up prefixed and suffixed with the server name, e.g. <code>gitlab-search_mcp_GitLab<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab 19.5.1 returned 36 tools:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">get_mcp_server_version  get_project  list_projects  list_groups  list_project_members  get_user\nget_work_item  list_work_items  save_work_item  link_work_items  get_work_item_types  save_note\nget_saved_view_work_items  search  semantic_search  search_labels\nget_merge_request  list_merge_requests  save_merge_request  accept_merge_request  save_merge_request_review\nget_commit  list_commits  add_commit  add_branch  list_branches  list_tags  list_releases\nget_repository_file  list_repository_tree  fork_repository\nget_pipeline  list_pipelines  save_pipeline  get_job  get_artifact_file<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Since the token belongs to the user, the tools act as that user in GitLab. &#x1f642;<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-elements-1 wp-block-paragraph\" style=\"color:#9ca3af;font-size:14px\">This post was written with the help of Claude, Anthropic&#8217;s AI assistant.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>GitLab ships its own MCP server. No sidecar container, no shared service token: each user connects with their own GitLab account, so the model only sees what that user can see in GitLab anyway. This [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2255,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-2262","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/2262","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/comments?post=2262"}],"version-history":[{"count":1,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/2262\/revisions"}],"predecessor-version":[{"id":2268,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/2262\/revisions\/2268"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media\/2255"}],"wp:attachment":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media?parent=2262"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/categories?post=2262"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/tags?post=2262"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}