@zereight/mcp-gitlab 2.3.6 → 2.3.7-next.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -314,6 +314,7 @@ MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH
314
314
  | `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS` | 선택 | 콜백 프록시 모드가 리다이렉트할 클라이언트 callback URL 목록 (기본값: Claude, ChatGPT, Cursor, 루프백 callback. 추가하려면 `defaults,<url>`) |
315
315
  | `GITLAB_OAUTH_SCOPES` | 선택 | 쉼표로 구분된 scope 목록(기본값: `api,read_api,read_user`) |
316
316
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | 선택 | 쉼표로 구분된 GitLab 그룹 전체 경로 — 해당 그룹 및 하위 그룹 멤버만 서버를 사용할 수 있음 (기존 `GITLAB_ALLOWED_GROUPS` 대체) |
317
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | 선택 | `true`면 refresh token을 발급하지 않음: 세션 TTL과 무관하게 GitLab access token이 만료될 때마다(2시간) 브라우저에서 다시 인가 |
317
318
 
318
319
  `STREAMABLE_HTTP=true`일 때 서버 측 GitLab 자격 증명(`GITLAB_PERSONAL_ACCESS_TOKEN`, `GITLAB_JOB_TOKEN`, `GITLAB_AUTH_COOKIE_PATH`, 또는 `GITLAB_USE_OAUTH`)은 `REMOTE_AUTHORIZATION=true`, `GITLAB_MCP_OAUTH=true`, 또는 `STREAMABLE_HTTP_AUTH_TOKEN`이 필요합니다.
319
320
 
@@ -418,6 +419,7 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
418
419
  - `GITLAB_OAUTH_CALLBACK_PROXY`
419
420
  - `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS`
420
421
  - `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR`
422
+ - `OAUTH_DISABLE_REFRESH_TOKEN`
421
423
  - `OAUTH_STATELESS_MODE`
422
424
  - `OAUTH_STATELESS_SECRET`
423
425
 
@@ -578,6 +580,7 @@ node build/index.js
578
580
  | `STREAMABLE_HTTP` | 예 | 반드시 `true`(SSE 미지원) |
579
581
  | `GITLAB_OAUTH_SCOPES` | 아니오 | 요청할 GitLab scope 목록(쉼표 구분). 기본값은 `api`이며, 유효 permission mode가 `readonly`일 때 `read_api`입니다. 사전 등록 애플리케이션에 해당 scope가 설정되어 있어야 합니다. |
580
582
  | `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR` | 아니오 | Dynamic Client Registration(`POST /register`)의 클라이언트 IP당 rolling 한도. 기본 `20`/시간, 범위 `1`–`1000`. IDE 창 여러 개 등으로 등록이 막히면 올리세요. GitLab API 한도와 무관합니다. |
