@zereight/mcp-gitlab 2.1.21 → 2.1.23

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/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_READ_ONLY_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, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_ALLOWED_GROUPS, } from "./config.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_READ_ONLY_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_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
3
3
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
4
4
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
5
5
  /**
@@ -59,17 +59,74 @@ function decryptDownloadToken(tokenStr) {
59
59
  return null;
60
60
  }
61
61
  }
62
+ function getLastHeaderValue(value) {
63
+ const raw = Array.isArray(value) ? value[value.length - 1] : value;
64
+ if (!raw)
65
+ return undefined;
66
+ const parts = raw.split(",").map(part => part.trim()).filter(Boolean);
67
+ return parts.length > 0 ? parts[parts.length - 1] : undefined;
68
+ }
69
+ function unquoteHeaderValue(value) {
70
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
71
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
72
+ }
73
+ return value;
74
+ }
75
+ function getForwardedPublicBaseUrl(req) {
76
+ if (!MCP_TRUST_PROXY)
77
+ return undefined;
78
+ const forwarded = getLastHeaderValue(req.headers.forwarded);
79
+ const forwardedValues = {};
80
+ if (forwarded) {
81
+ for (const part of forwarded.split(";")) {
82
+ const separator = part.indexOf("=");
83
+ if (separator <= 0)
84
+ continue;
85
+ const key = part.slice(0, separator).trim().toLowerCase();
86
+ const value = unquoteHeaderValue(part.slice(separator + 1).trim());
87
+ if (key && value)
88
+ forwardedValues[key] = value;
89
+ }
90
+ }
91
+ const proto = (forwardedValues.proto || getLastHeaderValue(req.headers["x-forwarded-proto"]))?.toLowerCase();
92
+ const host = forwardedValues.host || getLastHeaderValue(req.headers["x-forwarded-host"]);
93
+ if (!proto || !host || !/^https?$/.test(proto))
94
+ return undefined;
95
+ if (/[\s/\\]/.test(host))
96
+ return undefined;
97
+ const prefix = getLastHeaderValue(req.headers["x-forwarded-prefix"]);
98
+ const safePrefix = prefix &&
99
+ prefix.startsWith("/") &&
100
+ !prefix.startsWith("//") &&
101
+ !prefix.includes("://") &&
102
+ !/[\s\\]/.test(prefix)
103
+ ? prefix.replace(/\/+$/, "")
104
+ : undefined;
105
+ try {
106
+ const baseUrl = new URL(`${proto}://${host}`);
107
+ if (baseUrl.username || baseUrl.password || baseUrl.pathname !== "/" || baseUrl.search || baseUrl.hash) {
108
+ return undefined;
109
+ }
110
+ if (safePrefix)
111
+ baseUrl.pathname = safePrefix;
112
+ return baseUrl.toString().replace(/\/$/, "");
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
62
118
  /**
63
119
  * Build a URL pointing to the download proxy endpoint.
64
120
  * Embeds an encrypted auth token (and API URL for dynamic routing)
65
121
  * from the current session so the URL works standalone.
66
122
  */
67
123
  function buildDownloadUrl(type, params) {
68
- const base = MCP_SERVER_URL || `http://${HOST}:${PORT}`;
124
+ const base = MCP_SERVER_URL || sessionAuthStore.getStore()?.publicBaseUrl || `http://${HOST}:${PORT}`;
69
125
  const baseUrl = new URL(base);
70
126
  // Preserve any path prefix (e.g. /gitlab-mcp) from the base URL
71
127
  const basePath = baseUrl.pathname.replace(/\/+$/, "");
72
- const url = new URL(`${basePath}/downloads/${type}`, baseUrl.origin);
128
+ const safeBasePath = basePath ? `/${basePath.replace(/^\/+/, "")}` : "";
129
+ const url = new URL(`${safeBasePath}/downloads/${type}`, baseUrl.origin);
73
130
  for (const [key, value] of Object.entries(params)) {
74
131
  url.searchParams.set(key, value);
75
132
  }
@@ -126,6 +183,8 @@ import { z } from "zod";
126
183
  import { initializeOAuthClient } from "./oauth.js";
127
184
  import { createGitLabOAuthProvider } from "./oauth-proxy.js";
128
185
  import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
186
+ import { ipKeyGenerator } from "express-rate-limit";
187
+ import { normalizeProxyClientIpForRateLimit } from "./utils/proxy-client-ip.js";
129
188
  import { normalizeGitLabApiUrl } from "./utils/url.js";
130
189
  import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents } from "./utils/helpers.js";
