@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 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
- #### npx
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
- #### Server Configuration
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: use environment token
285
- if (IS_OLD && GITLAB_PERSONAL_ACCESS_TOKEN) {
286
- return { 'Private-Token': String(GITLAB_PERSONAL_ACCESS_TOKEN) };
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 (GITLAB_PERSONAL_ACCESS_TOKEN) {
289
- return { Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` };
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: "update_merge_request_note",
405
- description: "Modify an existing merge request thread note",
406
- inputSchema: toJSONSchema(UpdateMergeRequestNoteSchema),
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 an existing merge request thread",
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
- if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
919
- logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
920
- process.exit(1);
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 updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
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 createMergeRequestNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
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 "update_merge_request_note": {
3766
- const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
3767
- const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, // Now optional
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.discussion_id, args.body, args.created_at);
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_-]+$/.test(token))
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
  }