@zereight/mcp-gitlab 2.3.8-next.148 → 2.3.8-next.149
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 +11 -143
- package/build/server/gitlab-request-context.js +166 -0
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -63,12 +63,11 @@ import { loadKeyMaterialFromEnv, looksLikeStatelessSessionId, mintSessionId, ope
|
|
|
63
63
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
64
64
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
65
65
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
66
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
67
66
|
import fetchCookie from "fetch-cookie";
|
|
68
67
|
import fs from "node:fs";
|
|
69
68
|
import { pipeline as streamPipeline } from "node:stream/promises";
|
|
70
69
|
import os from "node:os";
|
|
71
|
-
import { fetch as undiciFetch, File, FormData
|
|
70
|
+
import { fetch as undiciFetch, File, FormData } from "undici";
|
|
72
71
|
import path from "node:path";
|
|
73
72
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
74
73
|
import { URL } from "node:url";
|
|
@@ -98,6 +97,7 @@ import { MCP_INSTRUCTIONS } from "./server/mcp-instructions.js";
|
|
|
98
97
|
import { formatUnknownHelpTopic, getWorkflowGuide, readHelpTopic, } from "./server/mcp-help.js";
|
|
99
98
|
import { isInitializationRequestBody, isUnauthenticatedDiscoveryRequestBody, readAcceptHeader, readMcpSessionIdHeader, redactSessionIdForLog, } from "./server/request-helpers.js";
|
|
100
99
|
export { readMcpSessionIdHeader } from "./server/request-helpers.js";
|
|
100
|
+
import { authBySession, BASE_HEADERS, bindSessionProjectScope, buildAuthHeaders, configureGitLabRequestContext, getEffectiveAllowedProjectIds, getEffectiveApiUrl, getFetchConfig, SESSION_PROJECT_SCOPE_MISMATCH, sessionAuthStore, updateSessionPublicBaseUrl, usesJobTokenHeader, withPublicBaseUrl, } from "./server/gitlab-request-context.js";
|
|
101
101
|
import { apiUrlUserinfoWarning, commaSplitHint, configuredApiUrlErrors, encodeGitLabPath, encodeGitLabPathSegment, invalidListEntryError, isCleartextRemoteUrl, normalizeGitLabApiUrl, parseConfiguredGitLabApiUrls, } from "./utils/url.js";
|
|
102
102
|
import { estimateMergeCommitCount, filterDiffsByPatterns, openSafeOutputWriteStream, resolveDownloadRoot, readSafeExistingFile, summarizeWebhookEvents, } from "./utils/helpers.js";
|
|
103
103
|
import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation, } from "./utils/graphql-query.js";
|
|
@@ -967,6 +967,15 @@ const clientPool = new GitLabClientPool({
|
|
|
967
967
|
caCertPath: GITLAB_CA_CERT_PATH,
|
|
968
968
|
poolMaxSize: GITLAB_POOL_MAX_SIZE,
|
|
969
969
|
});
|
|
970
|
+
// GITLAB_API_URL and GITLAB_ALLOWED_PROJECT_IDS are defined further down; the getters
|
|
971
|
+
// read them at call time, after module evaluation.
|
|
972
|
+
configureGitLabRequestContext({
|
|
973
|
+
logger,
|
|
974
|
+
getDefaultApiUrl: () => GITLAB_API_URL,
|
|
975
|
+
getDefaultAllowedProjectIds: () => GITLAB_ALLOWED_PROJECT_IDS,
|
|
976
|
+
getDispatcherForUrl: apiUrl => clientPool.getDispatcherForUrl(apiUrl),
|
|
977
|
+
getOAuthAccessToken: () => OAUTH_ACCESS_TOKEN,
|
|
978
|
+
});
|
|
970
979
|
// Create cookie jar with clean Netscape file parsing
|
|
971
980
|
// Resolve cookie path once using os.homedir() for cross-platform support
|
|
972
981
|
const resolvedCookiePath = GITLAB_AUTH_COOKIE_PATH
|
|
@@ -1093,129 +1102,6 @@ async function ensureSessionForRequest() {
|
|
|
1093
1102
|
logger.debug("Session warmup request failed, will retry on next request");
|
|
1094
1103
|
}
|
|
1095
1104
|
}
|
|
1096
|
-
const sessionAuthStore = new AsyncLocalStorage();
|
|
1097
|
-
// Session context map for storing auth data by session ID
|
|
1098
|
-
// This survives async boundaries where AsyncLocalStorage might not
|
|
1099
|
-
const authBySession = {};
|
|
1100
|
-
function withPublicBaseUrl(authData, publicBaseUrl, previous) {
|
|
1101
|
-
const effectivePublicBaseUrl = publicBaseUrl || previous?.publicBaseUrl;
|
|
1102
|
-
return effectivePublicBaseUrl ? { ...authData, publicBaseUrl: effectivePublicBaseUrl } : authData;
|
|
1103
|
-
}
|
|
1104
|
-
function updateSessionPublicBaseUrl(sessionId, publicBaseUrl) {
|
|
1105
|
-
if (sessionId && publicBaseUrl && authBySession[sessionId]) {
|
|
1106
|
-
authBySession[sessionId].publicBaseUrl = publicBaseUrl;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
// Base headers without authentication
|
|
1110
|
-
const BASE_HEADERS = {
|
|
1111
|
-
Accept: "application/json",
|
|
1112
|
-
"Content-Type": "application/json",
|
|
1113
|
-
};
|
|
1114
|
-
/**
|
|
1115
|
-
* Build authentication headers dynamically based on context
|
|
1116
|
-
* In REMOTE_AUTHORIZATION or GITLAB_MCP_OAUTH mode, reads from AsyncLocalStorage session context
|
|
1117
|
-
* Otherwise, uses environment token (OAuth token is refreshed lazily before each tool call)
|
|
1118
|
-
*/
|
|
1119
|
-
function buildAuthHeaders() {
|
|
1120
|
-
if (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) {
|
|
1121
|
-
const ctx = sessionAuthStore.getStore();
|
|
1122
|
-
logger.debug({
|
|
1123
|
-
sidPrefix: redactSessionIdForLog(ctx?.sessionId),
|
|
1124
|
-
header: ctx?.header,
|
|
1125
|
-
apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
|
|
1126
|
-
}, "buildAuthHeaders: session context");
|
|
1127
|
-
if (ctx?.token) {
|
|
1128
|
-
return {
|
|
1129
|
-
[ctx.header]: ctx.header === "Authorization" ? `Bearer ${ctx.token}` : ctx.token,
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
return {}; // No auth headers if no session context
|
|
1133
|
-
}
|
|
1134
|
-
// Standard mode: PAT preferred over job token (broader permissions).
|
|
1135
|
-
// OAuth token takes priority over PAT when both are set.
|
|
1136
|
-
// NOTE: Changed in PR #400 — previously GITLAB_JOB_TOKEN had highest priority.
|
|
1137
|
-
// If both GITLAB_PERSONAL_ACCESS_TOKEN and GITLAB_JOB_TOKEN are set, PAT wins.
|
|
1138
|
-
// OAuth tokens always travel as `Authorization: Bearer`, even with GITLAB_IS_OLD: the HTTP
|
|
1139
|
-
// client strips `Authorization` on cross-origin redirects but not `Private-Token`.
|
|
1140
|
-
if (OAUTH_ACCESS_TOKEN) {
|
|
1141
|
-
return { Authorization: `Bearer ${OAUTH_ACCESS_TOKEN}` };
|
|
1142
|
-
}
|
|
1143
|
-
if (GITLAB_PERSONAL_ACCESS_TOKEN) {
|
|
1144
|
-
return IS_OLD
|
|
1145
|
-
? { "Private-Token": String(GITLAB_PERSONAL_ACCESS_TOKEN) }
|
|
1146
|
-
: { Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` };
|
|
1147
|
-
}
|
|
1148
|
-
// Fall back to CI job token
|
|
1149
|
-
if (GITLAB_JOB_TOKEN) {
|
|
1150
|
-
return { "JOB-TOKEN": String(GITLAB_JOB_TOKEN) };
|
|
1151
|
-
}
|
|
1152
|
-
return {};
|
|
1153
|
-
}
|
|
1154
|
-
function usesJobTokenHeader() {
|
|
1155
|
-
if (GITLAB_JOB_TOKEN)
|
|
1156
|
-
return true;
|
|
1157
|
-
if (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) {
|
|
1158
|
-
const ctx = sessionAuthStore.getStore();
|
|
1159
|
-
return ctx?.header === "JOB-TOKEN";
|
|
1160
|
-
}
|
|
1161
|
-
return false;
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Get the effective GitLab API URL for the current request
|
|
1165
|
-
* In REMOTE_AUTHORIZATION mode with ENABLE_DYNAMIC_API_URL, reads from session context
|
|
1166
|
-
* Otherwise, uses environment GITLAB_API_URL
|
|
1167
|
-
*/
|
|
1168
|
-
function getEffectiveApiUrl() {
|
|
1169
|
-
if (ENABLE_DYNAMIC_API_URL) {
|
|
1170
|
-
const ctx = sessionAuthStore.getStore();
|
|
1171
|
-
if (ctx?.apiUrl) {
|
|
1172
|
-
return ctx.apiUrl;
|
|
1173
|
-
}
|
|
1174
|
-
logger.warn({
|
|
1175
|
-
sidPrefix: redactSessionIdForLog(ctx?.sessionId),
|
|
1176
|
-
header: ctx?.header,
|
|
1177
|
-
apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
|
|
1178
|
-
}, "getEffectiveApiUrl: No context or apiUrl found, falling back to default");
|
|
1179
|
-
}
|
|
1180
|
-
return GITLAB_API_URL;
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Get the effective project allowlist for the current request
|
|
1184
|
-
* In REMOTE_AUTHORIZATION mode with ENABLE_DYNAMIC_PROJECT_SCOPE, reads the scope
|
|
1185
|
-
* narrowed via the X-GitLab-Allowed-Project-Ids header from session context
|
|
1186
|
-
* (already validated against GITLAB_ALLOWED_PROJECT_IDS by parseAuthHeaders)
|
|
1187
|
-
* Otherwise, uses environment GITLAB_ALLOWED_PROJECT_IDS
|
|
1188
|
-
*/
|
|
1189
|
-
function getEffectiveAllowedProjectIds() {
|
|
1190
|
-
if (ENABLE_DYNAMIC_PROJECT_SCOPE) {
|
|
1191
|
-
const ctx = sessionAuthStore.getStore();
|
|
1192
|
-
if (ctx?.allowedProjectIds && ctx.allowedProjectIds.length > 0) {
|
|
1193
|
-
return ctx.allowedProjectIds;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return GITLAB_ALLOWED_PROJECT_IDS;
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* Keep a dynamic project scope bound to its session
|
|
1200
|
-
* A request that omits X-GitLab-Allowed-Project-Ids keeps the session scope, and a
|
|
1201
|
-
* request naming a different scope is refused (null). An unscoped session may be
|
|
1202
|
-
* narrowed. Re-initialize to change the scope.
|
|
1203
|
-
*/
|
|
1204
|
-
function bindSessionProjectScope(requested, sessionScope) {
|
|
1205
|
-
if (!sessionScope || sessionScope.length === 0) {
|
|
1206
|
-
return requested;
|
|
1207
|
-
}
|
|
1208
|
-
if (requested === undefined) {
|
|
1209
|
-
return sessionScope;
|
|
1210
|
-
}
|
|
1211
|
-
const want = new Set(requested);
|
|
1212
|
-
const have = new Set(sessionScope);
|
|
1213
|
-
return want.size === have.size && [...want].every(id => have.has(id)) ? sessionScope : null;
|
|
1214
|
-
}
|
|
1215
|
-
const SESSION_PROJECT_SCOPE_MISMATCH = {
|
|
1216
|
-
error: "Project scope mismatch",
|
|
1217
|
-
message: "X-GitLab-Allowed-Project-Ids does not match the project scope of this session. Re-initialize to change it.",
|
|
1218
|
-
};
|
|
1219
1105
|
/**
|
|
1220
1106
|
* Reject tools that cannot be limited to allowed projects when
|
|
1221
1107
|
* ENABLE_STRICT_PROJECT_SCOPE is set and an allowlist is in effect
|
|
@@ -1241,24 +1127,6 @@ function filterProjectsByAllowlist(projects) {
|
|
|
1241
1127
|
return projects.filter(project => allowedProjectIds.includes(String(project.id)) ||
|
|
1242
1128
|
allowedProjectIds.includes(project.path_with_namespace));
|
|
1243
1129
|
}
|
|
1244
|
-
/**
|
|
1245
|
-
* Get fetch configuration with proper client from pool
|
|
1246
|
-
* Uses connection pooling when dynamic API URLs are enabled
|
|
1247
|
-
*/
|
|
1248
|
-
/**
|
|
1249
|
-
* Constructs the fetch configuration dynamically.
|
|
1250
|
-
* This function is called immediately before each `fetch` to ensure it runs
|
|
1251
|
-
* within the correct AsyncLocalStorage context, capturing the necessary auth
|
|
1252
|
-
* and API URL information for the current request.
|
|
1253
|
-
*/
|
|
1254
|
-
const getFetchConfig = () => {
|
|
1255
|
-
const effectiveApiUrl = getEffectiveApiUrl();
|
|
1256
|
-
const dispatcher = clientPool.getDispatcherForUrl(effectiveApiUrl);
|
|
1257
|
-
return {
|
|
1258
|
-
headers: { ...BASE_HEADERS, ...buildAuthHeaders() },
|
|
1259
|
-
dispatcher,
|
|
1260
|
-
};
|
|
1261
|
-
};
|
|
1262
1130
|
/**
|
|
1263
1131
|
* Options for the download sinks that stream a file through `fetchWithValidatedRedirects`.
|
|
1264
1132
|
*
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request GitLab context shared by the tool handlers: the session auth store,
|
|
3
|
+
* the auth headers, the effective API URL and project allowlist, and the fetch
|
|
4
|
+
* config built from them.
|
|
5
|
+
*
|
|
6
|
+
* index.ts owns the values this module reads (the default API URL, the allowlist,
|
|
7
|
+
* the client pool, the OAuth token, and the logger) and hands them over once with
|
|
8
|
+
* configureGitLabRequestContext(). The getters are read at call time, as the free
|
|
9
|
+
* variables they replace were.
|
|
10
|
+
*/
|
|
11
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
|
+
import { ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_PERSONAL_ACCESS_TOKEN, IS_OLD, REMOTE_AUTHORIZATION, } from "../config.js";
|
|
13
|
+
import { redactUrlSecretsInText } from "../utils/redact-url.js";
|
|
14
|
+
import { redactSessionIdForLog } from "./request-helpers.js";
|
|
15
|
+
let deps;
|
|
16
|
+
export function configureGitLabRequestContext(next) {
|
|
17
|
+
deps = next;
|
|
18
|
+
}
|
|
19
|
+
function requireDeps() {
|
|
20
|
+
if (!deps) {
|
|
21
|
+
throw new Error("GitLab request context used before configureGitLabRequestContext()");
|
|
22
|
+
}
|
|
23
|
+
return deps;
|
|
24
|
+
}
|
|
25
|
+
export const sessionAuthStore = new AsyncLocalStorage();
|
|
26
|
+
// Session context map for storing auth data by session ID
|
|
27
|
+
// This survives async boundaries where AsyncLocalStorage might not
|
|
28
|
+
export const authBySession = {};
|
|
29
|
+
export function withPublicBaseUrl(authData, publicBaseUrl, previous) {
|
|
30
|
+
const effectivePublicBaseUrl = publicBaseUrl || previous?.publicBaseUrl;
|
|
31
|
+
return effectivePublicBaseUrl ? { ...authData, publicBaseUrl: effectivePublicBaseUrl } : authData;
|
|
32
|
+
}
|
|
33
|
+
export function updateSessionPublicBaseUrl(sessionId, publicBaseUrl) {
|
|
34
|
+
if (sessionId && publicBaseUrl && authBySession[sessionId]) {
|
|
35
|
+
authBySession[sessionId].publicBaseUrl = publicBaseUrl;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Base headers without authentication
|
|
39
|
+
export const BASE_HEADERS = {
|
|
40
|
+
Accept: "application/json",
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Build authentication headers dynamically based on context
|
|
45
|
+
* In REMOTE_AUTHORIZATION or GITLAB_MCP_OAUTH mode, reads from AsyncLocalStorage session context
|
|
46
|
+
* Otherwise, uses environment token (OAuth token is refreshed lazily before each tool call)
|
|
47
|
+
*/
|
|
48
|
+
export function buildAuthHeaders() {
|
|
49
|
+
const { logger, getOAuthAccessToken } = requireDeps();
|
|
50
|
+
if (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) {
|
|
51
|
+
const ctx = sessionAuthStore.getStore();
|
|
52
|
+
logger.debug({
|
|
53
|
+
sidPrefix: redactSessionIdForLog(ctx?.sessionId),
|
|
54
|
+
header: ctx?.header,
|
|
55
|
+
apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
|
|
56
|
+
}, "buildAuthHeaders: session context");
|
|
57
|
+
if (ctx?.token) {
|
|
58
|
+
return {
|
|
59
|
+
[ctx.header]: ctx.header === "Authorization" ? `Bearer ${ctx.token}` : ctx.token,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {}; // No auth headers if no session context
|
|
63
|
+
}
|
|
64
|
+
// Standard mode: PAT preferred over job token (broader permissions).
|
|
65
|
+
// OAuth token takes priority over PAT when both are set.
|
|
66
|
+
// NOTE: Changed in PR #400 — previously GITLAB_JOB_TOKEN had highest priority.
|
|
67
|
+
// If both GITLAB_PERSONAL_ACCESS_TOKEN and GITLAB_JOB_TOKEN are set, PAT wins.
|
|
68
|
+
// OAuth tokens always travel as `Authorization: Bearer`, even with GITLAB_IS_OLD: the HTTP
|
|
69
|
+
// client strips `Authorization` on cross-origin redirects but not `Private-Token`.
|
|
70
|
+
const oauthAccessToken = getOAuthAccessToken();
|
|
71
|
+
if (oauthAccessToken) {
|
|
72
|
+
return { Authorization: `Bearer ${oauthAccessToken}` };
|
|
73
|
+
}
|
|
74
|
+
if (GITLAB_PERSONAL_ACCESS_TOKEN) {
|
|
75
|
+
return IS_OLD
|
|
76
|
+
? { "Private-Token": String(GITLAB_PERSONAL_ACCESS_TOKEN) }
|
|
77
|
+
: { Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` };
|
|
78
|
+
}
|
|
79
|
+
// Fall back to CI job token
|
|
80
|
+
if (GITLAB_JOB_TOKEN) {
|
|
81
|
+
return { "JOB-TOKEN": String(GITLAB_JOB_TOKEN) };
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
export function usesJobTokenHeader() {
|
|
86
|
+
if (GITLAB_JOB_TOKEN)
|
|
87
|
+
return true;
|
|
88
|
+
if (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) {
|
|
89
|
+
const ctx = sessionAuthStore.getStore();
|
|
90
|
+
return ctx?.header === "JOB-TOKEN";
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the effective GitLab API URL for the current request
|
|
96
|
+
* In REMOTE_AUTHORIZATION mode with ENABLE_DYNAMIC_API_URL, reads from session context
|
|
97
|
+
* Otherwise, uses environment GITLAB_API_URL
|
|
98
|
+
*/
|
|
99
|
+
export function getEffectiveApiUrl() {
|
|
100
|
+
const { logger, getDefaultApiUrl } = requireDeps();
|
|
101
|
+
if (ENABLE_DYNAMIC_API_URL) {
|
|
102
|
+
const ctx = sessionAuthStore.getStore();
|
|
103
|
+
if (ctx?.apiUrl) {
|
|
104
|
+
return ctx.apiUrl;
|
|
105
|
+
}
|
|
106
|
+
logger.warn({
|
|
107
|
+
sidPrefix: redactSessionIdForLog(ctx?.sessionId),
|
|
108
|
+
header: ctx?.header,
|
|
109
|
+
apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
|
|
110
|
+
}, "getEffectiveApiUrl: No context or apiUrl found, falling back to default");
|
|
111
|
+
}
|
|
112
|
+
return getDefaultApiUrl();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the effective project allowlist for the current request
|
|
116
|
+
* In REMOTE_AUTHORIZATION mode with ENABLE_DYNAMIC_PROJECT_SCOPE, reads the scope
|
|
117
|
+
* narrowed via the X-GitLab-Allowed-Project-Ids header from session context
|
|
118
|
+
* (already validated against GITLAB_ALLOWED_PROJECT_IDS by parseAuthHeaders)
|
|
119
|
+
* Otherwise, uses environment GITLAB_ALLOWED_PROJECT_IDS
|
|
120
|
+
*/
|
|
121
|
+
export function getEffectiveAllowedProjectIds() {
|
|
122
|
+
const { getDefaultAllowedProjectIds } = requireDeps();
|
|
123
|
+
if (ENABLE_DYNAMIC_PROJECT_SCOPE) {
|
|
124
|
+
const ctx = sessionAuthStore.getStore();
|
|
125
|
+
if (ctx?.allowedProjectIds && ctx.allowedProjectIds.length > 0) {
|
|
126
|
+
return ctx.allowedProjectIds;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return getDefaultAllowedProjectIds();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Keep a dynamic project scope bound to its session
|
|
133
|
+
* A request that omits X-GitLab-Allowed-Project-Ids keeps the session scope, and a
|
|
134
|
+
* request naming a different scope is refused (null). An unscoped session may be
|
|
135
|
+
* narrowed. Re-initialize to change the scope.
|
|
136
|
+
*/
|
|
137
|
+
export function bindSessionProjectScope(requested, sessionScope) {
|
|
138
|
+
if (!sessionScope || sessionScope.length === 0) {
|
|
139
|
+
return requested;
|
|
140
|
+
}
|
|
141
|
+
if (requested === undefined) {
|
|
142
|
+
return sessionScope;
|
|
143
|
+
}
|
|
144
|
+
const want = new Set(requested);
|
|
145
|
+
const have = new Set(sessionScope);
|
|
146
|
+
return want.size === have.size && [...want].every(id => have.has(id)) ? sessionScope : null;
|
|
147
|
+
}
|
|
148
|
+
export const SESSION_PROJECT_SCOPE_MISMATCH = {
|
|
149
|
+
error: "Project scope mismatch",
|
|
150
|
+
message: "X-GitLab-Allowed-Project-Ids does not match the project scope of this session. Re-initialize to change it.",
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Constructs the fetch configuration dynamically.
|
|
154
|
+
* This function is called immediately before each `fetch` to ensure it runs
|
|
155
|
+
* within the correct AsyncLocalStorage context, capturing the necessary auth
|
|
156
|
+
* and API URL information for the current request.
|
|
157
|
+
* Uses connection pooling when dynamic API URLs are enabled.
|
|
158
|
+
*/
|
|
159
|
+
export const getFetchConfig = () => {
|
|
160
|
+
const effectiveApiUrl = getEffectiveApiUrl();
|
|
161
|
+
const dispatcher = requireDeps().getDispatcherForUrl(effectiveApiUrl);
|
|
162
|
+
return {
|
|
163
|
+
headers: { ...BASE_HEADERS, ...buildAuthHeaders() },
|
|
164
|
+
dispatcher,
|
|
165
|
+
};
|
|
166
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.3.8-next.
|
|
3
|
+
"version": "2.3.8-next.149",
|
|
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": [
|