583
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | 아니오 | `true`면 `/token` 응답에서 `refresh_token`을 빼고 refresh grant를 `invalid_grant`로 거부합니다. GitLab access token이 만료되면 브라우저에서 다시 인가해야 합니다. 기본 `false`. 콜백 프록시 모드에서는 이 서버의 흐름을 따르는 MCP 클라이언트가 GitLab refresh token을 받지 못합니다(passthrough에서는 클라이언트가 GitLab에서 직접 받을 수 있음). 표준 MCP 클라이언트를 위한 재인가 정책이며 보안 경계가 아닙니다. 켜기 전에 발급된 refresh token은 GitLab **User Settings → Applications → Authorized applications → Revoke**로 revoke할 때까지 유효합니다. |
581
584
  | `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | 아니오 | 로컬 HTTP 개발에서만 `true` |
582
585
 
583
586
  **중요 사항:**
package/README.md CHANGED
@@ -336,6 +336,7 @@ exchanging credentials with GitLab on behalf of the client.
336
336
  | `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS` | optional | Client callback URLs that callback proxy mode will redirect to (default: Claude, ChatGPT, Cursor and loopback callbacks; `defaults,<url>` to add one) |
337
337
  | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
338
338
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | optional | Comma-separated group full paths — only members (and subgroup members) may use the server (replaces deprecated `GITLAB_ALLOWED_GROUPS`) |
339
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | optional | Set to `true` to issue no refresh tokens: users re-authorize in the browser each time the GitLab access token expires (2 h), regardless of the session TTL |
339
340
 
340
341
  When `STREAMABLE_HTTP=true`, server-side GitLab credentials (`GITLAB_PERSONAL_ACCESS_TOKEN`, `GITLAB_JOB_TOKEN`, `GITLAB_AUTH_COOKIE_PATH`, or `GITLAB_USE_OAUTH`) require `REMOTE_AUTHORIZATION=true`, `GITLAB_MCP_OAUTH=true`, or `STREAMABLE_HTTP_AUTH_TOKEN`.
341
342
 
@@ -454,6 +455,7 @@ Commonly referenced variables:
454
455
  - `GITLAB_OAUTH_CALLBACK_PROXY`
455
456
  - `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS`
456
457
  - `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR`
458
+ - `OAUTH_DISABLE_REFRESH_TOKEN`
457
459
  - `OAUTH_STATELESS_MODE`
458
460
  - `OAUTH_STATELESS_SECRET`
459
461
 
@@ -620,6 +622,7 @@ No `headers` field is needed — Claude.ai obtains the token via OAuth automatic
620
622
  | `STREAMABLE_HTTP` | Yes | Must be `true` (SSE is not supported) |
621
623
  | `GITLAB_OAUTH_SCOPES` | No | Comma-separated GitLab scopes to request (e.g. `api,read_user`). Defaults to `api` (or `read_api` when the effective permission mode is `readonly`). The pre-registered application must be configured with at least these scopes. |
622
624
  | `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR` | No | Per-IP rolling limit for Dynamic Client Registration (`POST /register`). Default `20`/hour; range `1`–`1000`. Raise when clients (e.g. multiple IDE windows) hit registration throttling. Not a GitLab API limit. |
625
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | No | Set `true` to drop `refresh_token` from `/token` responses and reject the refresh grant with `invalid_grant`, so users re-authorize in the browser when the GitLab access token expires. Default `false`. In callback proxy mode, MCP clients that follow this server's flow never receive a GitLab refresh token (in passthrough the client can get one from GitLab directly). This is a re-authorization policy for standard MCP clients, not a security boundary. Refresh tokens issued before enabling stay valid until revoked in GitLab under **User Settings → Applications → Authorized applications → Revoke**. |
623
626
  | `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | No | Set `true` for local HTTP dev only |
624
627
 
625
628
  **Important Notes:**
package/README.pt-BR.md CHANGED
@@ -314,6 +314,7 @@ Use `GITLAB_OAUTH_REDIRECT_URI` somente quando o próprio servidor MCP receber o
314
314
  | `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS` | opcional | URLs de callback de cliente para as quais o modo de proxy de callback redireciona (padrão: callbacks do Claude, ChatGPT, Cursor e loopback; use `defaults,<url>` para adicionar) |
315
315
  | `GITLAB_OAUTH_SCOPES` | opcional | Scopes separados por vírgula (padrão: `api,read_api,read_user`) |
316
316
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | opcional | Caminhos completos de grupos GitLab separados por vírgula — apenas membros desses grupos e subgrupos podem usar o servidor (substitui o obsoleto `GITLAB_ALLOWED_GROUPS`) |
317
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | opcional | Defina como `true` para não emitir refresh tokens: o usuário autoriza de novo no navegador sempre que o access token do GitLab expira (2 h), independente do TTL da sessão |
317
318
 
318
319
  Quando `STREAMABLE_HTTP=true`, credenciais GitLab do lado do servidor (`GITLAB_PERSONAL_ACCESS_TOKEN`, `GITLAB_JOB_TOKEN`, `GITLAB_AUTH_COOKIE_PATH` ou `GITLAB_USE_OAUTH`) exigem `REMOTE_AUTHORIZATION=true`, `GITLAB_MCP_OAUTH=true` ou `STREAMABLE_HTTP_AUTH_TOKEN`.
319
320
 
@@ -418,6 +419,7 @@ Variáveis usadas com frequência:
418
419
  - `GITLAB_OAUTH_CALLBACK_PROXY`
419
420
  - `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS`
420
421
  - `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR`
422
+ - `OAUTH_DISABLE_REFRESH_TOKEN`
421
423
  - `OAUTH_STATELESS_MODE`
422
424
  - `OAUTH_STATELESS_SECRET`
423
425
 
@@ -578,6 +580,7 @@ Não é necessário definir `headers`. O Claude.ai obtém o token via OAuth.
578
580
  | `STREAMABLE_HTTP` | sim | Deve ser `true` (SSE não é suportado) |
