@zereight/mcp-gitlab 2.0.8 → 2.0.11
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 +80 -3
- package/build/index.js +233 -27
- package/build/oauth.js +518 -0
- package/build/schemas.js +39 -4
- package/build/test/oauth-tests.js +389 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -16,7 +16,58 @@ GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements
|
|
|
16
16
|
|
|
17
17
|
When using with the Claude App, you need to set up your API key and URLs directly.
|
|
18
18
|
|
|
19
|
-
####
|
|
19
|
+
#### Authentication Methods
|
|
20
|
+
|
|
21
|
+
The server supports two authentication methods:
|
|
22
|
+
|
|
23
|
+
1. **Personal Access Token** (traditional method)
|
|
24
|
+
2. **OAuth2** (recommended for better security)
|
|
25
|
+
|
|
26
|
+
#### Using OAuth2 Authentication
|
|
27
|
+
|
|
28
|
+
OAuth2 provides a more secure authentication flow using browser-based authentication. When enabled, the server will:
|
|
29
|
+
1. Open your browser to GitLab's authorization page
|
|
30
|
+
2. Wait for you to approve the access
|
|
31
|
+
3. Store the token securely for future use
|
|
32
|
+
4. Automatically refresh the token when it expires
|
|
33
|
+
|
|
34
|
+
For detailed OAuth2 setup instructions, see [OAuth Setup Guide](./docs/oauth-setup.md).
|
|
35
|
+
|
|
36
|
+
Quick setup - first create a GitLab OAuth application:
|
|
37
|
+
|
|
38
|
+
1. Go to your GitLab instance: `Settings` → `Applications`
|
|
39
|
+
2. Create a new application with:
|
|
40
|
+
- **Name**: `GitLab MCP Server` (or any name you prefer)
|
|
41
|
+
- **Redirect URI**: `http://127.0.0.1:8888/callback`
|
|
42
|
+
- **Scopes**: Select `api` (provides complete read/write access to the API)
|
|
43
|
+
3. Copy the **Application ID** (this is your Client ID)
|
|
44
|
+
|
|
45
|
+
Then configure the MCP server with OAuth:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"gitlab": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@zereight/mcp-gitlab"],
|
|
53
|
+
"env": {
|
|
54
|
+
"GITLAB_USE_OAUTH": "true",
|
|
55
|
+
"GITLAB_OAUTH_CLIENT_ID": "your_oauth_client_id",
|
|
56
|
+
"GITLAB_OAUTH_REDIRECT_URI": "http://127.0.0.1:8888/callback",
|
|
57
|
+
"GITLAB_API_URL": "your_gitlab_api_url",
|
|
58
|
+
"GITLAB_PROJECT_ID": "your_project_id", // Optional: default project
|
|
59
|
+
"GITLAB_ALLOWED_PROJECT_IDS": "", // Optional: comma-separated list of allowed project IDs
|
|
60
|
+
"GITLAB_READ_ONLY_MODE": "false",
|
|
61
|
+
"USE_GITLAB_WIKI": "false", // use wiki api?
|
|
62
|
+
"USE_MILESTONE": "false", // use milestone api?
|
|
63
|
+
"USE_PIPELINE": "false" // use pipeline api?
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Using Personal Access Token (traditional)
|
|
20
71
|
|
|
21
72
|
```json
|
|
22
73
|
{
|
|
@@ -67,6 +118,28 @@ When using with the Claude App, you need to set up your API key and URLs directl
|
|
|
67
118
|
}
|
|
68
119
|
```
|
|
69
120
|
|
|
121
|
+
#### Strands Agents SDK (MCP Tools)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
env_vars = {
|
|
125
|
+
"GITLAB_PERSONAL_ACCESS_TOKEN": gitlab_access_token,
|
|
126
|
+
"GITLAB_API_URL": gitlab_api_url,
|
|
127
|
+
"USE_GITLAB_WIKI": use_gitlab_wiki
|
|
128
|
+
# ......the rest of the optional parameters
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stdio_gitlab_mcp_client = MCPClient(
|
|
132
|
+
lambda: stdio_client(
|
|
133
|
+
StdioServerParameters(
|
|
134
|
+
command="npx",
|
|
135
|
+
args=["-y", "@zereight/mcp-gitlab"],
|
|
136
|
+
env=env_vars,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
|
|
70
143
|
#### Docker
|
|
71
144
|
|
|
72
145
|
- stdio mcp.json
|
|
@@ -163,7 +236,11 @@ docker run -i --rm \
|
|
|
163
236
|
|
|
164
237
|
#### Authentication Configuration
|
|
165
238
|
|
|
166
|
-
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. **Required in standard mode**; not used when `REMOTE_AUTHORIZATION=true
|
|
239
|
+
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. **Required in standard mode**; not used when `REMOTE_AUTHORIZATION=true` or when using OAuth.
|
|
240
|
+
- `GITLAB_USE_OAUTH`: Set to `true` to enable OAuth2 authentication instead of personal access token.
|
|
241
|
+
- `GITLAB_OAUTH_CLIENT_ID`: The Client ID from your GitLab OAuth application. Required when using OAuth.
|
|
242
|
+
- `GITLAB_OAUTH_REDIRECT_URI`: The OAuth callback URL. Default: `http://127.0.0.1:8888/callback`
|
|
243
|
+
- `GITLAB_OAUTH_TOKEN_PATH`: Custom path to store the OAuth token. Default: `~/.gitlab-mcp-token.json`
|
|
167
244
|
- `REMOTE_AUTHORIZATION`: When set to 'true', enables remote per-session authorization via HTTP headers. In this mode:
|
|
168
245
|
- The server accepts GitLab PAT tokens from HTTP headers (`Authorization: Bearer <token>` or `Private-Token: <token>`) on a per-session basis
|
|
169
246
|
- `GITLAB_PERSONAL_ACCESS_TOKEN` environment variable is **not required** and ignored
|
|
@@ -173,7 +250,7 @@ docker run -i --rm \
|
|
|
173
250
|
- Tokens are stored per session and automatically cleaned up when sessions close or timeout
|
|
174
251
|
- `SESSION_TIMEOUT_SECONDS`: Session auth token timeout in seconds. Default: `3600` (1 hour). Valid range: 1-86400 seconds (recommended: 60+). After this period of inactivity, the auth token is removed but the transport session remains active. The client must provide auth headers again on the next request. Only applies when `REMOTE_AUTHORIZATION=true`.
|
|
175
252
|
|
|
176
|
-
####
|
|
253
|
+
#### General Configuration
|
|
177
254
|
|
|
178
255
|
- `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`)
|
|
179
256
|
- `GITLAB_PROJECT_ID`: Default project ID. If set, Overwrite this value when making an API request.
|
package/build/index.js
CHANGED
|
@@ -17,12 +17,13 @@ import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
20
|
+
import { initializeOAuth } from "./oauth.js";
|
|
20
21
|
// Add type imports for proxy agents
|
|
21
22
|
import { Agent } from "http";
|
|
22
23
|
import { Agent as HttpsAgent } from "https";
|
|
23
24
|
import { URL } from "url";
|
|
24
25
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
|
|
25
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema,
|
|
26
|
+
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema,
|
|
26
27
|
// pipeline job schemas
|
|
27
28
|
GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
28
29
|
// Discussion Schemas
|
|
@@ -30,7 +31,7 @@ GitLabDiscussionNoteSchema, // Added
|
|
|
30
31
|
GitLabDiscussionSchema,
|
|
31
32
|
// Draft Notes Schemas
|
|
32
33
|
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
33
|
-
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, } from "./schemas.js";
|
|
34
|
+
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, 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 } from "./schemas.js";
|
|
34
35
|
import { randomUUID } from "crypto";
|
|
35
36
|
import { pino } from "pino";
|
|
36
37
|
const logger = pino({
|
|
@@ -130,10 +131,11 @@ function validateConfiguration() {
|
|
|
130
131
|
}
|
|
131
132
|
// Validate auth configuration
|
|
132
133
|
const remoteAuth = process.env.REMOTE_AUTHORIZATION === "true";
|
|
134
|
+
const useOAuth = process.env.GITLAB_USE_OAUTH === "true";
|
|
133
135
|
const hasToken = !!process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
134
136
|
const hasCookie = !!process.env.GITLAB_AUTH_COOKIE_PATH;
|
|
135
|
-
if (!remoteAuth && !hasToken && !hasCookie) {
|
|
136
|
-
errors.push('Either GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_AUTH_COOKIE_PATH, or REMOTE_AUTHORIZATION=true must be set');
|
|
137
|
+
if (!remoteAuth && !useOAuth && !hasToken && !hasCookie) {
|
|
138
|
+
errors.push('Either GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_AUTH_COOKIE_PATH, GITLAB_USE_OAUTH=true, or REMOTE_AUTHORIZATION=true must be set');
|
|
137
139
|
}
|
|
138
140
|
if (errors.length > 0) {
|
|
139
141
|
logger.error('Configuration validation failed:');
|
|
@@ -143,7 +145,9 @@ function validateConfiguration() {
|
|
|
143
145
|
logger.info('Configuration validation passed');
|
|
144
146
|
}
|
|
145
147
|
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
148
|
+
let OAUTH_ACCESS_TOKEN = null;
|
|
146
149
|
const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH;
|
|
150
|
+
const USE_OAUTH = process.env.GITLAB_USE_OAUTH === "true";
|
|
147
151
|
const IS_OLD = process.env.GITLAB_IS_OLD === "true";
|
|
148
152
|
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
|
|
149
153
|
const GITLAB_DENIED_TOOLS_REGEX = process.env.GITLAB_DENIED_TOOLS_REGEX ? new RegExp(process.env.GITLAB_DENIED_TOOLS_REGEX) : undefined;
|
|
@@ -281,12 +285,13 @@ function buildAuthHeaders() {
|
|
|
281
285
|
}
|
|
282
286
|
return {}; // No auth headers if no session context
|
|
283
287
|
}
|
|
284
|
-
// Standard mode:
|
|
285
|
-
|
|
286
|
-
|
|
288
|
+
// Standard mode: prioritize OAuth token, then fall back to environment token
|
|
289
|
+
const token = OAUTH_ACCESS_TOKEN || GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
290
|
+
if (IS_OLD && token) {
|
|
291
|
+
return { 'Private-Token': String(token) };
|
|
287
292
|
}
|
|
288
|
-
if (
|
|
289
|
-
return { Authorization: `Bearer ${
|
|
293
|
+
if (token) {
|
|
294
|
+
return { Authorization: `Bearer ${token}` };
|
|
290
295
|
}
|
|
291
296
|
return {};
|
|
292
297
|
}
|
|
@@ -395,21 +400,56 @@ const allTools = [
|
|
|
395
400
|
description: "Create a new thread on a merge request",
|
|
396
401
|
inputSchema: toJSONSchema(CreateMergeRequestThreadSchema),
|
|
397
402
|
},
|
|
403
|
+
{
|
|
404
|
+
name: 'resolve_merge_request_thread',
|
|
405
|
+
description: "Resolve a thread on a merge request",
|
|
406
|
+
inputSchema: toJSONSchema(ResolveMergeRequestThreadSchema),
|
|
407
|
+
},
|
|
398
408
|
{
|
|
399
409
|
name: "mr_discussions",
|
|
400
410
|
description: "List discussion items for a merge request",
|
|
401
411
|
inputSchema: toJSONSchema(ListMergeRequestDiscussionsSchema),
|
|
402
412
|
},
|
|
403
413
|
{
|
|
404
|
-
name: "
|
|
405
|
-
description: "
|
|
406
|
-
inputSchema: toJSONSchema(
|
|
414
|
+
name: "delete_merge_request_discussion_note",
|
|
415
|
+
description: "Delete a discussion note on a merge request",
|
|
416
|
+
inputSchema: toJSONSchema(DeleteMergeRequestDiscussionNoteSchema),
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "update_merge_request_discussion_note",
|
|
420
|
+
description: "Update a discussion note on a merge request",
|
|
421
|
+
inputSchema: toJSONSchema(UpdateMergeRequestDiscussionNoteSchema),
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "create_merge_request_discussion_note",
|
|
425
|
+
description: "Add a new discussion note to an existing merge request thread",
|
|
426
|
+
inputSchema: toJSONSchema(CreateMergeRequestDiscussionNoteSchema),
|
|
407
427
|
},
|
|
408
428
|
{
|
|
409
429
|
name: "create_merge_request_note",
|
|
410
|
-
description: "Add a new note to
|
|
430
|
+
description: "Add a new note to a merge request",
|
|
411
431
|
inputSchema: toJSONSchema(CreateMergeRequestNoteSchema),
|
|
412
432
|
},
|
|
433
|
+
{
|
|
434
|
+
name: "delete_merge_request_note",
|
|
435
|
+
description: "Delete an existing merge request note",
|
|
436
|
+
inputSchema: toJSONSchema(DeleteMergeRequestNoteSchema),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "get_merge_request_note",
|
|
440
|
+
description: "Get a specific note for a merge request",
|
|
441
|
+
inputSchema: toJSONSchema(GetMergeRequestNoteSchema),
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'get_merge_request_notes',
|
|
445
|
+
description: "List notes for a merge request",
|
|
446
|
+
inputSchema: toJSONSchema(GetMergeRequestNotesSchema),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "update_merge_request_note",
|
|
450
|
+
description: "Modify an existing merge request note",
|
|
451
|
+
inputSchema: toJSONSchema(UpdateMergeRequestNoteSchema),
|
|
452
|
+
},
|
|
413
453
|
{
|
|
414
454
|
name: "get_draft_note",
|
|
415
455
|
description: "Get a single draft note from a merge request",
|
|
@@ -913,12 +953,11 @@ if (REMOTE_AUTHORIZATION) {
|
|
|
913
953
|
}
|
|
914
954
|
logger.info("Remote authorization enabled: tokens will be read from HTTP headers");
|
|
915
955
|
}
|
|
916
|
-
else {
|
|
917
|
-
// Standard mode: token must be in environment
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
956
|
+
else if (!USE_OAUTH && !GITLAB_PERSONAL_ACCESS_TOKEN && !GITLAB_AUTH_COOKIE_PATH) {
|
|
957
|
+
// Standard mode: token must be in environment (unless using OAuth)
|
|
958
|
+
logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
|
|
959
|
+
logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
|
|
960
|
+
process.exit(1);
|
|
922
961
|
}
|
|
923
962
|
/**
|
|
924
963
|
* Utility function for handling GitLab API errors
|
|
@@ -1429,6 +1468,18 @@ async function listMergeRequestDiscussions(projectId, mergeRequestIid, options =
|
|
|
1429
1468
|
async function listIssueDiscussions(projectId, issueIid, options = {}) {
|
|
1430
1469
|
return listDiscussions(projectId, "issues", issueIid, options);
|
|
1431
1470
|
}
|
|
1471
|
+
async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId) {
|
|
1472
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1473
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
|
|
1474
|
+
const response = await fetch(url.toString(), {
|
|
1475
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1476
|
+
method: "DELETE",
|
|
1477
|
+
});
|
|
1478
|
+
if (!response.ok) {
|
|
1479
|
+
const errorText = await response.text();
|
|
1480
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1432
1483
|
/**
|
|
1433
1484
|
* Modify an existing merge request thread note
|
|
1434
1485
|
* 병합 요청 토론 노트 수정
|
|
@@ -1441,7 +1492,7 @@ async function listIssueDiscussions(projectId, issueIid, options = {}) {
|
|
|
1441
1492
|
* @param {boolean} [resolved] - Resolve/unresolve state
|
|
1442
1493
|
* @returns {Promise<GitLabDiscussionNote>} The updated note
|
|
1443
1494
|
*/
|
|
1444
|
-
async function
|
|
1495
|
+
async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
|
|
1445
1496
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1446
1497
|
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
|
|
1447
1498
|
// Only one of body or resolved can be sent according to GitLab API
|
|
@@ -1509,7 +1560,7 @@ async function createIssueNote(projectId, issueIid, discussionId, body, createdA
|
|
|
1509
1560
|
return GitLabDiscussionNoteSchema.parse(data);
|
|
1510
1561
|
}
|
|
1511
1562
|
/**
|
|
1512
|
-
* Add a new note to an existing merge request thread
|
|
1563
|
+
* Add a new discussion note to an existing merge request thread
|
|
1513
1564
|
* 기존 병합 요청 스레드에 새 노트 추가
|
|
1514
1565
|
*
|
|
1515
1566
|
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
@@ -1519,7 +1570,7 @@ async function createIssueNote(projectId, issueIid, discussionId, body, createdA
|
|
|
1519
1570
|
* @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
|
|
1520
1571
|
* @returns {Promise<GitLabDiscussionNote>} The created note
|
|
1521
1572
|
*/
|
|
1522
|
-
async function
|
|
1573
|
+
async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
|
|
1523
1574
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1524
1575
|
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`);
|
|
1525
1576
|
const payload = { body };
|
|
@@ -1535,6 +1586,81 @@ async function createMergeRequestNote(projectId, mergeRequestIid, discussionId,
|
|
|
1535
1586
|
const data = await response.json();
|
|
1536
1587
|
return GitLabDiscussionNoteSchema.parse(data);
|
|
1537
1588
|
}
|
|
1589
|
+
async function createMergeRequestNote(projectId, mergeRequestIid, body) {
|
|
1590
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1591
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes`);
|
|
1592
|
+
const payload = {
|
|
1593
|
+
id: projectId,
|
|
1594
|
+
merge_request_iid: mergeRequestIid,
|
|
1595
|
+
body,
|
|
1596
|
+
};
|
|
1597
|
+
const response = await fetch(url.toString(), {
|
|
1598
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1599
|
+
method: "POST",
|
|
1600
|
+
body: JSON.stringify(payload),
|
|
1601
|
+
});
|
|
1602
|
+
await handleGitLabError(response);
|
|
1603
|
+
const data = await response.json();
|
|
1604
|
+
return GitLabDiscussionNoteSchema.parse(data);
|
|
1605
|
+
}
|
|
1606
|
+
async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
1607
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1608
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
|
|
1609
|
+
const response = await fetch(url.toString(), {
|
|
1610
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1611
|
+
method: "DELETE",
|
|
1612
|
+
});
|
|
1613
|
+
if (!response.ok) {
|
|
1614
|
+
const errorText = await response.text();
|
|
1615
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
1619
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1620
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
|
|
1621
|
+
const response = await fetch(url.toString(), {
|
|
1622
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1623
|
+
method: "GET",
|
|
1624
|
+
});
|
|
1625
|
+
await handleGitLabError(response);
|
|
1626
|
+
const data = await response.json();
|
|
1627
|
+
return GitLabDiscussionNoteSchema.parse(data);
|
|
1628
|
+
}
|
|
1629
|
+
async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by) {
|
|
1630
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1631
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes`);
|
|
1632
|
+
if (sort) {
|
|
1633
|
+
url.searchParams.append("sort", sort);
|
|
1634
|
+
}
|
|
1635
|
+
if (order_by) {
|
|
1636
|
+
url.searchParams.append("order_by", order_by);
|
|
1637
|
+
}
|
|
1638
|
+
const response = await fetch(url.toString(), {
|
|
1639
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1640
|
+
method: "GET",
|
|
1641
|
+
});
|
|
1642
|
+
await handleGitLabError(response);
|
|
1643
|
+
const data = await response.json();
|
|
1644
|
+
return z.array(GitLabDiscussionNoteSchema).parse(data);
|
|
1645
|
+
}
|
|
1646
|
+
async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body) {
|
|
1647
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1648
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
|
|
1649
|
+
const payload = {
|
|
1650
|
+
id: projectId,
|
|
1651
|
+
merge_request_iid: mergeRequestIid,
|
|
1652
|
+
note_id: noteId,
|
|
1653
|
+
body,
|
|
1654
|
+
};
|
|
1655
|
+
const response = await fetch(url.toString(), {
|
|
1656
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1657
|
+
method: "PUT",
|
|
1658
|
+
body: JSON.stringify(payload),
|
|
1659
|
+
});
|
|
1660
|
+
await handleGitLabError(response);
|
|
1661
|
+
const data = await response.json();
|
|
1662
|
+
return GitLabDiscussionNoteSchema.parse(data);
|
|
1663
|
+
}
|
|
1538
1664
|
/**
|
|
1539
1665
|
* Create or update a file in a GitLab project
|
|
1540
1666
|
* 파일 생성 또는 업데이트
|
|
@@ -2145,6 +2271,21 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
|
|
|
2145
2271
|
return [];
|
|
2146
2272
|
}
|
|
2147
2273
|
}
|
|
2274
|
+
async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionId, resolved) {
|
|
2275
|
+
projectId = decodeURIComponent(projectId);
|
|
2276
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}`);
|
|
2277
|
+
if (resolved !== undefined) {
|
|
2278
|
+
url.searchParams.append("resolved", resolved ? "true" : "false");
|
|
2279
|
+
}
|
|
2280
|
+
const response = await fetch(url.toString(), {
|
|
2281
|
+
...DEFAULT_FETCH_CONFIG,
|
|
2282
|
+
method: "PUT",
|
|
2283
|
+
});
|
|
2284
|
+
if (!response.ok) {
|
|
2285
|
+
const errorText = await response.text();
|
|
2286
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2148
2289
|
/**
|
|
2149
2290
|
* Create a new thread on a merge request
|
|
2150
2291
|
* 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
|
|
@@ -3762,18 +3903,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3762
3903
|
content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
|
|
3763
3904
|
};
|
|
3764
3905
|
}
|
|
3765
|
-
case "
|
|
3766
|
-
const args =
|
|
3767
|
-
const
|
|
3906
|
+
case "delete_merge_request_discussion_note": {
|
|
3907
|
+
const args = DeleteMergeRequestDiscussionNoteSchema.parse(request.params.arguments);
|
|
3908
|
+
const { project_id, merge_request_iid, discussion_id, note_id } = args;
|
|
3909
|
+
await deleteMergeRequestDiscussionNote(project_id, merge_request_iid, discussion_id, note_id);
|
|
3910
|
+
return {
|
|
3911
|
+
content: [{ type: "text", text: "Merge request discussion note deleted successfully" }],
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
case "update_merge_request_discussion_note": {
|
|
3915
|
+
const args = UpdateMergeRequestDiscussionNoteSchema.parse(request.params.arguments);
|
|
3916
|
+
const note = await updateMergeRequestDiscussionNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, // Now optional
|
|
3768
3917
|
args.resolved // Now one of body or resolved must be provided, not both
|
|
3769
3918
|
);
|
|
3770
3919
|
return {
|
|
3771
3920
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
3772
3921
|
};
|
|
3773
3922
|
}
|
|
3923
|
+
case "create_merge_request_discussion_note": {
|
|
3924
|
+
const args = CreateMergeRequestDiscussionNoteSchema.parse(request.params.arguments);
|
|
3925
|
+
const note = await createMergeRequestDiscussionNote(args.project_id, args.merge_request_iid, args.discussion_id, args.body, args.created_at);
|
|
3926
|
+
return {
|
|
3927
|
+
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
3774
3930
|
case "create_merge_request_note": {
|
|
3775
3931
|
const args = CreateMergeRequestNoteSchema.parse(request.params.arguments);
|
|
3776
|
-
const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.
|
|
3932
|
+
const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.body);
|
|
3933
|
+
return {
|
|
3934
|
+
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
case "delete_merge_request_note": {
|
|
3938
|
+
const args = DeleteMergeRequestNoteSchema.parse(request.params.arguments);
|
|
3939
|
+
await deleteMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id);
|
|
3940
|
+
return {
|
|
3941
|
+
content: [{ type: "text", text: "Merge request note deleted successfully" }],
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
case 'get_merge_request_note': {
|
|
3945
|
+
const args = GetMergeRequestNoteSchema.parse(request.params.arguments);
|
|
3946
|
+
const note = await getMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id);
|
|
3947
|
+
return {
|
|
3948
|
+
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
case 'get_merge_request_notes': {
|
|
3952
|
+
const args = GetMergeRequestNotesSchema.parse(request.params.arguments);
|
|
3953
|
+
const notes = await getMergeRequestNotes(args.project_id, args.merge_request_iid, args.sort, args.order_by);
|
|
3954
|
+
return {
|
|
3955
|
+
content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
case 'update_merge_request_note': {
|
|
3959
|
+
const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
|
|
3960
|
+
const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id, args.body);
|
|
3777
3961
|
return {
|
|
3778
3962
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
3779
3963
|
};
|
|
@@ -3995,6 +4179,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3995
4179
|
content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
|
|
3996
4180
|
};
|
|
3997
4181
|
}
|
|
4182
|
+
case "resolve_merge_request_thread": {
|
|
4183
|
+
const args = ResolveMergeRequestThreadSchema.parse(request.params.arguments);
|
|
4184
|
+
const { project_id, merge_request_iid, discussion_id, resolved } = args;
|
|
4185
|
+
await resolveMergeRequestThread(project_id, merge_request_iid, discussion_id, resolved);
|
|
4186
|
+
return {
|
|
4187
|
+
content: [{ type: "text", text: "Thread resolved successfully" }],
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
3998
4190
|
case "list_issues": {
|
|
3999
4191
|
const args = ListIssuesSchema.parse(request.params.arguments);
|
|
4000
4192
|
const { project_id, ...options } = args;
|
|
@@ -4675,7 +4867,7 @@ async function startStreamableHTTPServer() {
|
|
|
4675
4867
|
// GitLab PAT format: glpat-xxxxx (min 20 chars)
|
|
4676
4868
|
if (token.length < 20)
|
|
4677
4869
|
return false;
|
|
4678
|
-
if (!/^[a-zA-Z0-9_
|
|
4870
|
+
if (!/^[a-zA-Z0-9_\.-]+$/.test(token))
|
|
4679
4871
|
return false;
|
|
4680
4872
|
return true;
|
|
4681
4873
|
};
|
|
@@ -5011,6 +5203,20 @@ async function runServer() {
|
|
|
5011
5203
|
try {
|
|
5012
5204
|
// Validate configuration before starting server
|
|
5013
5205
|
validateConfiguration();
|
|
5206
|
+
// Initialize OAuth token if OAuth is enabled
|
|
5207
|
+
if (USE_OAUTH) {
|
|
5208
|
+
logger.info("Using OAuth authentication...");
|
|
5209
|
+
try {
|
|
5210
|
+
const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4$/, "");
|
|
5211
|
+
OAUTH_ACCESS_TOKEN = await initializeOAuth(gitlabBaseUrl);
|
|
5212
|
+
logger.info("OAuth authentication successful");
|
|
5213
|
+
// Note: Headers are automatically generated by buildAuthHeaders() using OAUTH_ACCESS_TOKEN
|
|
5214
|
+
}
|
|
5215
|
+
catch (error) {
|
|
5216
|
+
logger.error("OAuth authentication failed:", error);
|
|
5217
|
+
process.exit(1);
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5014
5220
|
const transportMode = determineTransportMode();
|
|
5015
5221
|
await initializeServerByTransportMode(transportMode);
|
|
5016
5222
|
}
|