Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ GitLab은 [공식 MCP 서버](https://docs.gitlab.com/user/model_context_protoco

**로컬/데스크톱 사용**(가장 일반적):

1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN`) — 가장 단순한 설정
1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN` 또는 `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`) — 가장 단순한 설정
2. **OAuth2 — 로컬 브라우저** (`GITLAB_USE_OAUTH`) — 더 나은 보안을 위해 권장

**서버/원격 배포**:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The server supports four authentication methods:

**For local/desktop use** (most common):

1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN`) — simplest setup
1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN` or `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`) — simplest setup
2. **OAuth2 — Local Browser** (`GITLAB_USE_OAUTH`) — recommended for better security

**For server/remote deployments**:
Expand Down
2 changes: 1 addition & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ O servidor suporta quatro métodos de autenticação.

**Para uso local/desktop** (mais comum):

1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN`) — configuração mais simples
1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN` ou `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`) — configuração mais simples
2. **OAuth2 — navegador local** (`GITLAB_USE_OAUTH`) — recomendado para maior segurança

**Para implantações em servidor/remotas**:
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ GitLab 还提供[官方 MCP 服务器](https://docs.gitlab.com/user/model_contex

**本地/桌面使用**(最常见):

1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN`) — 最简单的设置
1. **Personal Access Token** (`GITLAB_PERSONAL_ACCESS_TOKEN` 或 `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`) — 最简单的设置
2. **OAuth2 — 本地浏览器** (`GITLAB_USE_OAUTH`) — 推荐用于更好的安全性

**服务器/远程部署**:
Expand Down
20 changes: 19 additions & 1 deletion config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { parseCliArgs } from "./cli-boolean-flags.js";
import { deprecatedEnvInputFromSources, type DeprecatedEnvInput } from "./deprecated-env.js";
import {
readPersonalAccessTokenFile,
resolvePersonalAccessToken,
} from "./personal-access-token.js";
import { parseToolProfile } from "./tools/tool-profile.js";
import { parseCompactToolAllowlist } from "./utils/compact-tool-allowlist.js";
import { createLogger, flushStartupLogs } from "./utils/logger.js";

// Parse CLI arguments before anything else
export const cliArgs: Record<string, string> = parseCliArgs(process.argv.slice(2));
Expand All @@ -21,7 +26,20 @@ export function getConfig(
// Authentication
// ---------------------------------------------------------------------------

export const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
function warnIgnoredPersonalAccessTokenFile(message: string): void {
const startupLogger = createLogger();
startupLogger.warn(message);
flushStartupLogs(startupLogger);
}

// Read once at module load. Later auth checks use this constant and do not reopen the file.
export const GITLAB_PERSONAL_ACCESS_TOKEN = resolvePersonalAccessToken({
cliToken: cliArgs.token,
envToken: process.env.GITLAB_PERSONAL_ACCESS_TOKEN,
filePath: process.env.GITLAB_PERSONAL_ACCESS_TOKEN_FILE,
readFile: readPersonalAccessTokenFile,
warn: warnIgnoredPersonalAccessTokenFile,
});
export const GITLAB_JOB_TOKEN = getConfig("job-token", "GITLAB_JOB_TOKEN");
export const GITLAB_AUTH_COOKIE_PATH = getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
export const USE_OAUTH = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
Expand Down
16 changes: 15 additions & 1 deletion docs/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ as outside the masking boundary.

Default static GitLab Personal Access Token used in standard mode.

A non-empty value overrides `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`.

### `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`

Optional path to a file that holds the personal access token.

The server reads the file once during startup. It trims leading and trailing whitespace, including newlines. It does not read the file again on later requests.

If `GITLAB_PERSONAL_ACCESS_TOKEN` or `--token` is non-empty, that inline value wins. The server ignores this file and writes a warning to stderr. It does not open the file.

If the inline value is empty and this variable is set, startup stops when the file is missing, cannot be read, or is empty after trim. The error includes the path and a cause code such as `ENOENT`, `EACCES`, or `EMPTY`. The error does not include the file contents.

This variable only fills the personal access token. It does not change the precedence of `GITLAB_JOB_TOKEN`, OAuth, `GITLAB_AUTH_COOKIE_PATH`, or remote authorization.

### `GITLAB_IS_OLD`

Optional. Set to `true` (or `--is-old=true`) to send Personal Access Tokens as
Expand Down Expand Up @@ -962,7 +976,7 @@ Maximum GitLab client pool size.

## Simple local PAT setup

- `GITLAB_PERSONAL_ACCESS_TOKEN`
- `GITLAB_PERSONAL_ACCESS_TOKEN` or `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`
- `GITLAB_API_URL`

## Local OAuth setup
Expand Down
2 changes: 2 additions & 0 deletions docs/getting-started/cli-without-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ read -rs GITLAB_PERSONAL_ACCESS_TOKEN && export GITLAB_PERSONAL_ACCESS_TOKEN
export GITLAB_API_URL=https://gitlab.example.com/api/v4 # omit for gitlab.com
```

`GITLAB_PERSONAL_ACCESS_TOKEN_FILE` can point at a token file instead. The process reads that file once at startup and trims surrounding whitespace. If `GITLAB_PERSONAL_ACCESS_TOKEN` is already non-empty, the file is ignored and a warning goes to stderr.

3. Verify:

```bash
Expand Down
14 changes: 7 additions & 7 deletions docs/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ This page helps you pick the right **authentication method** and the right

The server supports four ways to authenticate against GitLab.

| Method | When to use it | Config |
| -------------------------- | ------------------------------------------------------------------ | ------------------------------ |
| **Personal Access Token** | Simplest setup for local/desktop use. | `GITLAB_PERSONAL_ACCESS_TOKEN` |
| **OAuth2 — Local Browser** | Better security for local clients (Claude Desktop, VS Code). | `GITLAB_USE_OAUTH=true` |
| **OAuth2 — MCP Proxy** | Remote MCP clients such as Claude.ai. | `GITLAB_MCP_OAUTH=true` |
| **Remote Authorization** | Multi-user deployments where each caller provides their own token. | `REMOTE_AUTHORIZATION=true` |
| Method | When to use it | Config |
| -------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------- |
| **Personal Access Token** | Simplest setup for local/desktop use. | `GITLAB_PERSONAL_ACCESS_TOKEN` or `GITLAB_PERSONAL_ACCESS_TOKEN_FILE` |
| **OAuth2 — Local Browser** | Better security for local clients (Claude Desktop, VS Code). | `GITLAB_USE_OAUTH=true` |
| **OAuth2 — MCP Proxy** | Remote MCP clients such as Claude.ai. | `GITLAB_MCP_OAUTH=true` |
| **Remote Authorization** | Multi-user deployments where each caller provides their own token. | `REMOTE_AUTHORIZATION=true` |

### Decision flow

Expand All @@ -21,7 +21,7 @@ The server supports four ways to authenticate against GitLab.
- **Local desktop, single user** → Personal Access Token or OAuth Local.
- **Remote single tenant (e.g., Claude.ai)** → OAuth MCP Proxy.
- **Remote multi-tenant (each user has their own GitLab token)** → Remote Authorization.
- **Docker / containers** → Personal Access Token (OAuth needs a browser).
- **Docker / containers** → Personal Access Token, or `GITLAB_PERSONAL_ACCESS_TOKEN_FILE` when the token is mounted as a file (OAuth needs a browser).

Full guides:

Expand Down
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,8 @@ function validateConfiguration(): void {
// Validate auth configuration
const remoteAuth = getConfig("remote-auth", "REMOTE_AUTHORIZATION") === "true";
const useOAuth = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
const hasToken = !!getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
// getConfig() would ignore GITLAB_PERSONAL_ACCESS_TOKEN_FILE.
const hasToken = !!GITLAB_PERSONAL_ACCESS_TOKEN;
const hasJobToken = !!getConfig("job-token", "GITLAB_JOB_TOKEN");
const hasCookie = !!getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
const mcpOAuth = getConfig("mcp-oauth", "GITLAB_MCP_OAUTH") === "true";
Expand Down
93 changes: 93 additions & 0 deletions personal-access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { readFileSync } from "node:fs";

const EMPTY_FILE_CODE = "EMPTY";

export class PersonalAccessTokenFileError extends Error {
readonly code: string;
readonly filePath: string;

constructor(filePath: string, code: string) {
super(`Cannot load GITLAB_PERSONAL_ACCESS_TOKEN_FILE at ${filePath} (${code})`);
this.name = "PersonalAccessTokenFileError";
this.filePath = filePath;
this.code = code;
}
}

export interface PersonalAccessTokenSource {
readonly cliToken: string | undefined;
readonly envToken: string | undefined;
readonly filePath: string | undefined;
readonly readFile: (filePath: string) => string;
readonly warn: (message: string) => void;
}

function presentInlineToken(value: string | undefined): string | undefined {
if (value === undefined || value.length === 0) {
return undefined;
}
return value;
}

function configuredFilePath(filePath: string | undefined): string | undefined {
if (filePath === undefined) {
return undefined;
}
const trimmed = filePath.trim();
if (trimmed.length === 0) {
return undefined;
}
return trimmed;
}

function errnoCode(error: unknown): string {
if (typeof error !== "object" || error === null || !("code" in error)) {
return "EIO";
}
const code = error.code;
if (typeof code !== "string" || code.length === 0) {
return "EIO";
}
return code;
}

export function readPersonalAccessTokenFile(filePath: string): string {
return readFileSync(filePath, { encoding: "utf8" });
}

function ignoredFileWarning(filePath: string): string {
return `Ignoring GITLAB_PERSONAL_ACCESS_TOKEN_FILE at ${filePath} because an inline personal access token is set`;
}

function readTokenFile(source: PersonalAccessTokenSource, filePath: string): string {
try {
return source.readFile(filePath);
} catch (error: unknown) {
throw new PersonalAccessTokenFileError(filePath, errnoCode(error));
}
}

/**
* Resolve the personal access token once.
* `--token` wins, then a non-empty GITLAB_PERSONAL_ACCESS_TOKEN, then the file.
* Job tokens, OAuth, cookies, and remote authorization are not inputs.
*/
export function resolvePersonalAccessToken(source: PersonalAccessTokenSource): string | undefined {
const inline = presentInlineToken(source.cliToken) ?? presentInlineToken(source.envToken);
const filePath = configuredFilePath(source.filePath);
if (inline !== undefined) {
if (filePath !== undefined) {
source.warn(ignoredFileWarning(filePath));
}
return inline;
}
if (filePath === undefined) {
return undefined;
}

const token = readTokenFile(source, filePath).trim();
if (token.length === 0) {
throw new PersonalAccessTokenFileError(filePath, EMPTY_FILE_CODE);
}
return token;
}
2 changes: 2 additions & 0 deletions scripts/run-mock-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ run_mock_tests 4 \
-o -path 'test/deprecated-env.test.ts' \
-o -path 'test/oauth-scopes.test.ts' \
-o -path 'test/oauth-device-flow-tests.ts' \
-o -path 'test/personal-access-token-file.test.ts' \
-o -path 'test/cli/*.test.ts' \)

# Co-located unit tests for scripts/ helper modules — no MCP/mock server processes
Expand All @@ -61,6 +62,7 @@ run_mock_tests 1 \
! -path 'test/deprecated-env.test.ts' \
! -path 'test/oauth-scopes.test.ts' \
! -path 'test/oauth-device-flow-tests.ts' \
! -path 'test/personal-access-token-file.test.ts' \
! -path 'test/cli/*'

tsx test/oauth-tests.ts
9 changes: 8 additions & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@
"environmentVariables": [
{
"name": "GITLAB_PERSONAL_ACCESS_TOKEN",
"description": "GitLab personal access token for local stdio use. One of this token, GITLAB_JOB_TOKEN, GITLAB_AUTH_COOKIE_PATH, or GITLAB_USE_OAUTH=true must be set. Create a token with the GitLab scopes the tools need, such as api or read_api.",
"description": "GitLab personal access token for local stdio use. A non-empty value overrides GITLAB_PERSONAL_ACCESS_TOKEN_FILE. One of this token, GITLAB_PERSONAL_ACCESS_TOKEN_FILE, GITLAB_JOB_TOKEN, GITLAB_AUTH_COOKIE_PATH, or GITLAB_USE_OAUTH=true must be set. Create a token with the GitLab scopes the tools need, such as api or read_api.",
"isRequired": false,
"isSecret": true,
"format": "string"
},
{
"name": "GITLAB_PERSONAL_ACCESS_TOKEN_FILE",
"description": "Path to a file that contains the personal access token. Read once at startup and trimmed. Ignored when GITLAB_PERSONAL_ACCESS_TOKEN or --token is non-empty. A missing, unreadable, or empty file stops startup. Does not replace job token, OAuth, cookie, or remote authorization.",
"isRequired": false,
"isSecret": false,
"format": "filepath"
},
{
"name": "GITLAB_JOB_TOKEN",
"description": "Optional GitLab CI job token to use instead of a personal access token when running inside GitLab CI.",
Expand Down
2 changes: 2 additions & 0 deletions skills/gl-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ read -rs GITLAB_PERSONAL_ACCESS_TOKEN && export GITLAB_PERSONAL_ACCESS_TOKEN
export GITLAB_API_URL=https://gitlab.example.com/api/v4 # omit for gitlab.com
```

`GITLAB_PERSONAL_ACCESS_TOKEN_FILE` can point at a token file instead. The process reads it once at startup and trims surrounding whitespace. If `GITLAB_PERSONAL_ACCESS_TOKEN` is already non-empty, the file is ignored and a warning is written to stderr.

**B. OAuth device flow** (needs a GitLab OAuth application ID)

```bash
Expand Down
Loading
Loading