579
581
  | `GITLAB_OAUTH_SCOPES` | não | Scopes do GitLab a solicitar, separados por vírgula. O padrão é `api`, ou `read_api` quando o permission mode efetivo é `readonly`. O aplicativo previamente registrado deve ter pelo menos esses scopes configurados. |
580
582
  | `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR` | não | Limite rolling de Dynamic Client Registration (`POST /register`) por IP do cliente. Padrão `20`/hora, intervalo `1`–`1000`. Aumente se múltiplas janelas de IDE estiverem atingindo o limite. Não está relacionado aos limites da API do GitLab. |
583
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | não | Defina `true` para remover `refresh_token` das respostas de `/token` e rejeitar o refresh grant com `invalid_grant`; o usuário autoriza de novo no navegador quando o access token do GitLab expira. Padrão `false`. No modo de proxy de callback, clientes MCP que seguem o fluxo deste servidor nunca recebem um refresh token do GitLab (em passthrough o cliente pode obter um direto do GitLab). É uma política de reautorização para clientes MCP padrão, não uma barreira de segurança. Refresh tokens emitidos antes de ativar continuam válidos até serem revogados no GitLab em **User Settings → Applications → Authorized applications → Revoke**. |
581
584
  | `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | não | Apenas para desenvolvimento local via HTTP |
582
585
 
583
586
  **Observações importantes:**
package/README.zh-CN.md CHANGED
@@ -313,6 +313,7 @@ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送
313
313
  | `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS` | 可选 | 回调代理模式允许重定向的客户端 callback URL 列表(默认:Claude、ChatGPT、Cursor 和环回地址 callback;用 `defaults,<url>` 追加) |
314
314
  | `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
315
315
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | 可选 | 逗号分隔的 GitLab 群组完整路径 — 仅该群组及其子群组的成员可使用服务器(替代已废弃的 `GITLAB_ALLOWED_GROUPS`) |
316
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | 可选 | 设置为 `true` 时不发放 refresh token:每当 GitLab access token 过期(2 小时)用户都需在浏览器中重新授权,与会话 TTL 无关 |
316
317
 
317
318
  当 `STREAMABLE_HTTP=true` 时,服务端 GitLab 凭据(`GITLAB_PERSONAL_ACCESS_TOKEN`、`GITLAB_JOB_TOKEN`、`GITLAB_AUTH_COOKIE_PATH` 或 `GITLAB_USE_OAUTH`)需要 `REMOTE_AUTHORIZATION=true`、`GITLAB_MCP_OAUTH=true` 或 `STREAMABLE_HTTP_AUTH_TOKEN`。
318
319
 
@@ -417,6 +418,7 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
417
418
  - `GITLAB_OAUTH_CALLBACK_PROXY`
418
419
  - `GITLAB_OAUTH_ALLOWED_REDIRECT_URIS`
419
420
  - `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR`
421
+ - `OAUTH_DISABLE_REFRESH_TOKEN`
420
422
  - `OAUTH_STATELESS_MODE`
421
423
  - `OAUTH_STATELESS_SECRET`
422
424
 
@@ -577,6 +579,7 @@ node build/index.js
577
579
  | `STREAMABLE_HTTP` | 是 | 必须为 `true`(不支持 SSE) |
578
580
  | `GITLAB_OAUTH_SCOPES` | 否 | 要请求的 GitLab scopes,以逗号分隔。默认值为 `api`;有效 permission mode 为 `readonly` 时为 `read_api`。预注册应用必须配置至少这些 scopes。 |
579
581
  | `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR` | 否 | Dynamic Client Registration(`POST /register`)的每客户端 IP rolling 限制。默认 `20`/小时,范围 `1`–`1000`。多个 IDE 窗口等导致注册被限流时可调高。与 GitLab API 限额无关。 |
