@zereight/mcp-gitlab 2.0.33 → 2.0.34
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.md +12 -1
- package/build/gitlab-client-pool.js +108 -6
- package/build/index.js +110 -59
- package/build/oauth.js +11 -6
- package/build/schemas.js +3 -0
- package/build/test/no-proxy-integration-test.js +183 -0
- package/build/test/no-proxy-test.js +138 -0
- package/build/test/remote-auth-simple-test.js +12 -1
- package/build/test/test-geteffectiveprojectid.js +236 -0
- package/build/test/test-upload-markdown.js +148 -0
- package/build/test/utils/mock-gitlab-server.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -336,7 +336,7 @@ docker run -i --rm \
|
|
|
336
336
|
- `GITLAB_OAUTH_REDIRECT_URI`: The OAuth callback URL. Default: `http://127.0.0.1:8888/callback`
|
|
337
337
|
- `GITLAB_OAUTH_TOKEN_PATH`: Custom path to store the OAuth token. Default: `~/.gitlab-mcp-token.json`
|
|
338
338
|
- `REMOTE_AUTHORIZATION`: When set to 'true', enables remote per-session authorization via HTTP headers. In this mode:
|
|
339
|
-
- The server accepts GitLab PAT tokens from HTTP headers (`Authorization: Bearer <token>` or `
|
|
339
|
+
- The server accepts GitLab PAT tokens from HTTP headers (`Authorization: Bearer <token>`, `Private-Token: <token>` or `Job-Token: <token>`) on a per-session basis
|
|
340
340
|
- `GITLAB_PERSONAL_ACCESS_TOKEN` environment variable is **not required** and ignored
|
|
341
341
|
- Only works with **Streamable HTTP transport** (`STREAMABLE_HTTP=true`) because session management was already handled by the transport layer
|
|
342
342
|
- **SSE transport is disabled** - attempting to use SSE with remote authorization will cause the server to exit with an error
|
|
@@ -400,6 +400,7 @@ docker run -i --rm \
|
|
|
400
400
|
- `SSE`: When set to 'true', enables the Server-Sent Events transport.
|
|
401
401
|
- `STREAMABLE_HTTP`: When set to 'true', enables the Streamable HTTP transport. If both **SSE** and **STREAMABLE_HTTP** are set to 'true', the server will prioritize Streamable HTTP over SSE transport.
|
|
402
402
|
- `GITLAB_COMMIT_FILES_PER_PAGE`: The number of files per page that GitLab returns for commit diffs. This value should match the server-side GitLab setting. Adjust this if your GitLab instance uses a custom per-page value for commit diffs.
|
|
403
|
+
- `GITLAB_REPO_FILE_ENCODING`: Encoding for repository file create/update and related commit payloads sent to the GitLab API. Use `text` (default) or `base64`. Equivalent CLI: `--repo-file-encoding=text|base64`.
|
|
403
404
|
|
|
404
405
|
#### Performance & Security Configuration
|
|
405
406
|
|
|
@@ -407,6 +408,16 @@ docker run -i --rm \
|
|
|
407
408
|
- `MAX_SESSIONS`: Maximum number of concurrent sessions allowed. Default: `1000`. Valid range: 1-10000. When limit is reached, new connections are rejected with HTTP 503.
|
|
408
409
|
- `MAX_REQUESTS_PER_MINUTE`: Rate limit per session in requests per minute. Default: `60`. Valid range: 1-1000. Exceeded requests return HTTP 429.
|
|
409
410
|
- `PORT`: Server port. Default: `3002`. Valid range: 1-65535.
|
|
411
|
+
- `HTTP_PROXY`: HTTP proxy server URL for outgoing requests. Example: `http://proxy.example.com:8080`. Supports HTTP/HTTPS and SOCKS proxies (URLs starting with `socks://` or `socks5://`). CLI arg: `--http-proxy`
|
|
412
|
+
- `HTTPS_PROXY`: HTTPS proxy server URL for outgoing requests. Example: `https://proxy.example.com:8080`. Supports HTTP/HTTPS and SOCKS proxies. CLI arg: `--https-proxy`
|
|
413
|
+
- `NO_PROXY`: Comma-separated list of hosts that should bypass the proxy. Supports:
|
|
414
|
+
- Exact hostname matches (e.g., `localhost`, `gitlab.internal.com`)
|
|
415
|
+
- Domain suffix matches (e.g., `.internal.com` matches any subdomain)
|
|
416
|
+
- IP addresses (e.g., `127.0.0.1`, `192.168.1.1`)
|
|
417
|
+
- Port-specific matches (e.g., `example.com:443`)
|
|
418
|
+
- Wildcard `*` to bypass proxy for all hosts
|
|
419
|
+
- Example: `NO_PROXY=localhost,127.0.0.1,.internal.com`
|
|
420
|
+
- CLI arg: `--no-proxy`
|
|
410
421
|
|
|
411
422
|
#### Monitoring Endpoints
|
|
412
423
|
|
|
@@ -4,6 +4,64 @@ import { HttpProxyAgent } from "http-proxy-agent";
|
|
|
4
4
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
5
5
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
6
6
|
import fs from "fs";
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a URL should bypass the proxy based on NO_PROXY patterns.
|
|
9
|
+
* Supports:
|
|
10
|
+
* - Exact hostname matches (e.g., "localhost", "gitlab.example.com")
|
|
11
|
+
* - Domain suffix matches (e.g., ".example.com" matches "gitlab.example.com")
|
|
12
|
+
* - IP addresses (e.g., "127.0.0.1", "192.168.1.1")
|
|
13
|
+
* - Wildcard "*" to bypass all proxies
|
|
14
|
+
* - Port-specific matches (e.g., "example.com:8080")
|
|
15
|
+
*
|
|
16
|
+
* @param url The URL to check
|
|
17
|
+
* @param noProxy Comma-separated list of patterns from NO_PROXY
|
|
18
|
+
* @returns true if the URL should bypass the proxy, false otherwise
|
|
19
|
+
*/
|
|
20
|
+
function shouldBypassProxy(url, noProxy) {
|
|
21
|
+
if (!noProxy) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Parse URL to get hostname and port
|
|
25
|
+
let hostname;
|
|
26
|
+
let port;
|
|
27
|
+
let protocol;
|
|
28
|
+
try {
|
|
29
|
+
const parsedUrl = new URL(url);
|
|
30
|
+
hostname = parsedUrl.hostname.toLowerCase();
|
|
31
|
+
protocol = parsedUrl.protocol;
|
|
32
|
+
// Use explicit port if provided, otherwise use default port based on protocol
|
|
33
|
+
port = parsedUrl.port || (protocol === 'https:' ? '443' : '80');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Split NO_PROXY into patterns and trim whitespace
|
|
39
|
+
const patterns = noProxy.split(',').map(p => p.trim().toLowerCase()).filter(p => p.length > 0);
|
|
40
|
+
for (const pattern of patterns) {
|
|
41
|
+
// Wildcard matches everything
|
|
42
|
+
if (pattern === '*') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Handle port-specific patterns (e.g., "example.com:8080")
|
|
46
|
+
const [patternHost, patternPort] = pattern.split(':');
|
|
47
|
+
// If pattern specifies a port, check if it matches
|
|
48
|
+
if (patternPort && port !== patternPort) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Check for domain suffix match (e.g., ".example.com")
|
|
52
|
+
if (patternHost.startsWith('.')) {
|
|
53
|
+
const suffix = patternHost.substring(1);
|
|
54
|
+
if (hostname === suffix || hostname.endsWith('.' + suffix)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Check for exact hostname match
|
|
59
|
+
else if (hostname === patternHost) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
7
65
|
/**
|
|
8
66
|
* Manages a pool of HTTP/HTTPS agents for different GitLab API URLs.
|
|
9
67
|
* This allows the server to efficiently handle requests to multiple GitLab instances
|
|
@@ -23,7 +81,7 @@ export class GitLabClientPool {
|
|
|
23
81
|
* @returns A `ClientAgents` object containing the configured agents.
|
|
24
82
|
*/
|
|
25
83
|
createAgentsForUrl(apiUrl) {
|
|
26
|
-
const { httpProxy, httpsProxy, rejectUnauthorized, caCertPath } = this.options;
|
|
84
|
+
const { httpProxy, httpsProxy, noProxy, rejectUnauthorized, caCertPath } = this.options;
|
|
27
85
|
const url = new URL(apiUrl);
|
|
28
86
|
let sslOptions = {};
|
|
29
87
|
if (rejectUnauthorized === false) {
|
|
@@ -38,10 +96,12 @@ export class GitLabClientPool {
|
|
|
38
96
|
throw new Error(`Failed to read CA certificate: ${caCertPath}`);
|
|
39
97
|
}
|
|
40
98
|
}
|
|
99
|
+
// Check if this URL should bypass the proxy
|
|
100
|
+
const bypassProxy = shouldBypassProxy(apiUrl, noProxy);
|
|
41
101
|
let httpAgent;
|
|
42
102
|
let httpsAgent;
|
|
43
|
-
// Configure HTTP agent with proxy if specified
|
|
44
|
-
if (httpProxy) {
|
|
103
|
+
// Configure HTTP agent with proxy if specified and not bypassed
|
|
104
|
+
if (httpProxy && !bypassProxy) {
|
|
45
105
|
httpAgent = httpProxy.startsWith("socks")
|
|
46
106
|
? new SocksProxyAgent(httpProxy)
|
|
47
107
|
: new HttpProxyAgent(httpProxy);
|
|
@@ -49,8 +109,8 @@ export class GitLabClientPool {
|
|
|
49
109
|
else {
|
|
50
110
|
httpAgent = new Agent({ keepAlive: true });
|
|
51
111
|
}
|
|
52
|
-
// Configure HTTPS agent with proxy and SSL options if specified
|
|
53
|
-
if (httpsProxy) {
|
|
112
|
+
// Configure HTTPS agent with proxy and SSL options if specified and not bypassed
|
|
113
|
+
if (httpsProxy && !bypassProxy) {
|
|
54
114
|
httpsAgent = httpsProxy.startsWith("socks")
|
|
55
115
|
// The `as any` cast is used here to bypass a TypeScript type mismatch error.
|
|
56
116
|
// The `socks-proxy-agent` documentation indicates that TLS options like
|
|
@@ -72,6 +132,31 @@ export class GitLabClientPool {
|
|
|
72
132
|
* @returns The corresponding `Agent` for the URL's protocol.
|
|
73
133
|
*/
|
|
74
134
|
getOrCreateAgentForUrl(apiUrl) {
|
|
135
|
+
const agents = this.getOrCreateAgentsForUrl(apiUrl);
|
|
136
|
+
const url = new URL(apiUrl);
|
|
137
|
+
return url.protocol === "https:" ? agents.httpsAgent : agents.httpAgent;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns an agent-selection function for use with node-fetch's `agent` option.
|
|
141
|
+
* The returned function picks the correct HTTP or HTTPS agent based on the
|
|
142
|
+
* request URL's protocol. This is critical for self-hosted GitLab instances
|
|
143
|
+
* where the server may redirect between HTTP and HTTPS (e.g., when
|
|
144
|
+
* `external_url` differs from the actual internal protocol).
|
|
145
|
+
* @param apiUrl The base API URL used to look up or create the agent pair.
|
|
146
|
+
* @returns A function `(parsedURL: URL) => Agent` suitable for node-fetch.
|
|
147
|
+
*/
|
|
148
|
+
getAgentFunctionForUrl(apiUrl) {
|
|
149
|
+
const agents = this.getOrCreateAgentsForUrl(apiUrl);
|
|
150
|
+
return (parsedURL) => {
|
|
151
|
+
return parsedURL.protocol === "https:" ? agents.httpsAgent : agents.httpAgent;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Ensures agents exist for the given API URL and returns the pair.
|
|
156
|
+
* @param apiUrl The full URL of the request.
|
|
157
|
+
* @returns The `ClientAgents` (both HTTP and HTTPS agents) for the URL.
|
|
158
|
+
*/
|
|
159
|
+
getOrCreateAgentsForUrl(apiUrl) {
|
|
75
160
|
const url = new URL(apiUrl);
|
|
76
161
|
const baseUrl = `${url.protocol}//${url.host}${url.pathname.substring(0, url.pathname.lastIndexOf('/api/v4') + '/api/v4'.length)}`;
|
|
77
162
|
if (!this.clients.has(baseUrl)) {
|
|
@@ -86,7 +171,7 @@ export class GitLabClientPool {
|
|
|
86
171
|
// This should not happen given the logic above, but it satisfies TypeScript
|
|
87
172
|
throw new Error(`Failed to create or get client for URL: ${baseUrl}`);
|
|
88
173
|
}
|
|
89
|
-
return
|
|
174
|
+
return agents;
|
|
90
175
|
}
|
|
91
176
|
/**
|
|
92
177
|
* Retrieves the client agents for a specific base API URL.
|
|
@@ -110,4 +195,21 @@ export class GitLabClientPool {
|
|
|
110
195
|
}
|
|
111
196
|
return this.clients.get(defaultUrl);
|
|
112
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Destroy all pooled agents and clear pool state.
|
|
200
|
+
* This should be called on graceful shutdown so sockets are closed
|
|
201
|
+
* and the process can exit cleanly.
|
|
202
|
+
*/
|
|
203
|
+
closeAll() {
|
|
204
|
+
for (const [, agents] of this.clients) {
|
|
205
|
+
const destroyIfSupported = (agent) => {
|
|
206
|
+
if (agent && typeof agent.destroy === "function") {
|
|
207
|
+
agent.destroy();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
destroyIfSupported(agents.httpAgent);
|
|
211
|
+
destroyIfSupported(agents.httpsAgent);
|
|
212
|
+
}
|
|
213
|
+
this.clients.clear();
|
|
214
|
+
}
|
|
113
215
|
}
|
package/build/index.js
CHANGED
|
@@ -50,7 +50,7 @@ GitLabDiscussionNoteSchema, // Added
|
|
|
50
50
|
GitLabDiscussionSchema,
|
|
51
51
|
// Draft Notes Schemas
|
|
52
52
|
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
53
|
-
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
|
|
53
|
+
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
|
|
54
54
|
import { randomUUID } from "node:crypto";
|
|
55
55
|
import { pino } from "pino";
|
|
56
56
|
const logger = pino({
|
|
@@ -314,6 +314,7 @@ const PORT = Number.parseInt(getConfig("port", "PORT", "3002"), 10);
|
|
|
314
314
|
// Add proxy configuration
|
|
315
315
|
const HTTP_PROXY = getConfig("http-proxy", "HTTP_PROXY");
|
|
316
316
|
const HTTPS_PROXY = getConfig("https-proxy", "HTTPS_PROXY");
|
|
317
|
+
const NO_PROXY = getConfig("no-proxy", "NO_PROXY");
|
|
317
318
|
const NODE_TLS_REJECT_UNAUTHORIZED = getConfig("tls-reject-unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED");
|
|
318
319
|
const GITLAB_CA_CERT_PATH = getConfig("ca-cert-path", "GITLAB_CA_CERT_PATH");
|
|
319
320
|
const GITLAB_POOL_MAX_SIZE = getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE")
|
|
@@ -355,6 +356,7 @@ const clientPool = new GitLabClientPool({
|
|
|
355
356
|
.map(normalizeGitLabApiUrl),
|
|
356
357
|
httpProxy: HTTP_PROXY,
|
|
357
358
|
httpsProxy: HTTPS_PROXY,
|
|
359
|
+
noProxy: NO_PROXY,
|
|
358
360
|
rejectUnauthorized: NODE_TLS_REJECT_UNAUTHORIZED !== "0",
|
|
359
361
|
caCertPath: GITLAB_CA_CERT_PATH,
|
|
360
362
|
poolMaxSize: GITLAB_POOL_MAX_SIZE,
|
|
@@ -535,7 +537,7 @@ function getEffectiveApiUrl() {
|
|
|
535
537
|
*/
|
|
536
538
|
const getFetchConfig = () => {
|
|
537
539
|
const effectiveApiUrl = getEffectiveApiUrl();
|
|
538
|
-
const agent = clientPool.
|
|
540
|
+
const agent = clientPool.getAgentFunctionForUrl(effectiveApiUrl);
|
|
539
541
|
return {
|
|
540
542
|
headers: { ...BASE_HEADERS, ...buildAuthHeaders() },
|
|
541
543
|
agent: agent,
|
|
@@ -603,6 +605,11 @@ const allTools = [
|
|
|
603
605
|
description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
|
|
604
606
|
inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
|
|
605
607
|
},
|
|
608
|
+
{
|
|
609
|
+
name: "get_merge_request_conflicts",
|
|
610
|
+
description: "Get the conflicts of a merge request in a GitLab project",
|
|
611
|
+
inputSchema: toJSONSchema(GetMergeRequestConflictsSchema),
|
|
612
|
+
},
|
|
606
613
|
{
|
|
607
614
|
name: "execute_graphql",
|
|
608
615
|
description: "Execute a GitLab GraphQL query",
|
|
@@ -1235,6 +1242,7 @@ const readOnlyTools = new Set([
|
|
|
1235
1242
|
"get_release",
|
|
1236
1243
|
"download_release_asset",
|
|
1237
1244
|
"get_merge_request_approval_state",
|
|
1245
|
+
"get_merge_request_conflicts",
|
|
1238
1246
|
"list_webhooks",
|
|
1239
1247
|
"list_webhook_events",
|
|
1240
1248
|
"get_webhook_event",
|
|
@@ -1291,6 +1299,7 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1291
1299
|
"approve_merge_request",
|
|
1292
1300
|
"unapprove_merge_request",
|
|
1293
1301
|
"get_merge_request_approval_state",
|
|
1302
|
+
"get_merge_request_conflicts",
|
|
1294
1303
|
"get_merge_request",
|
|
1295
1304
|
"get_merge_request_diffs",
|
|
1296
1305
|
"list_merge_request_diffs",
|
|
@@ -1583,6 +1592,9 @@ const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split
|
|
|
1583
1592
|
const GITLAB_COMMIT_FILES_PER_PAGE = process.env.GITLAB_COMMIT_FILES_PER_PAGE
|
|
1584
1593
|
? Number.parseInt(process.env.GITLAB_COMMIT_FILES_PER_PAGE, 10)
|
|
1585
1594
|
: 20;
|
|
1595
|
+
const GITLAB_REPO_FILE_ENCODING = getConfig("repo-file-encoding", "GITLAB_REPO_FILE_ENCODING", "text") === "base64"
|
|
1596
|
+
? "base64"
|
|
1597
|
+
: "text";
|
|
1586
1598
|
// Validate authentication configuration
|
|
1587
1599
|
if (REMOTE_AUTHORIZATION) {
|
|
1588
1600
|
// Remote authorization mode: token comes from HTTP headers
|
|
@@ -1598,7 +1610,7 @@ if (REMOTE_AUTHORIZATION) {
|
|
|
1598
1610
|
}
|
|
1599
1611
|
logger.info("Remote authorization enabled: tokens will be read from HTTP headers");
|
|
1600
1612
|
}
|
|
1601
|
-
else if (!USE_OAUTH && !GITLAB_PERSONAL_ACCESS_TOKEN && !GITLAB_AUTH_COOKIE_PATH) {
|
|
1613
|
+
else if (!USE_OAUTH && !GITLAB_PERSONAL_ACCESS_TOKEN && !GITLAB_JOB_TOKEN && !GITLAB_AUTH_COOKIE_PATH) {
|
|
1602
1614
|
// Standard mode: token must be in environment (unless using OAuth)
|
|
1603
1615
|
logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
|
|
1604
1616
|
logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
|
|
@@ -1647,7 +1659,14 @@ function getEffectiveProjectId(projectId) {
|
|
|
1647
1659
|
}
|
|
1648
1660
|
return projectId || GITLAB_ALLOWED_PROJECT_IDS[0];
|
|
1649
1661
|
}
|
|
1650
|
-
|
|
1662
|
+
// Prioritize the passed projectId over GITLAB_PROJECT_ID to allow querying different projects
|
|
1663
|
+
if (projectId) {
|
|
1664
|
+
return projectId;
|
|
1665
|
+
}
|
|
1666
|
+
if (GITLAB_PROJECT_ID) {
|
|
1667
|
+
return GITLAB_PROJECT_ID;
|
|
1668
|
+
}
|
|
1669
|
+
throw new Error("No project ID provided and GITLAB_PROJECT_ID is not set");
|
|
1651
1670
|
}
|
|
1652
1671
|
/**
|
|
1653
1672
|
* Create a fork of a GitLab project
|
|
@@ -2344,6 +2363,12 @@ async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body)
|
|
|
2344
2363
|
const data = await response.json();
|
|
2345
2364
|
return GitLabDiscussionNoteSchema.parse(data);
|
|
2346
2365
|
}
|
|
2366
|
+
function encodeRepoFilePayloadContent(content) {
|
|
2367
|
+
if (GITLAB_REPO_FILE_ENCODING === "base64") {
|
|
2368
|
+
return Buffer.from(content).toString("base64");
|
|
2369
|
+
}
|
|
2370
|
+
return content;
|
|
2371
|
+
}
|
|
2347
2372
|
/**
|
|
2348
2373
|
* Create or update a file in a GitLab project
|
|
2349
2374
|
* 파일 생성 또는 업데이트
|
|
@@ -2362,9 +2387,9 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
|
|
|
2362
2387
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
|
|
2363
2388
|
const body = {
|
|
2364
2389
|
branch,
|
|
2365
|
-
content,
|
|
2390
|
+
content: encodeRepoFilePayloadContent(content),
|
|
2366
2391
|
commit_message: commitMessage,
|
|
2367
|
-
encoding:
|
|
2392
|
+
encoding: GITLAB_REPO_FILE_ENCODING,
|
|
2368
2393
|
...(previousPath ? { previous_path: previousPath } : {}),
|
|
2369
2394
|
};
|
|
2370
2395
|
// Check if file exists
|
|
@@ -2436,8 +2461,8 @@ async function createTree(projectId, files, ref) {
|
|
|
2436
2461
|
body: JSON.stringify({
|
|
2437
2462
|
files: files.map(file => ({
|
|
2438
2463
|
file_path: file.path,
|
|
2439
|
-
content: file.content,
|
|
2440
|
-
encoding:
|
|
2464
|
+
content: encodeRepoFilePayloadContent(file.content),
|
|
2465
|
+
encoding: GITLAB_REPO_FILE_ENCODING,
|
|
2441
2466
|
})),
|
|
2442
2467
|
}),
|
|
2443
2468
|
});
|
|
@@ -2474,8 +2499,8 @@ async function createCommit(projectId, message, branch, actions) {
|
|
|
2474
2499
|
actions: actions.map(action => ({
|
|
2475
2500
|
action: "create",
|
|
2476
2501
|
file_path: action.path,
|
|
2477
|
-
content: action.content,
|
|
2478
|
-
encoding:
|
|
2502
|
+
content: encodeRepoFilePayloadContent(action.content),
|
|
2503
|
+
encoding: GITLAB_REPO_FILE_ENCODING,
|
|
2479
2504
|
})),
|
|
2480
2505
|
}),
|
|
2481
2506
|
});
|
|
@@ -3007,6 +3032,23 @@ async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
|
|
|
3007
3032
|
source_endpoint: "approval_state",
|
|
3008
3033
|
};
|
|
3009
3034
|
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Get the conflicts of a merge request
|
|
3037
|
+
*
|
|
3038
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3039
|
+
* @param {string | number} mergeRequestIid - The internal ID of the merge request
|
|
3040
|
+
* @returns {Promise<Record<string, unknown>>} The merge request conflicts
|
|
3041
|
+
*/
|
|
3042
|
+
async function getMergeRequestConflicts(projectId, mergeRequestIid) {
|
|
3043
|
+
projectId = decodeURIComponent(projectId);
|
|
3044
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/conflicts`);
|
|
3045
|
+
const response = await fetch(url.toString(), {
|
|
3046
|
+
...getFetchConfig(),
|
|
3047
|
+
method: "GET",
|
|
3048
|
+
});
|
|
3049
|
+
await handleGitLabError(response);
|
|
3050
|
+
return (await response.json());
|
|
3051
|
+
}
|
|
3010
3052
|
async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
|
|
3011
3053
|
const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approvals`);
|
|
3012
3054
|
const approvalsResponse = await fetch(approvalsUrl.toString(), {
|
|
@@ -3088,7 +3130,7 @@ noteableIid, body) {
|
|
|
3088
3130
|
* @returns {Promise<GitLabDraftNote[]>} Array of draft notes
|
|
3089
3131
|
*/
|
|
3090
3132
|
async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
|
|
3091
|
-
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(project_id)}/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}
|
|
3133
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(project_id)}/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}`, { ...getFetchConfig() });
|
|
3092
3134
|
if (!response.ok) {
|
|
3093
3135
|
const errorText = await response.text();
|
|
3094
3136
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
@@ -4181,11 +4223,8 @@ async function createPipeline(projectId, ref, variables, inputs) {
|
|
|
4181
4223
|
body.inputs = inputs;
|
|
4182
4224
|
}
|
|
4183
4225
|
const response = await fetch(url.toString(), {
|
|
4226
|
+
...getFetchConfig(),
|
|
4184
4227
|
method: "POST",
|
|
4185
|
-
headers: {
|
|
4186
|
-
...BASE_HEADERS,
|
|
4187
|
-
...buildAuthHeaders(),
|
|
4188
|
-
},
|
|
4189
4228
|
body: JSON.stringify(body),
|
|
4190
4229
|
});
|
|
4191
4230
|
await handleGitLabError(response);
|
|
@@ -4203,11 +4242,8 @@ async function retryPipeline(projectId, pipelineId) {
|
|
|
4203
4242
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4204
4243
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/retry`);
|
|
4205
4244
|
const response = await fetch(url.toString(), {
|
|
4245
|
+
...getFetchConfig(),
|
|
4206
4246
|
method: "POST",
|
|
4207
|
-
headers: {
|
|
4208
|
-
...BASE_HEADERS,
|
|
4209
|
-
...buildAuthHeaders(),
|
|
4210
|
-
},
|
|
4211
4247
|
});
|
|
4212
4248
|
await handleGitLabError(response);
|
|
4213
4249
|
const data = await response.json();
|
|
@@ -4224,11 +4260,8 @@ async function cancelPipeline(projectId, pipelineId) {
|
|
|
4224
4260
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4225
4261
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/cancel`);
|
|
4226
4262
|
const response = await fetch(url.toString(), {
|
|
4263
|
+
...getFetchConfig(),
|
|
4227
4264
|
method: "POST",
|
|
4228
|
-
headers: {
|
|
4229
|
-
...BASE_HEADERS,
|
|
4230
|
-
...buildAuthHeaders(),
|
|
4231
|
-
},
|
|
4232
4265
|
});
|
|
4233
4266
|
await handleGitLabError(response);
|
|
4234
4267
|
const data = await response.json();
|
|
@@ -4319,13 +4352,7 @@ async function getRepositoryTree(options) {
|
|
|
4319
4352
|
queryParams.append("page_token", options.page_token);
|
|
4320
4353
|
if (options.pagination)
|
|
4321
4354
|
queryParams.append("pagination", options.pagination);
|
|
4322
|
-
const
|
|
4323
|
-
...BASE_HEADERS,
|
|
4324
|
-
...buildAuthHeaders(),
|
|
4325
|
-
};
|
|
4326
|
-
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, {
|
|
4327
|
-
headers,
|
|
4328
|
-
});
|
|
4355
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, { ...getFetchConfig() });
|
|
4329
4356
|
if (response.status === 404) {
|
|
4330
4357
|
throw new Error("Repository or path not found");
|
|
4331
4358
|
}
|
|
@@ -4786,15 +4813,12 @@ async function markdownUpload(projectId, filePath) {
|
|
|
4786
4813
|
contentType: "application/octet-stream",
|
|
4787
4814
|
});
|
|
4788
4815
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
|
|
4816
|
+
const defaultFetchConfig = getFetchConfig();
|
|
4817
|
+
delete defaultFetchConfig.headers["Content-Type"]; // Let form-data set the correct Content-Type with boundary
|
|
4789
4818
|
const response = await fetch(url.toString(), {
|
|
4819
|
+
...defaultFetchConfig,
|
|
4790
4820
|
method: "POST",
|
|
4791
|
-
|
|
4792
|
-
...BASE_HEADERS,
|
|
4793
|
-
...buildAuthHeaders(),
|
|
4794
|
-
// Remove Content-Type header to let form-data set it with boundary
|
|
4795
|
-
"Content-Type": undefined,
|
|
4796
|
-
},
|
|
4797
|
-
body: form,
|
|
4821
|
+
body: form
|
|
4798
4822
|
});
|
|
4799
4823
|
if (!response.ok) {
|
|
4800
4824
|
await handleGitLabError(response);
|
|
@@ -4820,11 +4844,8 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
|
|
|
4820
4844
|
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
4821
4845
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`);
|
|
4822
4846
|
const response = await fetch(url.toString(), {
|
|
4847
|
+
...getFetchConfig(),
|
|
4823
4848
|
method: "GET",
|
|
4824
|
-
headers: {
|
|
4825
|
-
...BASE_HEADERS,
|
|
4826
|
-
...buildAuthHeaders(),
|
|
4827
|
-
},
|
|
4828
4849
|
});
|
|
4829
4850
|
if (!response.ok) {
|
|
4830
4851
|
await handleGitLabError(response);
|
|
@@ -4871,13 +4892,7 @@ async function listEvents(options = {}) {
|
|
|
4871
4892
|
url.searchParams.append(key, value.toString());
|
|
4872
4893
|
}
|
|
4873
4894
|
});
|
|
4874
|
-
const response = await fetch(url.toString(), {
|
|
4875
|
-
method: "GET",
|
|
4876
|
-
headers: {
|
|
4877
|
-
...BASE_HEADERS,
|
|
4878
|
-
...buildAuthHeaders(),
|
|
4879
|
-
},
|
|
4880
|
-
});
|
|
4895
|
+
const response = await fetch(url.toString(), { ...getFetchConfig() });
|
|
4881
4896
|
if (!response.ok) {
|
|
4882
4897
|
await handleGitLabError(response);
|
|
4883
4898
|
}
|
|
@@ -4899,13 +4914,7 @@ async function getProjectEvents(projectId, options = {}) {
|
|
|
4899
4914
|
url.searchParams.append(key, value.toString());
|
|
4900
4915
|
}
|
|
4901
4916
|
});
|
|
4902
|
-
const response = await fetch(url.toString(), {
|
|
4903
|
-
method: "GET",
|
|
4904
|
-
headers: {
|
|
4905
|
-
...BASE_HEADERS,
|
|
4906
|
-
...buildAuthHeaders(),
|
|
4907
|
-
},
|
|
4908
|
-
});
|
|
4917
|
+
const response = await fetch(url.toString(), { ...getFetchConfig() });
|
|
4909
4918
|
if (!response.ok) {
|
|
4910
4919
|
await handleGitLabError(response);
|
|
4911
4920
|
}
|
|
@@ -5396,6 +5405,13 @@ async function handleToolCall(params) {
|
|
|
5396
5405
|
content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
|
|
5397
5406
|
};
|
|
5398
5407
|
}
|
|
5408
|
+
case "get_merge_request_conflicts": {
|
|
5409
|
+
const args = GetMergeRequestConflictsSchema.parse(params.arguments);
|
|
5410
|
+
const conflicts = await getMergeRequestConflicts(args.project_id, args.merge_request_iid);
|
|
5411
|
+
return {
|
|
5412
|
+
content: [{ type: "text", text: JSON.stringify(conflicts, null, 2) }],
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5399
5415
|
case "mr_discussions": {
|
|
5400
5416
|
const args = ListMergeRequestDiscussionsSchema.parse(params.arguments);
|
|
5401
5417
|
const { project_id, merge_request_iid, ...options } = args;
|
|
@@ -6306,6 +6322,10 @@ function determineTransportMode() {
|
|
|
6306
6322
|
async function startStdioServer() {
|
|
6307
6323
|
const serverInstance = createServer();
|
|
6308
6324
|
const transport = new StdioServerTransport();
|
|
6325
|
+
transport.onclose = () => {
|
|
6326
|
+
logger.info("Stdio transport closed, releasing client pool");
|
|
6327
|
+
clientPool.closeAll();
|
|
6328
|
+
};
|
|
6309
6329
|
await serverInstance.connect(transport);
|
|
6310
6330
|
}
|
|
6311
6331
|
/**
|
|
@@ -6314,6 +6334,7 @@ async function startStdioServer() {
|
|
|
6314
6334
|
async function startSSEServer() {
|
|
6315
6335
|
const app = express();
|
|
6316
6336
|
const transports = {};
|
|
6337
|
+
let shuttingDown = false;
|
|
6317
6338
|
app.get("/sse", async (_, res) => {
|
|
6318
6339
|
const serverInstance = createServer();
|
|
6319
6340
|
const transport = new SSEServerTransport("/messages", res);
|
|
@@ -6340,12 +6361,35 @@ async function startSSEServer() {
|
|
|
6340
6361
|
transport: TransportMode.SSE,
|
|
6341
6362
|
});
|
|
6342
6363
|
});
|
|
6343
|
-
app.listen(Number(PORT), HOST, () => {
|
|
6364
|
+
const httpServer = app.listen(Number(PORT), HOST, () => {
|
|
6344
6365
|
logger.info(`GitLab MCP Server running with SSE transport`);
|
|
6345
6366
|
const colorGreen = "\x1b[32m";
|
|
6346
6367
|
const colorReset = "\x1b[0m";
|
|
6347
6368
|
logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/sse${colorReset}`);
|
|
6348
6369
|
});
|
|
6370
|
+
const shutdown = async (signal) => {
|
|
6371
|
+
if (shuttingDown)
|
|
6372
|
+
return;
|
|
6373
|
+
shuttingDown = true;
|
|
6374
|
+
logger.info(`${signal} received, shutting down SSE server...`);
|
|
6375
|
+
httpServer.close(() => logger.info("SSE HTTP server closed"));
|
|
6376
|
+
await Promise.allSettled(Object.values(transports).map(async (transport) => {
|
|
6377
|
+
try {
|
|
6378
|
+
await transport.close();
|
|
6379
|
+
}
|
|
6380
|
+
catch (error) {
|
|
6381
|
+
logger.error("Error closing SSE transport:", error);
|
|
6382
|
+
}
|
|
6383
|
+
}));
|
|
6384
|
+
clientPool.closeAll();
|
|
6385
|
+
process.exit(0);
|
|
6386
|
+
};
|
|
6387
|
+
process.on("SIGTERM", () => {
|
|
6388
|
+
void shutdown("SIGTERM");
|
|
6389
|
+
});
|
|
6390
|
+
process.on("SIGINT", () => {
|
|
6391
|
+
void shutdown("SIGINT");
|
|
6392
|
+
});
|
|
6349
6393
|
}
|
|
6350
6394
|
/**
|
|
6351
6395
|
* Start server with Streamable HTTP transport
|
|
@@ -6399,10 +6443,12 @@ async function startStreamableHTTPServer() {
|
|
|
6399
6443
|
/**
|
|
6400
6444
|
* Parse authentication from request headers
|
|
6401
6445
|
* Returns null if no auth found or invalid format
|
|
6446
|
+
* Supports: JOB-TOKEN header, Private-Token header, Authorization Bearer header
|
|
6402
6447
|
*/
|
|
6403
6448
|
const parseAuthHeaders = (req) => {
|
|
6404
6449
|
const authHeader = req.headers["authorization"] || "";
|
|
6405
6450
|
const privateToken = req.headers["private-token"] || "";
|
|
6451
|
+
const jobToken = req.headers["job-token"] || "";
|
|
6406
6452
|
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
|
|
6407
6453
|
let apiUrl = GITLAB_API_URL; // Default API URL
|
|
6408
6454
|
// Only process dynamic URL if the feature is enabled
|
|
@@ -6419,7 +6465,11 @@ async function startStreamableHTTPServer() {
|
|
|
6419
6465
|
// Extract token
|
|
6420
6466
|
let token = null;
|
|
6421
6467
|
let header = null;
|
|
6422
|
-
if (
|
|
6468
|
+
if (jobToken) {
|
|
6469
|
+
token = jobToken.trim();
|
|
6470
|
+
header = "JOB-TOKEN";
|
|
6471
|
+
}
|
|
6472
|
+
else if (privateToken) {
|
|
6423
6473
|
token = privateToken.trim();
|
|
6424
6474
|
header = "Private-Token";
|
|
6425
6475
|
}
|
|
@@ -6507,8 +6557,8 @@ async function startStreamableHTTPServer() {
|
|
|
6507
6557
|
if (!authData) {
|
|
6508
6558
|
metrics.authFailures++;
|
|
6509
6559
|
res.status(401).json({
|
|
6510
|
-
error: "Missing Authorization
|
|
6511
|
-
message: "Remote authorization is enabled. Please provide Authorization
|
|
6560
|
+
error: "Missing Authorization, Private-Token, or JOB-TOKEN header",
|
|
6561
|
+
message: "Remote authorization is enabled. Please provide Authorization, Private-Token, or JOB-TOKEN header.",
|
|
6512
6562
|
});
|
|
6513
6563
|
return;
|
|
6514
6564
|
}
|
|
@@ -6706,6 +6756,7 @@ async function startStreamableHTTPServer() {
|
|
|
6706
6756
|
Object.keys(authTimeouts).forEach(sessionId => {
|
|
6707
6757
|
clearAuthTimeout(sessionId);
|
|
6708
6758
|
});
|
|
6759
|
+
clientPool.closeAll();
|
|
6709
6760
|
logger.info("Graceful shutdown complete");
|
|
6710
6761
|
process.exit(0);
|
|
6711
6762
|
};
|
package/build/oauth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
1
2
|
import * as fs from "fs";
|
|
2
3
|
import * as os from "os";
|
|
3
4
|
import * as path from "path";
|
|
@@ -10,7 +11,11 @@ import { pino } from "pino";
|
|
|
10
11
|
const logger = pino({
|
|
11
12
|
name: "gitlab-mcp-oauth",
|
|
12
13
|
level: process.env.LOG_LEVEL || "info",
|
|
13
|
-
});
|
|
14
|
+
}, pino.destination(2));
|
|
15
|
+
function escapeHtml(str) {
|
|
16
|
+
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
17
|
+
return String(str).replace(/[&<>"']/g, c => map[c] || c);
|
|
18
|
+
}
|
|
14
19
|
// Track pending auth requests across multiple MCP instances
|
|
15
20
|
const pendingAuthRequests = new Map();
|
|
16
21
|
/**
|
|
@@ -225,7 +230,7 @@ export class GitLabOAuth {
|
|
|
225
230
|
*/
|
|
226
231
|
async startOAuthFlow() {
|
|
227
232
|
const callbackPort = parseInt(new URL(this.config.redirectUri).port || "8888");
|
|
228
|
-
const requestId =
|
|
233
|
+
const requestId = crypto.randomUUID();
|
|
229
234
|
// Check if port is already in use
|
|
230
235
|
const portInUse = await isPortInUse(callbackPort);
|
|
231
236
|
if (portInUse) {
|
|
@@ -250,7 +255,7 @@ export class GitLabOAuth {
|
|
|
250
255
|
const requestIdToOAuthInstance = new Map();
|
|
251
256
|
return new Promise((resolve, reject) => {
|
|
252
257
|
// Create initial request
|
|
253
|
-
const state =
|
|
258
|
+
const state = crypto.randomUUID();
|
|
254
259
|
stateToRequestId.set(state, initialRequestId);
|
|
255
260
|
requestIdToOAuthInstance.set(initialRequestId, this);
|
|
256
261
|
const timeout = setTimeout(() => {
|
|
@@ -271,7 +276,7 @@ export class GitLabOAuth {
|
|
|
271
276
|
}
|
|
272
277
|
logger.info(`Received auth request from another instance: ${newRequestId}`);
|
|
273
278
|
// Create a new OAuth flow for this request
|
|
274
|
-
const newState =
|
|
279
|
+
const newState = crypto.randomUUID();
|
|
275
280
|
stateToRequestId.set(newState, newRequestId);
|
|
276
281
|
// Store a reference to use the same OAuth config
|
|
277
282
|
requestIdToOAuthInstance.set(newRequestId, this);
|
|
@@ -315,7 +320,7 @@ export class GitLabOAuth {
|
|
|
315
320
|
<html>
|
|
316
321
|
<body>
|
|
317
322
|
<h1>Authentication Failed</h1>
|
|
318
|
-
<p>Error: ${error}</p>
|
|
323
|
+
<p>Error: ${escapeHtml(String(error))}</p>
|
|
319
324
|
<p>You can close this window.</p>
|
|
320
325
|
</body>
|
|
321
326
|
</html>
|
|
@@ -523,7 +528,7 @@ export async function initializeOAuthClient(gitlabUrl = "https://gitlab.com") {
|
|
|
523
528
|
clientSecret,
|
|
524
529
|
redirectUri,
|
|
525
530
|
gitlabUrl,
|
|
526
|
-
scopes: ["api"],
|
|
531
|
+
scopes: [process.env.GITLAB_READ_ONLY_MODE === "true" ? "read_api" : "api"],
|
|
527
532
|
tokenStoragePath,
|
|
528
533
|
});
|
|
529
534
|
// Single call: triggers browser flow if needed, or reads cached token
|
package/build/schemas.js
CHANGED
|
@@ -1333,6 +1333,9 @@ export const GitLabMergeRequestApprovalStateSchema = z.object({
|
|
|
1333
1333
|
export const GetMergeRequestApprovalStateSchema = ProjectParamsSchema.extend({
|
|
1334
1334
|
merge_request_iid: z.coerce.string().describe("The IID of the merge request"),
|
|
1335
1335
|
});
|
|
1336
|
+
export const GetMergeRequestConflictsSchema = ProjectParamsSchema.extend({
|
|
1337
|
+
merge_request_iid: z.coerce.string().describe("The IID of the merge request"),
|
|
1338
|
+
});
|
|
1336
1339
|
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
1337
1340
|
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
|
|
1338
1341
|
excluded_file_patterns: z
|