131
190
  import { graphqlQueryContainsWriteOperation } from "./utils/graphql-query.js";
@@ -429,6 +488,7 @@ function createServer() {
429
488
  token: authData.token,
430
489
  lastUsed: authData.lastUsed,
431
490
  apiUrl: authData.apiUrl,
491
+ publicBaseUrl: authData.publicBaseUrl,
432
492
  };
433
493
  // Run the handler within the retrieved context
434
494
  const result = await sessionAuthStore.run(sessionContext, () => handleToolCall(request.params));
@@ -660,7 +720,7 @@ async function ensureValidOAuthToken() {
660
720
  logger.info("OAuth token refreshed successfully");
661
721
  }
662
722
  catch (error) {
663
- logger.error("Failed to refresh OAuth token:", error);
723
+ logger.error({ err: error }, "Failed to refresh OAuth token");
664
724
  throw error;
665
725
  }
666
726
  }
@@ -856,6 +916,15 @@ const sessionAuthStore = new AsyncLocalStorage();
856
916
  // Session context map for storing auth data by session ID
857
917
  // This survives async boundaries where AsyncLocalStorage might not
858
918
  const authBySession = {};
919
+ function withPublicBaseUrl(authData, publicBaseUrl, previous) {
920
+ const effectivePublicBaseUrl = publicBaseUrl || previous?.publicBaseUrl;
921
+ return effectivePublicBaseUrl ? { ...authData, publicBaseUrl: effectivePublicBaseUrl } : authData;
922
+ }
923
+ function updateSessionPublicBaseUrl(sessionId, publicBaseUrl) {
924
+ if (sessionId && publicBaseUrl && authBySession[sessionId]) {
925
+ authBySession[sessionId].publicBaseUrl = publicBaseUrl;
926
+ }
927
+ }
859
928
  // Base headers without authentication
860
929
  const BASE_HEADERS = {
861
930
  Accept: "application/json",
@@ -1007,7 +1076,7 @@ async function handleGitLabError(response) {
1007
1076
  const errorBody = await response.text();
1008
1077
  // Check specifically for Rate Limit error
1009
1078
  if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
1010
- logger.error("GitLab API Rate Limit Exceeded:", errorBody);
1079
+ logger.error({ err: errorBody }, "GitLab API Rate Limit Exceeded");
1011
1080
  logger.error("User API Key Rate limit exceeded. Please try again later.");
1012
1081
  throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
1013
1082
  }
@@ -5431,7 +5500,7 @@ async function getUser(username) {
5431
5500
  return null;
5432
5501
  }
5433
5502
  catch (error) {
5434
- logger.error(`Error fetching user by username '${username}':`, error);
5503
+ logger.error({ err: error }, `Error fetching user by username '${username}'`);
5435
5504
  return null;
5436
5505
  }
5437
5506
  }
@@ -5450,7 +5519,7 @@ async function getUsers(usernames) {
5450
5519
  users[username] = user;
5451
5520
  }
5452
5521
  catch (error) {
5453
- logger.error(`Error processing username '${username}':`, error);
5522
+ logger.error({ err: error }, `Error processing username '${username}'`);
5454
5523
  users[username] = null;
5455
5524
  }
5456
5525
  }
@@ -6431,7 +6500,7 @@ async function handleToolCall(params) {
6431
6500
  };
6432
6501
  }
