@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/README.ko.md +45 -45
- package/README.md +36 -22
- package/README.zh-CN.md +44 -44
- package/build/config.js +8 -2
- package/build/index.js +127 -32
- package/build/oauth.js +9 -9
- package/build/schemas.js +6 -3
- package/build/scripts/generate-tool-docs.js +404 -0
- package/build/test/config-allowed-groups.test.js +97 -0
- package/build/test/test-oauth-proxy-rate-limit.js +133 -0
- package/build/test/test-remote-downloads.js +162 -1
- package/build/test/utils/proxy-client-ip.test.js +28 -0
- package/build/utils/proxy-client-ip.js +11 -0
- package/package.json +2 -2
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,
|
|
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
|
|
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
|
|
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
|
|
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}'
|
|
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}'
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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..."
|
|
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
|
|
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 =
|
|
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
|
|
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
|
})
|