From 83603b1daf47cd4983a2497209aad3cf59c4b2d1 Mon Sep 17 00:00:00 2001 From: Vitali Tatarintev Date: Wed, 22 Oct 2025 18:07:47 +0200 Subject: [PATCH] Fix PKCE validation blocking MCP token refresh PKCE is only required for authorization code exchange per RFC 7636, not for refresh token grants. This allows MCP clients to automatically refresh expired tokens without re-authentication. Changelog: fixed --- app/controllers/oauth/tokens_controller.rb | 2 + .../model_context_protocol/mcp_server.md | 45 +++++++++++++++++-- spec/requests/oauth/tokens_controller_spec.rb | 29 ++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/app/controllers/oauth/tokens_controller.rb b/app/controllers/oauth/tokens_controller.rb index 5133b9fc32e6db..8a8080c3893777 100644 --- a/app/controllers/oauth/tokens_controller.rb +++ b/app/controllers/oauth/tokens_controller.rb @@ -48,6 +48,8 @@ def append_info_to_payload(payload) def validate_pkce_for_dynamic_applications return unless server.client&.application&.dynamic? + # PKCE is only required for authorization_code grant, not refresh_token + return if params[:grant_type] == 'refresh_token' # rubocop:disable Rails/StrongParams -- Only accessing a single named param return unless params[:code_verifier].blank? # rubocop:disable Rails/StrongParams -- Only accessing a single named param render json: { diff --git a/doc/user/gitlab_duo/model_context_protocol/mcp_server.md b/doc/user/gitlab_duo/model_context_protocol/mcp_server.md index cbe9abd7fbf5ab..4a8c3bd748f005 100644 --- a/doc/user/gitlab_duo/model_context_protocol/mcp_server.md +++ b/doc/user/gitlab_duo/model_context_protocol/mcp_server.md @@ -59,20 +59,57 @@ For a click-through demo, see [Duo Agent Platform - MCP server](https://gitlab.n ## Connect Cursor to a GitLab MCP server +Cursor supports two connection methods: + +- **HTTP transport (Recommended)**: Direct connection without additional dependencies. +- **stdio transport with mcp-remote**: Connection through a proxy (requires Node.js). + +### HTTP transport (Recommended) + +To configure the GitLab MCP server using HTTP transport: + +1. Open Cursor. +1. In Cursor, go to **Settings** > **Cursor Settings** > **Tools & MCP**. +1. Under **Installed MCP Servers**, select **New MCP Server**. +1. Add this definition to the `mcpServers` key in the opened `mcp.json` file: + - Replace `` with: + - On GitLab Self-Managed, your GitLab instance URL. + - On GitLab.com, `gitlab.com`. + + ```json + { + "mcpServers": { + "GitLab": { + "type": "http", + "url": "https:///api/v4/mcp" + } + } + } + ``` + +1. Save the file and restart Cursor. +1. In Cursor, go to **Settings** > **Cursor Settings** > **Tools & MCP**. +1. Under **Installed MCP Servers**, find your GitLab server and click **Connect**. +1. Your browser opens for OAuth authorization. Review and approve the authorization request. + +You can now start a new chat and ask a question depending on the available tools. + +### stdio transport with mcp-remote + Prerequisites: - Install Node.js version 20 or later. -To configure the GitLab MCP server in Cursor: +To configure the GitLab MCP server using stdio transport: 1. Open Cursor. -1. In Cursor, go to **Settings** > **Cursor Settings** > **Tools & Integrations**. -1. Under **MCP Tools**, select `New MCP Server`. +1. In Cursor, go to **Settings** > **Cursor Settings** > **Tools & MCP**. +1. Under **Installed MCP Servers**, select **New MCP Server**. 1. Add this definition to the `mcpServers` key in the opened `mcp.json` file, editing as needed: - For the `"command":` parameter, if `npx` is installed locally instead of globally, provide the full path to `npx`. - Replace `` with: - On GitLab Self-Managed, your GitLab instance URL. - - On GitLab.com, `GitLab.com`. + - On GitLab.com, `gitlab.com`. ```json { diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb index 520025827c8286..8796ce34d00f66 100644 --- a/spec/requests/oauth/tokens_controller_spec.rb +++ b/spec/requests/oauth/tokens_controller_spec.rb @@ -312,6 +312,35 @@ def authenticate(with_password) expect(response.parsed_body['error_description']).not_to include('PKCE code_verifier is required') end end + + context 'when using refresh token grant' do + let_it_be(:dynamic_oauth_application) do + create(:oauth_application, dynamic: true, confidential: false, scopes: 'mcp') + end + + let_it_be(:oauth_token) do + create( + :oauth_access_token, + application: dynamic_oauth_application, + resource_owner_id: user.id, + use_refresh_token: true, + scopes: 'mcp' + ) + end + + it 'does not require PKCE code_verifier for refresh token flow' do + post('/oauth/token', params: { + grant_type: 'refresh_token', + refresh_token: oauth_token.plaintext_refresh_token, + client_id: dynamic_oauth_application.uid, + scopes: 'mcp' + }) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.parsed_body).to have_key('access_token') + expect(response.parsed_body['error']).to be_nil + end + end end context 'with non-dynamic OAuth application' do -- GitLab