6433
6502
  catch (forkError) {
6434
- logger.error("Error forking repository:", forkError);
6503
+ logger.error({ err: forkError }, "Error forking repository");
6435
6504
  let forkErrorMessage = "Failed to fork repository";
6436
6505
  if (forkError instanceof Error) {
6437
6506
  forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`;
@@ -8635,7 +8704,7 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8635
8704
  }
8636
8705
  }
8637
8706
  catch (error) {
8638
- logger.error("Download proxy error:", error);
8707
+ logger.error({ err: error }, "Download proxy error");
8639
8708
  if (!res.headersSent) {
8640
8709
  res.status(502).json({ error: "Failed to proxy download from GitLab" });
8641
8710
  }
@@ -8647,6 +8716,9 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8647
8716
  */
8648
8717
  async function startSSEServer() {
8649
8718
  const app = express();
8719
+ if (MCP_TRUST_PROXY) {
8720
+ app.set("trust proxy", 1);
8721
+ }
8650
8722
  const transports = {};
8651
8723
  let shuttingDown = false;
8652
8724
  app.get("/sse", async (_, res) => {
@@ -8691,7 +8763,7 @@ async function startSSEServer() {
8691
8763
  await transport.close();
8692
8764
  }
8693
8765
  catch (error) {
8694
- logger.error("Error closing SSE transport:", error);
8766
+ logger.error({ err: error }, "Error closing SSE transport");
8695
8767
  }
8696
8768
  }));
8697
8769
  clientPool.closeAll();
@@ -8995,6 +9067,7 @@ async function startStreamableHTTPServer() {
8995
9067
  token: effective.token,
8996
9068
  lastUsed: Date.now(),
8997
9069
  apiUrl: effective.apiUrl,
9070
+ publicBaseUrl: getForwardedPublicBaseUrl(req),
8998
9071
  };
8999
9072
  // Step 4: create a fresh transport per request.
9000
9073
  const transport = isInit
@@ -9044,13 +9117,13 @@ async function startStreamableHTTPServer() {
9044
9117
  });
9045
9118
  };
9046
9119
  // Configure Express middleware
9120
+ if (MCP_TRUST_PROXY) {
9121
+ app.set("trust proxy", 1);
9122
+ }
9047
9123
  app.use(express.json());
9048
9124
  registerDownloadProxy(app);
9049
9125
  // MCP OAuth — mount auth router and prepare bearer-auth middleware
9050
9126
  if (GITLAB_MCP_OAUTH) {
9051
- // Trust first proxy so express-rate-limit uses X-Forwarded-For for real client IP.
9052
- // Only enabled in OAuth mode where the server is typically behind a reverse proxy.
9053
- app.set("trust proxy", 1);
9054
9127
  const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4\/?$/, "").replace(/\/$/, "");
9055
9128
  const issuerUrl = new URL(MCP_SERVER_URL);
9056
9129
  const callbackUrl = GITLAB_OAUTH_CALLBACK_PROXY
@@ -9064,7 +9137,7 @@ async function startStreamableHTTPServer() {
9064
9137
  storedTtlSeconds: OAUTH_STATELESS_STORED_TTL_SECONDS,
9065
9138
  }
9066
9139
  : null;
9067
- const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES, GITLAB_ALLOWED_GROUPS, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions);
9140
+ const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions);
9068
9141
  const scopesSupported = GITLAB_OAUTH_SCOPES ?? ["api", "read_api", "read_user"];
9069
9142
  // When server URL has a path (e.g. behind Kong), the SDK's well-known metadata
9070
9143
  // advertises root-level endpoints. Override to use path-prefixed endpoints.
@@ -9108,12 +9181,22 @@ async function startStreamableHTTPServer() {
9108
9181
  }
9109
9182
  // Mounts /.well-known/oauth-authorization-server (shadowed above when basePath set),
9110
9183
  // /.well-known/oauth-protected-resource, /authorize, /token, /register, /revoke
9184
+ // Some proxies include the port in X-Forwarded-For (e.g. "1.2.3.4:5678" or
9185
+ // "[2001:db8::1]:5678"), which makes express-rate-limit throw
9186
+ // ERR_ERL_INVALID_IP_ADDRESS. Strip the port first, then delegate to
9187
+ // ipKeyGenerator for correct IPv6 subnet handling.
9188
+ const rateLimitKeyGenerator = (req) => ipKeyGenerator(normalizeProxyClientIpForRateLimit(req.ip ?? ""));
9189
+ const rateLimitOptions = { keyGenerator: rateLimitKeyGenerator };
9111
9190
  app.use(mcpAuthRouter({
9112
9191
  provider: oauthProvider,
9113
9192
  issuerUrl,
9114
9193
  baseUrl: issuerUrl,
9115
9194
  scopesSupported,
9116
9195
  resourceName: "GitLab MCP Server",
9196
+ authorizationOptions: { rateLimit: rateLimitOptions },
9197
+ tokenOptions: { rateLimit: rateLimitOptions },
9198
+ revocationOptions: { rateLimit: rateLimitOptions },
9199
+ clientRegistrationOptions: { rateLimit: rateLimitOptions },
9117
9200
  }));
9118
9201
  // Expose provider so the /mcp route middleware can reference it
9119
9202
  app._mcpOAuthProvider = oauthProvider;
@@ -9178,6 +9261,7 @@ async function startStreamableHTTPServer() {
9178
9261
  // Streamable HTTP endpoint - handles both session creation and message handling
9179
9262
  app.post("/mcp", mcpBearerAuth, async (req, res) => {
9180
9263
  const sessionId = readMcpSessionIdHeader(req);
9264
+ const publicBaseUrl = getForwardedPublicBaseUrl(req);
9181
9265
  // Track request
9182
9266
  metrics.requestsProcessed++;
9183
9267
  // Stateless-mode branch: bypass authBySession / streamableTransports
@@ -9222,19 +9306,20 @@ async function startStreamableHTTPServer() {
9222
9306
  return;
9223
9307
  }
9224
9308
  // Store auth for this session
9225
- authBySession[sessionId] = authData;
9309
+ authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9226
9310
  logger.info(`Session ${sessionId}: stored ${authData.header} header`);
9227
9311
  setAuthTimeout(sessionId);
9228
9312
  }
9229
9313
  else if (sessionId && authData) {
9230
9314
  // Existing session: allow auth rotation/update
9231
- authBySession[sessionId] = authData;
9315
+ authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl, authBySession[sessionId]);
9232
9316
  logger.debug(`Session ${sessionId}: updated ${authData.header} header`);
9233
9317
  setAuthTimeout(sessionId);
9234
9318
  }
9235
9319
  else if (sessionId && authBySession[sessionId]) {
9236
9320
  // Existing session with stored auth: update last used time and reset timeout
9237
9321
  authBySession[sessionId].lastUsed = Date.now();
9322
+ updateSessionPublicBaseUrl(sessionId, publicBaseUrl);
9238
9323
  setAuthTimeout(sessionId);
9239
9324
  }
9240
9325
  else if (!sessionId && !authData) {
@@ -9250,17 +9335,12 @@ async function startStreamableHTTPServer() {
9250
9335
  if (headerAuthData) {
9251
9336
  if (headerAuthData && sessionId) {
9252
9337
  if (!authBySession[sessionId]) {
9253
- authBySession[sessionId] = headerAuthData;
9338
+ authBySession[sessionId] = withPublicBaseUrl(headerAuthData, publicBaseUrl);
9254
9339
  logger.info(`Session ${sessionId}: stored ${headerAuthData.header} header (header auth)`);
9255
9340
  setAuthTimeout(sessionId);
9256
9341
  }
9257
9342
  else {
9258
- authBySession[sessionId] = {
9259
- ...authBySession[sessionId],
9260
- header: headerAuthData.header,
9261
- token: headerAuthData.token,
9262
- lastUsed: Date.now(),
9263
- };
9343
+ authBySession[sessionId] = withPublicBaseUrl(headerAuthData, publicBaseUrl, authBySession[sessionId]);
9264
9344
  setAuthTimeout(sessionId);
9265
9345
  }
9266
9346
  }
@@ -9274,6 +9354,7 @@ async function startStreamableHTTPServer() {
9274
9354
  token: authInfo.token,
9275
9355
  lastUsed: Date.now(),
9276
9356
  apiUrl: GITLAB_API_URL,
9357
+ publicBaseUrl,
9277
9358
  };
9278
9359
  logger.info(`Session ${sessionId}: stored OAuth token (client: ${authInfo.clientId})`);
9279
9360
  setAuthTimeout(sessionId);
@@ -9282,6 +9363,7 @@ async function startStreamableHTTPServer() {
9282
9363
  // Update token on every request — the client may have refreshed it
9283
9364
  authBySession[sessionId].token = authInfo.token;
9284
9365
  authBySession[sessionId].lastUsed = Date.now();
9366
+ updateSessionPublicBaseUrl(sessionId, publicBaseUrl);
9285
9367
  setAuthTimeout(sessionId);
9286
9368
  }
9287
9369
  }
@@ -9309,7 +9391,7 @@ async function startStreamableHTTPServer() {
9309
9391
  if (REMOTE_AUTHORIZATION && !authBySession[newSessionId]) {
9310
9392
  const authData = parseAuthHeaders(req);
9311
9393
  if (authData) {
9312
- authBySession[newSessionId] = authData;
9394
+ authBySession[newSessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9313
9395
  logger.info(`Session ${newSessionId}: stored ${authData.header} header`);
9314
9396
  setAuthTimeout(newSessionId);
9315
9397
  }
@@ -9320,7 +9402,7 @@ async function startStreamableHTTPServer() {
9320
9402
  if (hasHeaderAuth(req)) {
9321
9403
  const authData = parseAuthHeaders(req);
9322
9404
  if (authData) {
9323
- authBySession[newSessionId] = authData;
9405
+ authBySession[newSessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9324
9406
  logger.info(`Session ${newSessionId}: stored ${authData.header} header (header auth)`);
9325
9407
  setAuthTimeout(newSessionId);
9326
9408
  }
@@ -9333,6 +9415,7 @@ async function startStreamableHTTPServer() {
9333
9415
  token: authInfo.token,
9334
9416
  lastUsed: Date.now(),
9335
9417
  apiUrl: GITLAB_API_URL,
9418
+ publicBaseUrl,
9336
9419
  };
9337
9420
  logger.info(`Session ${newSessionId}: stored OAuth token (client: ${authInfo.clientId})`);
9338
9421
  setAuthTimeout(newSessionId);
@@ -9364,7 +9447,7 @@ async function startStreamableHTTPServer() {
9364
9447
  }
9365
9448
  }
9366
9449
  catch (error) {
9367
- logger.error("Streamable HTTP error:", error);
9450
+ logger.error({ err: error }, "Streamable HTTP error");
9368
9451
  res.status(500).json({
9369
9452
  error: "Internal server error",
9370
9453
  message: error instanceof Error ? error.message : "Unknown error",
@@ -9380,6 +9463,7 @@ async function startStreamableHTTPServer() {
9380
9463
  token: authData.token,
9381
9464
  lastUsed: authData.lastUsed,
9382
9465
  apiUrl: authData.apiUrl,
9466
+ publicBaseUrl: authData.publicBaseUrl,
9383
9467
  };
9384
9468
  // Run the entire request handling within AsyncLocalStorage context
9385
9469
  await sessionAuthStore.run(ctx, handleRequest);
@@ -9447,7 +9531,7 @@ async function startStreamableHTTPServer() {
9447
9531
  res.status(204).send();
9448
9532
  }
9449
9533
  catch (error) {
9450
- logger.error(`Error closing session ${sessionId}:`, error);
9534
+ logger.error({ err: error }, `Error closing session ${sessionId}`);
9451
9535
  res.status(500).json({ error: "Failed to close session" });
9452
9536
  }
9453
9537
  }
@@ -9482,7 +9566,7 @@ async function startStreamableHTTPServer() {
9482
9566
  }
9483
9567
  }
9484
9568
  catch (error) {
9485
- logger.error(`Error closing session ${sessionId}:`, error);
9569
+ logger.error({ err: error }, `Error closing session ${sessionId}`);
9486
9570
  }
9487
9571
  });
9488
9572
  await Promise.allSettled(closePromises);
@@ -9503,7 +9587,7 @@ async function startStreamableHTTPServer() {
9503
9587
  * Handle transport-specific initialization logic
9504
9588
  */
9505
9589
  async function initializeServerByTransportMode(mode) {
9506
- logger.info("Initializing server with transport mode:", mode);
9590
+ logger.info({ mode }, "Initializing server with transport mode");
9507
9591
  switch (mode) {
9508
9592
  case TransportMode.STDIO:
9509
9593
  logger.warn("Starting GitLab MCP Server with stdio transport");
@@ -9543,7 +9627,7 @@ async function runServer() {
9543
9627
  logger.info("OAuth authentication successful");
9544
9628
  }
9545
9629
  catch (error) {
9546
- logger.error("OAuth authentication failed:", error);
9630
+ logger.error({ err: error }, "OAuth authentication failed");
9547
9631
  process.exit(1);
9548
9632
  }
9549
9633
  }
@@ -9551,14 +9635,25 @@ async function runServer() {
9551
9635
  await initializeServerByTransportMode(transportMode);
9552
9636
  logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
9553
9637
  logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
9638
+ if (GITLAB_ALLOWED_GROUPS_RAW) {
9639
+ if (GITLAB_OAUTH_ALLOWED_GROUPS_RAW) {
9640
+ logger.warn("GITLAB_ALLOWED_GROUPS is set but ignored — GITLAB_OAUTH_ALLOWED_GROUPS takes precedence.");
9641
+ }
9642
+ else {
9643
+ logger.warn("GITLAB_ALLOWED_GROUPS is deprecated. Use GITLAB_OAUTH_ALLOWED_GROUPS instead.");
9644
+ }
9645
+ }
9646
+ if (GITLAB_OAUTH_ALLOWED_GROUPS) {
9647
+ logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
9648
+ }
9554
9649
  }
9555
9650
  catch (error) {
9556
- logger.error("Error initializing server:", error);
9651
+ logger.error({ err: error }, "Error initializing server");
9557
9652
  process.exit(1);
9558
9653
  }
9559
9654
  }
9560
9655
  // 下記の2行を追記
9561
9656
  runServer().catch(error => {
9562
- logger.error("Fatal error in main():", error);
9657
+ logger.fatal({ err: error }, "Fatal error in main()");
9563
9658
  process.exit(1);
9564
9659
  });
package/build/oauth.js CHANGED
@@ -193,7 +193,7 @@ export class GitLabOAuth {
193
193
  logger.info(`Token saved to ${this.tokenStoragePath}`);
194
194
  }
195
195
  catch (error) {
196
- logger.error("Failed to save token:", error);
196
+ logger.error({ err: error }, "Failed to save token");
197
197
  throw error;
198
198
  }
199
199
  }
@@ -209,7 +209,7 @@ export class GitLabOAuth {
209
209
  return JSON.parse(data);
210
210
  }
211
211
  catch (error) {
212
- logger.error("Failed to load token:", error);
212
+ logger.error({ err: error }, "Failed to load token");
213
213
  return null;
214
214
  }
215
215
  }
@@ -240,7 +240,7 @@ export class GitLabOAuth {
240
240
  return await requestAuthFromExistingServer(callbackPort, requestId);
241
241
  }
242
242
  catch (error) {
243
- logger.error("Failed to connect to existing OAuth server:", error);
243
+ logger.error({ err: error }, "Failed to connect to existing OAuth server");
244
244
  throw new Error(`Port ${callbackPort} is in use but cannot connect to existing OAuth server. Please close other instances or use a different port.`);
245
245
  }
246
246
  }
@@ -285,7 +285,7 @@ export class GitLabOAuth {
285
285
  logger.info("Opening browser for new authentication request...");
286
286
  logger.info(`If browser doesn't open, visit: ${authUrl}`);
287
287
  open(authUrl).catch(err => {
288
- logger.error("Failed to open browser:", err);
288
+ logger.error({ err }, "Failed to open browser");
289
289
  logger.info(`Please manually open: ${authUrl}`);
290
290
  });
291
291
  // Wait for the auth to complete
@@ -430,7 +430,7 @@ export class GitLabOAuth {
430
430
  }
431
431
  }
432
432
  catch (error) {
433
- logger.error("Error handling request:", error);
433
+ logger.error({ err: error }, "Error handling request");
434
434
  res.writeHead(500, { "Content-Type": "text/plain" });
435
435
  res.end("Internal Server Error");
436
436
  }
@@ -441,12 +441,12 @@ export class GitLabOAuth {
441
441
  logger.info("Opening browser for authentication...");
442
442
  logger.info(`If browser doesn't open, visit: ${authUrl}`);
443
443
  open(authUrl).catch(err => {
444
- logger.error("Failed to open browser:", err);
444
+ logger.error({ err }, "Failed to open browser");
445
445
  logger.info(`Please manually open: ${authUrl}`);
446
446
  });
447
447
  });
