From 402f06847056903058a5bf5bed0b65d81e0c5757 Mon Sep 17 00:00:00 2001 From: mattweg <71153724+mattweg@users.noreply.github.com> Date: Sun, 15 Jun 2025 22:38:14 -0500 Subject: [PATCH] feat: add cookie-based authentication support for enterprise GitLab (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add cookie-based authentication support for GitLab instances 🍪 - Add GITLAB_AUTH_COOKIE_PATH environment variable support - Handle #HttpOnly_ prefix in cookie files properly - Enable redirect following when cookies are present - Maintain compatibility with existing token-based auth * chore: prepare fork for npm publishing as @mattweg/gitlab-mcp - Update package name to @mattweg/gitlab-mcp - Bump version to 1.0.63-fork.1 - Add attribution to original author zereight - Add deprecation notice referencing upstream PR #100 - Add repository and homepage URLs for fork * fix: remove duplicate documentation line - removed the duplicate GITLAB_AUTH_COOKIE_PATH from README.md * fix: move cookie header outside conditional block for universal auth support - Move cookie header setting outside if/else block to ensure it applies to both old (Private-Token) and new (Bearer) GitLab authentication - Fixes issue where cookies were only set for Bearer token auth - Maintains backward compatibility with existing authentication methods - Enables cookie-based authentication for all GitLab instance types Resolves authentication failures when using GITLAB_AUTH_COOKIE_PATH with GitLab instances that require cookie-based authentication. * 1.0.63 * fix: add support for macOS cookie format in auth cookie parsing 🍪 - Add fallback parsing for macOS cookie format - Handle cookie files with different structure than standard Netscape format - Maintain compatibility with existing Linux cookie parsing - Extract cookie name and value from space-separated format Resolves authentication failures when using GITLAB_AUTH_COOKIE_PATH on macOS systems. * 1.0.64 * chore: update version to 1.0.63-fork.3 * fix: implement proper cookie jar authentication for macOS - Replace static cookie string with fetch-cookie + tough-cookie - Add proper Netscape cookie format parsing with domain context - Enable automatic cookie handling during OAuth2 redirects - Fixes authentication issues on macOS with enterprise SSO * chore: update version to 1.0.63-fork.4 * feat: add cookie-based authentication support for enterprise GitLab instances Add support for Netscape cookie file authentication to enable access to enterprise GitLab instances that use SSO/OAuth2 redirects. - Add GITLAB_AUTH_COOKIE_PATH environment variable - Implement cookie jar with proper domain handling for redirects - Use conditional fetch assignment: cookie-enabled when path configured - Maintains backward compatibility: no cookies = original behavior - Zero changes to existing fetch() calls throughout codebase Enables authentication flows like: curl -L -b ~/.midway/cookie Useful for enterprise environments with federated authentication. * chore: update to fork version 1.0.63-fork.5 with cookie auth support * feat: add cookie-based authentication support for enterprise GitLab instances Add support for Netscape cookie file authentication to enable access to enterprise GitLab instances that use SSO/OAuth2 redirects. - Add GITLAB_AUTH_COOKIE_PATH environment variable - Implement cookie jar with proper domain handling for redirects - Use conditional fetch assignment: cookie-enabled when path configured - Maintains backward compatibility: no cookies = original behavior - Zero changes to existing fetch() calls throughout codebase Enables authentication flows like: curl -L -b ~/.midway/cookie Useful for enterprise environments with federated authentication. * feat: implement robust cookie-based authentication with hybrid parsing - Add support for Netscape cookie file format with #HttpOnly_ prefix handling - Implement hybrid approach using tough-cookie's parse() for robust cookie parsing - Add automatic session establishment for enterprise GitLab authentication - Support cookie file path via GITLAB_AUTH_COOKIE_PATH environment variable - Integrate with fetch-cookie for automatic redirect handling and session persistence - Ensure compatibility with Midway enterprise authentication flow This enables seamless authentication with enterprise GitLab instances that require cookie-based authentication while maintaining clean, maintainable code using widely-supported packages (tough-cookie + fetch-cookie). * chore: bump version to 1.0.63-fork.6 with ultra-clean cookie auth * fix: correct package name to @mattweg/gitlab-mcp for proper npx dependency resolution - Fix package name mismatch that prevented npx from installing dependencies - Bump version to 1.0.63-fork.7 - This resolves cookie authentication issues by ensuring fetch-cookie and tough-cookie are properly installed * Improve cookie authentication with robust session establishment * feat: add cookie-based authentication support This feature adds support for cookie-based authentication with GitLab instances by: - Adding a new GITLAB_AUTH_COOKIE_PATH environment variable to specify the path to a Netscape-format cookie file - Implementing a cookie jar parser that handles standard Netscape cookie format - Adding session establishment logic that checks for GitLab session cookies - Ensuring all API requests use the authenticated session This allows the MCP server to authenticate with GitLab instances that use cookie-based authentication, which is particularly useful for instances that require SSO or other authentication methods that don't support personal access tokens. --------- Co-authored-by: Moon (mattweg's AI assistant) Co-authored-by: Matt Weg --- README.md | 1 + index.ts | 92 ++++++++++++++++++++++++++++++++++++++++++++++- package-lock.json | 52 +++++++++++++++++++++++++-- package.json | 2 ++ 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d588cd..14d3859 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ $ sh scripts/image_push.sh docker_user_name - `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled. - `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled. - `USE_PIPELINE`: When set to 'true', enables the pipeline-related tools (list_pipelines, get_pipeline, list_pipeline_jobs, get_pipeline_job, get_pipeline_job_output, create_pipeline, retry_pipeline, cancel_pipeline). By default, pipeline features are disabled. +- `GITLAB_AUTH_COOKIE_PATH`: Path to an authentication cookie file for GitLab instances that require cookie-based authentication. When provided, the cookie will be included in all GitLab API requests. ## Tools 🛠️ diff --git a/index.ts b/index.ts index 3fe5a56..b92f3b8 100644 --- a/index.ts +++ b/index.ts @@ -4,7 +4,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; -import fetch from "node-fetch"; +import nodeFetch from "node-fetch"; +import fetchCookie from "fetch-cookie"; +import { CookieJar, parse as parseCookie } from "tough-cookie"; import { SocksProxyAgent } from "socks-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent"; import { HttpProxyAgent } from "http-proxy-agent"; @@ -203,6 +205,7 @@ const server = new Server( ); const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN; +const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH; const IS_OLD = process.env.GITLAB_IS_OLD === "true"; const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true"; const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true"; @@ -245,6 +248,88 @@ if (HTTPS_PROXY) { httpsAgent = httpsAgent || new HttpsAgent(sslOptions); httpAgent = httpAgent || new Agent(); +// Create cookie jar with clean Netscape file parsing +const createCookieJar = (): CookieJar | null => { + if (!GITLAB_AUTH_COOKIE_PATH) return null; + + try { + const cookiePath = GITLAB_AUTH_COOKIE_PATH.startsWith("~/") + ? path.join(process.env.HOME || "", GITLAB_AUTH_COOKIE_PATH.slice(2)) + : GITLAB_AUTH_COOKIE_PATH; + + const jar = new CookieJar(); + const cookieContent = fs.readFileSync(cookiePath, "utf8"); + + cookieContent.split("\n").forEach(line => { + // Handle #HttpOnly_ prefix + if (line.startsWith("#HttpOnly_")) { + line = line.slice(10); + } + // Skip comments and empty lines + if (line.startsWith("#") || !line.trim()) { + return; + } + + // Parse Netscape format: domain, flag, path, secure, expires, name, value + const parts = line.split("\t"); + if (parts.length >= 7) { + const [domain, , path, secure, expires, name, value] = parts; + + // Build cookie string in standard format + const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`; + + // Use tough-cookie's parse function for robust parsing + const cookie = parseCookie(cookieStr); + if (cookie) { + const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`; + jar.setCookieSync(cookie, url); + } + } + }); + + return jar; + } catch (error) { + console.error("Error loading cookie file:", error); + return null; + } +}; + +// Initialize cookie jar and fetch +const cookieJar = createCookieJar(); +const fetch = cookieJar ? fetchCookie(nodeFetch, cookieJar) : nodeFetch; + +// Ensure session is established for the current request +async function ensureSessionForRequest(): Promise { + if (!cookieJar || !GITLAB_AUTH_COOKIE_PATH) return; + + // Extract the base URL from GITLAB_API_URL + const apiUrl = new URL(GITLAB_API_URL); + const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`; + + // Check if we already have GitLab session cookies + const gitlabCookies = cookieJar.getCookiesSync(baseUrl); + const hasSessionCookie = gitlabCookies.some(cookie => + cookie.key === '_gitlab_session' || cookie.key === 'remember_user_token' + ); + + if (!hasSessionCookie) { + try { + // Establish session with a lightweight request + await fetch(`${GITLAB_API_URL}/user`, { + ...DEFAULT_FETCH_CONFIG, + redirect: 'follow' + }).catch(() => { + // Ignore errors - the important thing is that cookies get set during redirects + }); + + // Small delay to ensure cookies are fully processed + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + // Ignore session establishment errors + } + } +} + // Modify DEFAULT_HEADERS to include agent configuration const DEFAULT_HEADERS: Record = { Accept: "application/json", @@ -3112,6 +3197,11 @@ server.setRequestHandler(CallToolRequestSchema, async request => { if (!request.params.arguments) { throw new Error("Arguments are required"); } + + // Ensure session is established for every request if cookie authentication is enabled + if (GITLAB_AUTH_COOKIE_PATH) { + await ensureSessionForRequest(); + } switch (request.params.name) { case "fork_repository": { diff --git a/package-lock.json b/package-lock.json index 290dcf2..b4c60e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,24 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.60", + "version": "1.0.62", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zereight/mcp-gitlab", - "version": "1.0.60", + "version": "1.0.62", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", "@types/node-fetch": "^2.6.12", "express": "^5.1.0", + "fetch-cookie": "^3.1.0", "form-data": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "node-fetch": "^3.3.2", "socks-proxy-agent": "^8.0.5", + "tough-cookie": "^5.1.2", "zod-to-json-schema": "^3.23.5" }, "bin": { @@ -1709,6 +1711,16 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fetch-cookie": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.1.0.tgz", + "integrity": "sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^5.0.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2902,6 +2914,12 @@ "node": ">= 18" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3090,6 +3108,24 @@ "node": ">=8" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3112,6 +3148,18 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index 19fc7ce..5156c72 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,13 @@ "@modelcontextprotocol/sdk": "1.8.0", "@types/node-fetch": "^2.6.12", "express": "^5.1.0", + "fetch-cookie": "^3.1.0", "form-data": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "node-fetch": "^3.3.2", "socks-proxy-agent": "^8.0.5", + "tough-cookie": "^5.1.2", "zod-to-json-schema": "^3.23.5" }, "devDependencies": {