582
+ | `OAUTH_DISABLE_REFRESH_TOKEN` | 否 | 设置 `true` 时 `/token` 响应不含 `refresh_token`,refresh grant 以 `invalid_grant` 拒绝;GitLab access token 过期后用户需在浏览器中重新授权。默认 `false`。回调代理模式下,遵循本服务器流程的 MCP 客户端拿不到 GitLab refresh token(passthrough 模式下客户端可直接从 GitLab 获取)。这是面向标准 MCP 客户端的重新授权策略,而非安全边界。启用前已发放的 refresh token 在 GitLab **User Settings → Applications → Authorized applications → Revoke** 撤销前仍然有效。 |
580
583
  | `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | 否 | 仅用于本地 HTTP 开发 |
581
584
 
582
585
  **重要说明:**
package/build/cli/args.js CHANGED
@@ -48,6 +48,7 @@ const GLOBAL_FLAG_NAMES = new Set([
48
48
  "oauth-allowed-redirect-uris",
49
49
  "allowed-groups",
50
50
  "oauth-allowed-groups",
51
+ "oauth-disable-refresh-token",
51
52
  "enable-dynamic-api-url",
52
53
  "enable-dynamic-project-scope",
53
54
  "enable-strict-project-scope",
@@ -17,6 +17,7 @@ export const BOOLEAN_CLI_FLAG_NAMES = new Set([
17
17
  "allow-unauthenticated-tool-discovery",
18
18
  "mcp-trust-proxy",
19
19
  "oauth-callback-proxy",
20
+ "oauth-disable-refresh-token",
20
21
  "enable-dynamic-api-url",
21
22
  "enable-dynamic-project-scope",
22
23
  "enable-strict-project-scope",
package/build/config.js CHANGED
@@ -111,6 +111,12 @@ export const GITLAB_OAUTH_ALLOWED_GROUPS = (() => {
111
111
  .filter(Boolean);
112
112
  return groups.length > 0 ? groups : undefined;
113
113
  })();
114
+ /**
115
+ * MCP OAuth proxy: reject the refresh_token grant (invalid_grant) and omit
116
+ * refresh_token from issued tokens, so clients must redo the browser
117
+ * authorization once the GitLab access token expires.
118
+ */
119
+ export const OAUTH_DISABLE_REFRESH_TOKEN = getConfig("oauth-disable-refresh-token", "OAUTH_DISABLE_REFRESH_TOKEN") === "true";
114
120
  export const ENABLE_DYNAMIC_API_URL = getConfig("enable-dynamic-api-url", "ENABLE_DYNAMIC_API_URL") === "true";
115
121
  export const ENABLE_DYNAMIC_PROJECT_SCOPE = getConfig("enable-dynamic-project-scope", "ENABLE_DYNAMIC_PROJECT_SCOPE") === "true";
116
122
  export const ENABLE_STRICT_PROJECT_SCOPE = getConfig("enable-strict-project-scope", "ENABLE_STRICT_PROJECT_SCOPE") === "true";
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { cliArgs, getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_MCP_REQUEST_BODY_LIMIT, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_OAUTH_ALLOWED_REDIRECT_URIS_RAW, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, GITLAB_MCP_COMPACT_TOOL_NAMES, GITLAB_TOOL_PROFILE, GITLAB_DOWNLOAD_DIR, buildDeprecatedEnvInput, } from "./config.js";
2
+ import { cliArgs, getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_MCP_REQUEST_BODY_LIMIT, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_OAUTH_ALLOWED_REDIRECT_URIS_RAW, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_DISABLE_REFRESH_TOKEN, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, GITLAB_MCP_COMPACT_TOOL_NAMES, GITLAB_TOOL_PROFILE, GITLAB_DOWNLOAD_DIR, buildDeprecatedEnvInput, } from "./config.js";
3
3
  import { getDeprecatedEnvWarnings } from "./deprecated-env.js";
4
4
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
5
5
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
@@ -11664,7 +11664,7 @@ async function startSSEServer() {
11664
11664
  * Start server with Streamable HTTP transport
11665
11665
  */
11666
11666
  async function startStreamableHTTPServer() {
11667
- const { express, rateLimit, ipKeyGenerator, StreamableHTTPServerTransport, mcpAuthRouter, requireBearerAuth, } = await loadRemoteHttpStack();
11667
+ const { express, rateLimit, ipKeyGenerator, StreamableHTTPServerTransport, mcpAuthRouter, createOAuthMetadata, metadataHandler, requireBearerAuth, } = await loadRemoteHttpStack();
11668
11668
  const app = express();
11669
11669
  // Session IDs are untrusted map keys. A null prototype prevents inherited
11670
11670
  // names such as "constructor" from masquerading as live transports.
@@ -12214,8 +12214,11 @@ async function startStreamableHTTPServer() {
12214
12214
  storedTtlSeconds: OAUTH_STATELESS_STORED_TTL_SECONDS,
12215
12215
  }
12216
12216
  : null;
12217
- const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_PERMISSION_MODE === "readonly", GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions, parseAllowedRedirectUris(GITLAB_OAUTH_ALLOWED_REDIRECT_URIS_RAW));
12217
+ const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_PERMISSION_MODE === "readonly", GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions, parseAllowedRedirectUris(GITLAB_OAUTH_ALLOWED_REDIRECT_URIS_RAW), OAUTH_DISABLE_REFRESH_TOKEN);
12218
12218
  const scopesSupported = GITLAB_OAUTH_SCOPES ?? ["api", "read_api", "read_user"];
12219
+ const grantTypesSupported = OAUTH_DISABLE_REFRESH_TOKEN
12220
+ ? ["authorization_code"]
12221
+ : ["authorization_code", "refresh_token"];
12219
12222
  // When server URL has a path (e.g. behind Kong), the SDK's well-known metadata
12220
12223
  // advertises root-level endpoints. Override to use path-prefixed endpoints.
12221
12224
  const issuerPath = issuerUrl.pathname.replace(/\/$/, "");
@@ -12230,7 +12233,7 @@ async function startStreamableHTTPServer() {
12230
12233
  response_types_supported: ["code"],
12231
12234
  code_challenge_methods_supported: ["S256"],
12232
12235
  token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
12233
- grant_types_supported: ["authorization_code", "refresh_token"],
12236
+ grant_types_supported: grantTypesSupported,
12234
12237
  scopes_supported: scopesSupported,
12235
12238
  revocation_endpoint_auth_methods_supported: ["client_secret_post"],
12236
12239
  };
@@ -12272,7 +12275,21 @@ async function startStreamableHTTPServer() {
12272
12275
  });
12273
12276
  });
12274
12277
  }
12275
- // Mounts /.well-known/oauth-authorization-server (shadowed above when basePath set),
12278
+ // The SDK's root metadata always lists refresh_token; shadow it with the
12279
+ // same document minus that grant (CORS + GET/OPTIONS kept by metadataHandler).
12280
+ if (!issuerPath && OAUTH_DISABLE_REFRESH_TOKEN) {
12281
+ app.use("/.well-known/oauth-authorization-server", metadataHandler({
12282
+ ...createOAuthMetadata({
12283
+ provider: oauthProvider,
12284
+ issuerUrl,
12285
+ baseUrl: issuerUrl,
12286
+ scopesSupported,
12287
+ }),
12288
+ grant_types_supported: grantTypesSupported,
12289
+ }));
12290
+ }
12291
+ // Mounts /.well-known/oauth-authorization-server (shadowed above when basePath set
12292
+ // or OAUTH_DISABLE_REFRESH_TOKEN is on),
12276
12293
  // /.well-known/oauth-protected-resource, /authorize, /token, /register, /revoke
12277
12294
  // Some proxies include the port in X-Forwarded-For (e.g. "1.2.3.4:5678" or
12278
12295
  // "[2001:db8::1]:5678"), which makes express-rate-limit throw
@@ -72,6 +72,7 @@ const CLIENT_CACHE_MAX_SIZE = 1000;
72
72
  const GROUP_MEMBERSHIP_CACHE_MAX_SIZE = 1000;
73
73
  const GROUP_MEMBERSHIP_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
74
74
  const GROUP_ACCESS_DENIED_MESSAGE = "Access denied: user is not a member of an allowed group";
75
+ const REFRESH_TOKEN_DISABLED_MESSAGE = "Refresh tokens are disabled on this server (OAUTH_DISABLE_REFRESH_TOKEN); start a new authorization";
75
76
  function singleQueryParam(value) {
76
77
  return typeof value === "string" ? value : undefined;
77
78
  }
@@ -155,7 +156,11 @@ class GitLabOAuthServerProvider {
155
156
  // mode, where GitLab checks redirect_uri against the app registration.
156
157
  _redirectPolicy = null;
157
158
  _consent = null;
158
- constructor(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, allowedGroups, callbackProxyEnabled = false, callbackUrl = "", stateless = null, allowedRedirectUris = DEFAULT_ALLOWED_REDIRECT_URIS) {
159
+ // OAUTH_DISABLE_REFRESH_TOKEN: issued tokens carry no refresh_token and the
160
+ // refresh_token grant is rejected, so the client re-runs the browser
161
+ // authorization once the GitLab access token expires.
162
+ _disableRefreshToken;
163
+ constructor(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, allowedGroups, callbackProxyEnabled = false, callbackUrl = "", stateless = null, allowedRedirectUris = DEFAULT_ALLOWED_REDIRECT_URIS, disableRefreshToken = false) {
159
164
  this._gitlabBaseUrl = gitlabBaseUrl;
160
165
  this._gitlabAppId = gitlabAppId;
161
166
  this._resourceName = resourceName;
@@ -169,6 +174,7 @@ class GitLabOAuthServerProvider {
169
174
  this._callbackProxyEnabled = callbackProxyEnabled;
170
175
  this._callbackUrl = callbackUrl;
171
176
  this._stateless = stateless;
177
+ this._disableRefreshToken = disableRefreshToken;
172
178
  if (callbackProxyEnabled && !callbackUrl) {
173
179
  throw new Error("callbackUrl is required when callbackProxyEnabled is true");
174
180
  }
@@ -190,6 +196,10 @@ class GitLabOAuthServerProvider {
190
196
  `pending TTL: ${stateless.pendingTtlSeconds}s, ` +
191
197
  `stored TTL: ${stateless.storedTtlSeconds}s)`);
192
198
  }
199
+ if (disableRefreshToken) {
200
+ logger.info("OAUTH_DISABLE_REFRESH_TOKEN enabled — refresh_token grant rejected; " +
201
+ "clients must re-authorize in the browser when the GitLab access token expires");
202
+ }
193
203
  }
194
204
  // ---- Client store (local DCR) ------------------------------------------
195
205
  get clientsStore() {
@@ -197,6 +207,7 @@ class GitLabOAuthServerProvider {
197
207
  const resourceName = this._resourceName;
198
208
  const stateless = this._stateless;
199
209
  const redirectPolicy = this._redirectPolicy;
210
+ const disableRefreshToken = this._disableRefreshToken;
200
211
  return {
201
212
  getClient: async (clientId) => {
202
213
  // Stateless path: a signed client_id carries the registration.
@@ -231,7 +242,12 @@ class GitLabOAuthServerProvider {
231
242
  };
232
243
  },
233
244
  registerClient: async (client) => {
234
- const grantTypes = client.grant_types ?? ["authorization_code"];
245
+ let grantTypes = client.grant_types ?? ["authorization_code"];
246
+ if (disableRefreshToken) {
247
+ // OAUTH_DISABLE_REFRESH_TOKEN: match grant_types_supported in the metadata.
248
+ const withoutRefresh = grantTypes.filter((gt) => gt !== "refresh_token");
249
+ grantTypes = withoutRefresh.length > 0 ? withoutRefresh : ["authorization_code"];
250
+ }
235
251
  const redirectUris = client.redirect_uris ?? [];
236
252
  // Callback proxy mode: GitLab never sees the client's redirect_uri, so
237
253
  // this server is the only place it can be checked.
@@ -514,6 +530,12 @@ class GitLabOAuthServerProvider {
514
530
  this._usedProxyCodes.commit(reservedProxyCode.hash, reservedProxyCode.id);
515
531
  reservedProxyCode = null;
516
532
  }
533
+ if (this._disableRefreshToken) {
534
+ // Without a refresh_token the client cannot renew silently and has to
535
+ // restart authorization once the access token expires.
536
+ const { refresh_token: _refreshToken, ...withoutRefresh } = tokens;
537
+ return withoutRefresh;
538
+ }
517
539
  return tokens;
518
540
  }
519
541
  catch (err) {
@@ -585,6 +607,11 @@ class GitLabOAuthServerProvider {
585
607
  }
586
608
  // ---- Refresh token -----------------------------------------------------
587
609
  async exchangeRefreshToken(_client, refreshToken, scopes, resource) {
610
+ if (this._disableRefreshToken) {
611
+ // invalid_grant makes MCP clients drop stored tokens and restart the
612
+ // authorization_code flow; GitLab is not contacted.
613
+ throw new InvalidGrantError(REFRESH_TOKEN_DISABLED_MESSAGE);
614
+ }
588
615
  const params = new URLSearchParams({
589
616
  grant_type: "refresh_token",
590
617
  client_id: this._gitlabAppId,
@@ -736,7 +763,13 @@ class GitLabOAuthServerProvider {
736
763
  res.status(502).send("Token exchange with GitLab failed");
737
764
  return;
738
765
  }
739
- const tokens = OAuthTokensSchema.parse(await tokenResponse.json());
766
+ let tokens = OAuthTokensSchema.parse(await tokenResponse.json());
767
+ if (this._disableRefreshToken) {
768
+ // Drop the refresh_token before it is stored in the LRU or sealed into
769
+ // the proxy code that travels through the browser.
770
+ const { refresh_token: _refreshToken, ...withoutRefresh } = tokens;
771
+ tokens = withoutRefresh;
772
+ }
740
773
  // Generate a proxy auth code for the MCP client. Sealed in stateless
741
774
  // mode; random UUID + LRU entry in legacy mode.
742
775
  const proxyCode = stateless
@@ -811,7 +844,10 @@ class GitLabOAuthServerProvider {
811
844
  * instead of an in-memory cache, enabling multi-pod deploys.
812
845
  * @param allowedRedirectUris Callback proxy mode only: client redirect URIs that
813
846
  * may register and receive codes (see oauth-consent.ts).
847
+ * @param disableRefreshToken When true, issued tokens omit refresh_token and the
848
+ * refresh_token grant fails with invalid_grant, so clients
849
+ * re-authorize in the browser after access-token expiry.
814
850
  */
815
- export function createGitLabOAuthProvider(gitlabBaseUrl, gitlabAppId, resourceName = "GitLab MCP Server", readOnly = false, customScopes, allowedGroups, callbackProxyEnabled = false, callbackUrl = "", stateless = null, allowedRedirectUris = DEFAULT_ALLOWED_REDIRECT_URIS) {
816
- return new GitLabOAuthServerProvider(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, allowedGroups, callbackProxyEnabled, callbackUrl, stateless, allowedRedirectUris);
851
+ export function createGitLabOAuthProvider(gitlabBaseUrl, gitlabAppId, resourceName = "GitLab MCP Server", readOnly = false, customScopes, allowedGroups, callbackProxyEnabled = false, callbackUrl = "", stateless = null, allowedRedirectUris = DEFAULT_ALLOWED_REDIRECT_URIS, disableRefreshToken = false) {
852
+ return new GitLabOAuthServerProvider(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, allowedGroups, callbackProxyEnabled, callbackUrl, stateless, allowedRedirectUris, disableRefreshToken);
817
853
  }
@@ -18,12 +18,13 @@ export async function loadRemoteHttpStack() {
18
18
  if (remoteHttpStackCache.current) {
19
19
  return remoteHttpStackCache.current;
20
20
  }
21
- const [expressModule, rateLimitModule, sseModule, streamableModule, authRouterModule, bearerModule,] = await Promise.all([
21
+ const [expressModule, rateLimitModule, sseModule, streamableModule, authRouterModule, metadataModule, bearerModule,] = await Promise.all([
22
22
  import("express"),
23
23
  import("express-rate-limit"),
24
24
  import("@modelcontextprotocol/sdk/server/sse.js"),
25
25
  import("@modelcontextprotocol/sdk/server/streamableHttp.js"),
26
26
  import("@modelcontextprotocol/sdk/server/auth/router.js"),
27
+ import("@modelcontextprotocol/sdk/server/auth/handlers/metadata.js"),
27
28
  import("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"),
28
29
  ]);
29
30
  remoteHttpStackCache.current = {
@@ -33,6 +34,8 @@ export async function loadRemoteHttpStack() {
33
34
  SSEServerTransport: sseModule.SSEServerTransport,
34
35
  StreamableHTTPServerTransport: streamableModule.StreamableHTTPServerTransport,
35
36
  mcpAuthRouter: authRouterModule.mcpAuthRouter,
37
+ createOAuthMetadata: authRouterModule.createOAuthMetadata,
38
+ metadataHandler: metadataModule.metadataHandler,
36
39
  requireBearerAuth: bearerModule.requireBearerAuth,
37
40
  };
38
41
  return remoteHttpStackCache.current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.3.6",
3
+ "version": "2.3.7-next.136",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [
@@ -49,7 +49,6 @@
49
49
  "prepare": "npm run build",
50
50
  "dev": "npm run build && node build/index.js",
51
51
  "watch": "tsc -p tsconfig.build.json --watch",
52
- "deploy": "npm publish --access public",
53
52
  "changelog": "auto-changelog -p",
54
53
  "test": "npm run test:all",
55
54
  "test:all": "npm run build && npm run test:mock && npm run test:live",