448
448
  server.on("error", error => {
449
- logger.error("OAuth server error:", error);
449
+ logger.error({ err: error }, "OAuth server error");
450
450
  const pending = pendingAuthRequests.get(initialRequestId);
451
451
  if (pending) {
452
452
  clearTimeout(pending.timeout);
@@ -474,7 +474,7 @@ export class GitLabOAuth {
474
474
  this.saveToken(tokenData);
475
475
  }
476
476
  catch (error) {
477
- logger.error("Token refresh failed. Starting new OAuth flow...", error);
477
+ logger.error({ err: error }, "Token refresh failed. Starting new OAuth flow...");
478
478
  tokenData = await this.startOAuthFlow();
479
479
  }
480
480
  }
@@ -496,7 +496,7 @@ export class GitLabOAuth {
496
496
  }
497
497
  }
498
498
  catch (error) {
499
- logger.error("Failed to clear token:", error);
499
+ logger.error({ err: error }, "Failed to clear token");
500
500
  }
501
501
  }
502
502
  /**
package/build/schemas.js CHANGED
@@ -467,7 +467,10 @@ export const RetryPipelineSchema = z.object({
467
467
  pipeline_id: z.coerce.string().describe("The ID of the pipeline to retry"),
468
468
  });
469
469
  // Schema for canceling a pipeline
470
- export const CancelPipelineSchema = RetryPipelineSchema;
470
+ export const CancelPipelineSchema = z.object({
471
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
472
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline to cancel"),
473
+ });
471
474
  // Schema for the input parameters for pipeline job operations
472
475
  export const GetPipelineJobOutputSchema = z.object({
473
476
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
@@ -1668,7 +1671,7 @@ export const MergeMergeRequestSchema = ProjectParamsSchema.extend({
1668
1671
  .boolean()
1669
1672
  .optional()
1670
1673
  .default(false)
1671
- .describe("If true, the merge request merges when the pipeline succeeds.in GitLab 17.11. Use"),
1674
+ .describe("If true, the merge request merges when the pipeline succeeds. Deprecated in GitLab 17.11. Use `auto_merge` instead."),
1672
1675
  should_remove_source_branch: z.coerce
1673
1676
  .boolean()
1674
1677
  .optional()
@@ -2091,7 +2094,7 @@ export const ListLabelsSchema = z
2091
2094
  with_counts: z
2092
2095
  .coerce.boolean()
2093
2096
  .optional()
2094
- .describe("Whether or not to include issue and merge request counts"),
2097
+ .describe("Whether to include issue and merge request counts"),
2095
2098
  include_ancestor_groups: z.coerce.boolean().optional().describe("Include ancestor groups"),
2096
2099
  search: z.string().optional().describe("Keyword to filter labels by"),
2097
2100
  })