@zereight/mcp-gitlab 2.1.7 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -155,6 +155,21 @@ docker run -i --rm \
155
155
 
156
156
  MCP OAuth 사양을 지원하는 원격 MCP 클라이언트(예: Claude.ai)용입니다. 서버는 완전한 OAuth 2.0 인증 서버로 동작합니다. 인증되지 않은 요청은 `401 + WWW-Authenticate` 응답을 받고, 클라이언트 측 OAuth 브라우저 플로우가 자동으로 시작됩니다.
157
157
 
158
+ OpenCode, MCPJam, Claude.ai 같은 원격 MCP 클라이언트는 인증 중에 자체 callback URL을 보낼 수 있습니다. 모든 클라이언트 callback URL을 GitLab에 등록할 수 없다면 `GITLAB_OAUTH_CALLBACK_PROXY=true`를 켜세요. 콜백 프록시 모드에서는 GitLab에 `{MCP_SERVER_URL}/callback` 하나만 Redirect URI로 등록하면 됩니다.
159
+
160
+ `GITLAB_OAUTH_REDIRECT_URI`는 로컬 OAuth(`GITLAB_USE_OAUTH`) 전용입니다. 원격 MCP OAuth 클라이언트 callback URL을 덮어쓰지 않으며, 원격 `Unregistered redirect_uri` 오류 해결용으로 사용하면 안 됩니다.
161
+
162
+ 이 변수가 존재하는 이유는 로컬 OAuth 플로우가 MCP 서버와 같은 머신에서 브라우저를 열고, `http://127.0.0.1:8888/callback` 같은 로컬 HTTP 서버로 callback을 받기 때문입니다.
163
+
164
+ 원격 MCP OAuth는 다릅니다. `GITLAB_MCP_OAUTH=true` 모드에서는 MCP 클라이언트가 `/authorize` 요청 중에 자체 callback URL을 제공합니다. `GITLAB_OAUTH_REDIRECT_URI`는 그 클라이언트 제공 URL을 대체하지 않습니다.
165
+
166
+ | 모드 | 활성화 변수 | Callback 변수 | GitLab Redirect URI |
167
+ | --- | --- | --- | --- |
168
+ | 로컬 OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` 또는 로컬 callback |
169
+ | 원격 MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
170
+
171
+ MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH_REDIRECT_URI`를 사용하세요. 원격 MCP 클라이언트가 callback URL을 소유하는 경우에는 `GITLAB_OAUTH_CALLBACK_PROXY=true`를 사용하세요.
172
+
158
173
  **동작 방식**: 공개 HTTPS URL을 가진 위치에 이 MCP 서버를 배포합니다. MCP 클라이언트는 `{MCP_SERVER_URL}/mcp`로 연결합니다. 서버는 OAuth 2.0 플로우를 처리하고 GitLab과 자격 증명을 교환합니다.
159
174
 
160
175
  **사전 준비:**
@@ -173,6 +188,16 @@ MCP OAuth 사양을 지원하는 원격 MCP 클라이언트(예: Claude.ai)용
173
188
  | `GITLAB_OAUTH_CALLBACK_PROXY` | 선택 | MCP 서버의 고정 `/callback` URL을 사용하려면 `true` |
174
189
  | `GITLAB_OAUTH_SCOPES` | 선택 | 쉼표로 구분된 scope 목록(기본값: `api,read_api,read_user`) |
175
190
 
191
+ > **`Unregistered redirect_uri` 문제 해결**
192
+ >
193
+ > 브라우저 URL의 `redirect_uri`를 확인하세요. 값이 `http://127.0.0.1:xxxxx/.../callback` 같은 클라이언트 callback을 가리키면 다음 설정을 켜세요.
194
+ >
195
+ > ```env
196
+ > GITLAB_OAUTH_CALLBACK_PROXY=true
197
+ > ```
198
+ >
199
+ > 원격 MCP OAuth 문제를 `GITLAB_OAUTH_REDIRECT_URI` 변경으로 해결하려고 하지 마세요. 이 변수는 로컬 OAuth(`GITLAB_USE_OAUTH`) 전용입니다.
200
+
176
201
  ```shell
177
202
  docker run -i --rm \
178
203
  -e HOST=0.0.0.0 \
package/README.md CHANGED
@@ -33,6 +33,7 @@ Quick start: choose either Personal Access Token or OAuth2 setup below and use `
33
33
  - [OAuth2 Authentication Setup Guide](./docs/oauth-setup.md)
34
34
  - [Environment Variables Reference](./docs/environment-variables.md)
35
35
  - [Stateless Mode — Multi-Pod HPA](./docs/stateless-mode.md)
36
+ - [Custom Agents and Multiple PAT Setup](./docs/custom-agent-multiple-pat.md)
36
37
 
37
38
  ## Usage
38
39
 
@@ -159,6 +160,32 @@ The server acts as a full OAuth 2.0 authorization server — unauthenticated req
159
160
  receive a `401 + WWW-Authenticate` response, which triggers the OAuth browser flow
160
161
  automatically on the client side.
161
162
 
163
+ Remote MCP clients such as OpenCode, MCPJam, and Claude.ai can send their own
164
+ callback URL during authorization. If you cannot register every client callback
165
+ URL in GitLab, enable `GITLAB_OAUTH_CALLBACK_PROXY=true`. With callback proxy
166
+ mode, GitLab only needs one registered redirect URI: `{MCP_SERVER_URL}/callback`.
167
+
168
+ `GITLAB_OAUTH_REDIRECT_URI` is for local OAuth (`GITLAB_USE_OAUTH`) only. It does
169
+ not override remote MCP OAuth client callback URLs and should not be used to fix
170
+ remote `Unregistered redirect_uri` errors.
171
+
172
+ This variable exists because the local OAuth flow starts a browser on the same
173
+ machine as the MCP server and listens for the callback on a local HTTP server,
174
+ for example `http://127.0.0.1:8888/callback`.
175
+
176
+ Remote MCP OAuth is different. In `GITLAB_MCP_OAUTH=true` mode, the MCP client
177
+ provides its own callback URL during `/authorize`. `GITLAB_OAUTH_REDIRECT_URI`
178
+ does not replace that client-provided URL.
179
+
180
+ | Mode | Enable with | Callback variable | GitLab redirect URI |
181
+ | --- | --- | --- | --- |
182
+ | Local OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` or your local callback |
183
+ | Remote MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
184
+
185
+ Use `GITLAB_OAUTH_REDIRECT_URI` only when the MCP server itself owns the local
186
+ browser callback. Use `GITLAB_OAUTH_CALLBACK_PROXY=true` when a remote MCP client
187
+ owns the callback URL.
188
+
162
189
  **How it works**: You deploy this MCP server somewhere with a public HTTPS URL. MCP
163
190
  clients connect to `{MCP_SERVER_URL}/mcp`. The server handles the OAuth 2.0 flow,
164
191
  exchanging credentials with GitLab on behalf of the client.
@@ -179,6 +206,18 @@ exchanging credentials with GitLab on behalf of the client.
179
206
  | `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL |
180
207
  | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
181
208
 
209
+ > **Troubleshooting `Unregistered redirect_uri`**
210
+ >
211
+ > Check the `redirect_uri` in the browser URL. If it points to a client callback
212
+ > such as `http://127.0.0.1:xxxxx/.../callback`, enable:
213
+ >
214
+ > ```env
215
+ > GITLAB_OAUTH_CALLBACK_PROXY=true
216
+ > ```
217
+ >
218
+ > Do not fix remote MCP OAuth by changing `GITLAB_OAUTH_REDIRECT_URI`. That
219
+ > variable is for local OAuth (`GITLAB_USE_OAUTH`) only.
220
+
182
221
  ```shell
183
222
  docker run -i --rm \
184
223
  -e HOST=0.0.0.0 \
@@ -497,102 +536,108 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
497
536
  48. `get_issue` - Get details of a specific issue in a GitLab project
498
537
  49. `update_issue` - Update an issue in a GitLab project
499
538
  50. `delete_issue` - Delete an issue from a GitLab project
500
- 51. `list_issue_links` - List all issue links for a specific issue
501
- 52. `list_issue_discussions` - List discussions for an issue in a GitLab project
502
- 53. `get_issue_link` - Get a specific issue link
503
- 54. `create_issue_link` - Create an issue link between two issues
504
- 55. `delete_issue_link` - Delete an issue link
505
- 56. `list_namespaces` - List all namespaces available to the current user
506
- 57. `get_namespace` - Get details of a namespace by ID or path
507
- 58. `verify_namespace` - Verify if a namespace path exists
508
- 59. `get_project` - Get details of a specific project
509
- 60. `list_projects` - List projects accessible by the current user
510
- 61. `list_project_members` - List members of a GitLab project
511
- 62. `list_group_projects` - List projects in a GitLab group with filtering options
512
- 63. `list_group_iterations` - List group iterations with filtering options
513
- 64. `list_labels` - List labels for a project
514
- 65. `get_label` - Get a single label from a project
515
- 66. `create_label` - Create a new label in a project
516
- 67. `update_label` - Update an existing label in a project
517
- 68. `delete_label` - Delete a label from a project
518
- 69. `list_pipelines` - List pipelines in a GitLab project with filtering options
519
- 70. `get_pipeline` - Get details of a specific pipeline in a GitLab project
520
- 71. `list_pipeline_jobs` - List all jobs in a specific pipeline
521
- 72. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
522
- 73. `get_pipeline_job` - Get details of a GitLab pipeline job number
523
- 74. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
524
- 75. `create_pipeline` - Create a new pipeline for a branch or tag
525
- 76. `retry_pipeline` - Retry a failed or canceled pipeline
526
- 77. `cancel_pipeline` - Cancel a running pipeline
527
- 78. `play_pipeline_job` - Run a manual pipeline job
528
- 79. `retry_pipeline_job` - Retry a failed or canceled pipeline job
529
- 80. `cancel_pipeline_job` - Cancel a running pipeline job
530
- 81. `list_deployments` - List deployments in a GitLab project with filtering options
531
- 82. `get_deployment` - Get details of a specific deployment in a GitLab project
532
- 83. `list_environments` - List environments in a GitLab project
533
- 84. `get_environment` - Get details of a specific environment in a GitLab project
534
- 85. `list_job_artifacts` - List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes
535
- 86. `download_job_artifacts` - Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path
536
- 87. `get_job_artifact_file` - Get the content of a single file from a job's artifacts by its path within the archive
537
- 88. `list_milestones` - List milestones in a GitLab project with filtering options
538
- 89. `get_milestone` - Get details of a specific milestone
539
- 90. `create_milestone` - Create a new milestone in a GitLab project
540
- 91. `edit_milestone` - Edit an existing milestone in a GitLab project
541
- 92. `delete_milestone` - Delete a milestone from a GitLab project
542
- 93. `get_milestone_issue` - Get issues associated with a specific milestone
543
- 94. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
544
- 95. `promote_milestone` - Promote a milestone to the next stage
545
- 96. `get_milestone_burndown_events` - Get burndown events for a specific milestone
546
- 97. `list_wiki_pages` - List wiki pages in a GitLab project
547
- 98. `get_wiki_page` - Get details of a specific wiki page
548
- 99. `create_wiki_page` - Create a new wiki page in a GitLab project
549
- 100. `update_wiki_page` - Update an existing wiki page in a GitLab project
550
- 101. `delete_wiki_page` - Delete a wiki page from a GitLab project
551
- 102. `list_group_wiki_pages` - List wiki pages in a GitLab group
552
- 103. `get_group_wiki_page` - Get details of a specific group wiki page
553
- 104. `create_group_wiki_page` - Create a new wiki page in a GitLab group
554
- 105. `update_group_wiki_page` - Update an existing wiki page in a GitLab group
555
- 106. `delete_group_wiki_page` - Delete a wiki page from a GitLab group
556
- 107. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
557
- 108. `list_commits` - List repository commits with filtering options
558
- 109. `get_commit` - Get details of a specific commit
559
- 110. `get_commit_diff` - Get changes/diffs of a specific commit
560
- 111. `list_releases` - List all releases for a project
561
- 112. `get_release` - Get a release by tag name
562
- 113. `create_release` - Create a new release in a GitLab project
563
- 114. `update_release` - Update an existing release in a GitLab project
564
- 115. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
565
- 116. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
566
- 117. `download_release_asset` - Download a release asset file by direct asset path
567
- 118. `list_tags` - List repository tags with filtering and pagination support
568
- 119. `get_tag` - Get details of a specific repository tag
569
- 120. `create_tag` - Create a new tag in the repository
570
- 121. `delete_tag` - Delete a tag from the repository
571
- 122. `get_tag_signature` - Get the signature of a signed tag
572
- 123. `get_users` - Get GitLab user details by usernames
573
- 124. `list_events` - List all events for the currently authenticated user
574
- 125. `get_project_events` - List all visible events for a specified project
575
- 126. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
576
- 127. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
577
- 128. `get_work_item` - Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets
578
- 129. `list_work_items` - List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info
579
- 130. `create_work_item` - Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality
580
- 131. `update_work_item` - Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields
581
- 132. `convert_work_item_type` - Convert a work item to a different type (e.g. issue to task, task to incident)
582
- 133. `list_work_item_statuses` - List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses
583
- 134. `list_custom_field_definitions` - List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item
584
- 135. `move_work_item` - Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation
585
- 136. `list_work_item_notes` - List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags
586
- 137. `create_work_item_note` - Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies
587
- 138. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
588
- 139. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
589
- 140. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
590
- 141. `list_webhook_events` - List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details
591
- 142. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
592
- 143. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
593
- 144. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
594
- 145. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
595
- 146. `execute_graphql` - Execute a GitLab GraphQL query
539
+ 51. `list_todos` - List GitLab to-do items for the current user
540
+ 52. `mark_todo_done` - Mark a GitLab to-do item as done
541
+ 53. `mark_all_todos_done` - Mark all pending GitLab to-do items as done for the current user
542
+ 54. `list_issue_links` - List all issue links for a specific issue
543
+ 55. `list_issue_discussions` - List discussions for an issue in a GitLab project
544
+ 56. `get_issue_link` - Get a specific issue link
545
+ 57. `create_issue_link` - Create an issue link between two issues
546
+ 58. `delete_issue_link` - Delete an issue link
547
+ 59. `list_namespaces` - List all namespaces available to the current user
548
+ 60. `get_namespace` - Get details of a namespace by ID or path
549
+ 61. `verify_namespace` - Verify if a namespace path exists
550
+ 62. `get_project` - Get details of a specific project
551
+ 63. `list_projects` - List projects accessible by the current user
552
+ 64. `list_project_members` - List members of a GitLab project
553
+ 65. `list_group_projects` - List projects in a GitLab group with filtering options
554
+ 66. `list_group_iterations` - List group iterations with filtering options
555
+ 67. `list_labels` - List labels for a project
556
+ 68. `get_label` - Get a single label from a project
557
+ 69. `create_label` - Create a new label in a project
558
+ 70. `update_label` - Update an existing label in a project
559
+ 71. `delete_label` - Delete a label from a project
560
+ 72. `list_pipelines` - List pipelines in a GitLab project with filtering options
561
+ 73. `get_pipeline` - Get details of a specific pipeline in a GitLab project
562
+ 74. `list_pipeline_jobs` - List all jobs in a specific pipeline
563
+ 75. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
564
+ 76. `get_pipeline_job` - Get details of a GitLab pipeline job number
565
+ 77. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
566
+ 78. `validate_ci_lint` - Validate provided GitLab CI/CD YAML content for a project
567
+ 79. `validate_project_ci_lint` - Validate an existing `.gitlab-ci.yml` configuration for a project
568
+ 80. `create_pipeline` - Create a new pipeline for a branch or tag
569
+ 81. `retry_pipeline` - Retry a failed or canceled pipeline
570
+ 82. `cancel_pipeline` - Cancel a running pipeline
571
+ 83. `play_pipeline_job` - Run a manual pipeline job
572
+ 84. `retry_pipeline_job` - Retry a failed or canceled pipeline job
573
+ 85. `cancel_pipeline_job` - Cancel a running pipeline job
574
+ 86. `list_deployments` - List deployments in a GitLab project with filtering options
575
+ 87. `get_deployment` - Get details of a specific deployment in a GitLab project
576
+ 88. `list_environments` - List environments in a GitLab project
577
+ 89. `get_environment` - Get details of a specific environment in a GitLab project
578
+ 90. `list_job_artifacts` - List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes
579
+ 91. `download_job_artifacts` - Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path
580
+ 92. `get_job_artifact_file` - Get the content of a single file from a job's artifacts by its path within the archive
581
+ 93. `list_milestones` - List milestones in a GitLab project with filtering options
582
+ 94. `get_milestone` - Get details of a specific milestone
583
+ 95. `create_milestone` - Create a new milestone in a GitLab project
584
+ 96. `edit_milestone` - Edit an existing milestone in a GitLab project
585
+ 97. `delete_milestone` - Delete a milestone from a GitLab project
586
+ 98. `get_milestone_issue` - Get issues associated with a specific milestone
587
+ 99. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
588
+ 100. `promote_milestone` - Promote a milestone to the next stage
589
+ 101. `get_milestone_burndown_events` - Get burndown events for a specific milestone
590
+ 102. `list_wiki_pages` - List wiki pages in a GitLab project
591
+ 103. `get_wiki_page` - Get details of a specific wiki page
592
+ 104. `create_wiki_page` - Create a new wiki page in a GitLab project
593
+ 105. `update_wiki_page` - Update an existing wiki page in a GitLab project
594
+ 106. `delete_wiki_page` - Delete a wiki page from a GitLab project
595
+ 107. `list_group_wiki_pages` - List wiki pages in a GitLab group
596
+ 108. `get_group_wiki_page` - Get details of a specific group wiki page
597
+ 109. `create_group_wiki_page` - Create a new wiki page in a GitLab group
598
+ 110. `update_group_wiki_page` - Update an existing wiki page in a GitLab group
599
+ 111. `delete_group_wiki_page` - Delete a wiki page from a GitLab group
600
+ 112. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
601
+ 113. `list_commits` - List repository commits with filtering options
602
+ 114. `get_commit` - Get details of a specific commit
603
+ 115. `get_commit_diff` - Get changes/diffs of a specific commit
604
+ 116. `list_releases` - List all releases for a project
605
+ 117. `get_release` - Get a release by tag name
606
+ 118. `create_release` - Create a new release in a GitLab project
607
+ 119. `update_release` - Update an existing release in a GitLab project
608
+ 120. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
609
+ 121. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
610
+ 122. `download_release_asset` - Download a release asset file by direct asset path
611
+ 123. `list_tags` - List repository tags with filtering and pagination support
612
+ 124. `get_tag` - Get details of a specific repository tag
613
+ 125. `create_tag` - Create a new tag in the repository
614
+ 126. `delete_tag` - Delete a tag from the repository
615
+ 127. `get_tag_signature` - Get the signature of a signed tag
616
+ 128. `get_users` - Get GitLab user details by usernames
617
+ 129. `list_events` - List all events for the currently authenticated user
618
+ 130. `get_project_events` - List all visible events for a specified project
619
+ 131. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
620
+ 132. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
621
+ 133. `get_work_item` - Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets
622
+ 134. `list_work_items` - List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info
623
+ 135. `create_work_item` - Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality
624
+ 136. `update_work_item` - Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields
625
+ 137. `convert_work_item_type` - Convert a work item to a different type (e.g. issue to task, task to incident)
626
+ 138. `list_work_item_statuses` - List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses
627
+ 139. `list_custom_field_definitions` - List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item
628
+ 140. `move_work_item` - Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation
629
+ 141. `list_work_item_notes` - List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags
630
+ 142. `create_work_item_note` - Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies
631
+ 143. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
632
+ 144. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
633
+ 145. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
634
+ 146. `list_webhook_events` - List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details
635
+ 147. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
636
+ 148. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
637
+ 149. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
638
+ 150. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
639
+ 151. `execute_graphql` - Execute a GitLab GraphQL query
640
+
596
641
  <!-- TOOLS-END -->
597
642
 
598
643
  </details>
package/README.zh-CN.md CHANGED
@@ -155,6 +155,21 @@ docker run -i --rm \
155
155
 
156
156
  适用于支持 MCP OAuth 规范的远程 MCP 客户端(例如 Claude.ai)。服务器会作为完整 OAuth 2.0 授权服务器运行。未认证请求会收到 `401 + WWW-Authenticate` 响应,从而触发客户端侧 OAuth 浏览器流程。
157
157
 
158
+ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送自己的 callback URL。如果你无法在 GitLab 中注册每个客户端 callback URL,请启用 `GITLAB_OAUTH_CALLBACK_PROXY=true`。启用回调代理模式后,GitLab 只需要注册一个 Redirect URI:`{MCP_SERVER_URL}/callback`。
159
+
160
+ `GITLAB_OAUTH_REDIRECT_URI` 仅用于本地 OAuth(`GITLAB_USE_OAUTH`)。它不会覆盖远程 MCP OAuth 客户端 callback URL,也不应用来修复远程 `Unregistered redirect_uri` 错误。
161
+
162
+ 这个变量存在是因为本地 OAuth 流程会在与 MCP 服务器相同的机器上打开浏览器,并通过本地 HTTP 服务器接收 callback,例如 `http://127.0.0.1:8888/callback`。
163
+
164
+ 远程 MCP OAuth 不同。在 `GITLAB_MCP_OAUTH=true` 模式下,MCP 客户端会在 `/authorize` 请求中提供自己的 callback URL。`GITLAB_OAUTH_REDIRECT_URI` 不会替换这个客户端提供的 URL。
165
+
166
+ | 模式 | 启用方式 | Callback 变量 | GitLab Redirect URI |
167
+ | --- | --- | --- | --- |
168
+ | 本地 OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` 或你的本地 callback |
169
+ | 远程 MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
170
+
171
+ 只有当 MCP 服务器自己接收本地浏览器 callback 时,才使用 `GITLAB_OAUTH_REDIRECT_URI`。当远程 MCP 客户端拥有 callback URL 时,请使用 `GITLAB_OAUTH_CALLBACK_PROXY=true`。
172
+
158
173
  **工作方式**:将此 MCP 服务器部署到拥有公开 HTTPS URL 的位置。MCP 客户端连接到 `{MCP_SERVER_URL}/mcp`。服务器处理 OAuth 2.0 流程,并代表客户端与 GitLab 交换凭据。
159
174
 
160
175
  **前置条件:**
@@ -173,6 +188,16 @@ docker run -i --rm \
173
188
  | `GITLAB_OAUTH_CALLBACK_PROXY` | 可选 | 设置为 `true` 时使用 MCP 服务器固定的 `/callback` URL |
174
189
  | `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
175
190
 
191
+ > **排查 `Unregistered redirect_uri`**
192
+ >
193
+ > 检查浏览器 URL 中的 `redirect_uri`。如果它指向客户端 callback,例如 `http://127.0.0.1:xxxxx/.../callback`,请启用:
194
+ >
195
+ > ```env
196
+ > GITLAB_OAUTH_CALLBACK_PROXY=true
197
+ > ```
198
+ >
199
+ > 不要通过修改 `GITLAB_OAUTH_REDIRECT_URI` 来修复远程 MCP OAuth。该变量仅用于本地 OAuth(`GITLAB_USE_OAUTH`)。
200
+
176
201
  ```shell
177
202
  docker run -i --rm \
178
203
  -e HOST=0.0.0.0 \
package/build/index.js CHANGED
@@ -25,15 +25,13 @@ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middlew
25
25
  import { GitLabClientPool } from "./gitlab-client-pool.js";
26
26
  import { allTools, readOnlyTools, destructiveTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
27
27
  import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
28
- CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema,
29
- // pipeline job schemas
30
- GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
28
+ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
31
29
  // Discussion Schemas
32
30
  GitLabDiscussionNoteSchema, // Added
33
31
  GitLabDiscussionSchema,
34
32
  // Draft Notes Schemas
35
- GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
36
- GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
33
+ GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
34
+ GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
37
35
  import { randomUUID } from "node:crypto";
38
36
  import { pino } from "pino";
39
37
  const logger = pino({
@@ -1097,6 +1095,36 @@ async function listIssues(projectId, options = {}) {
1097
1095
  const data = await response.json();
1098
1096
  return z.array(GitLabIssueSchema).parse(data);
1099
1097
  }
1098
+ async function listTodos(options = {}) {
1099
+ const url = new URL(`${getEffectiveApiUrl()}/todos`);
1100
+ Object.entries(options).forEach(([key, value]) => {
1101
+ if (value !== undefined) {
1102
+ url.searchParams.append(key, String(value));
1103
+ }
1104
+ });
1105
+ const response = await fetch(url.toString(), {
1106
+ ...getFetchConfig(),
1107
+ });
1108
+ await handleGitLabError(response);
1109
+ const data = await response.json();
1110
+ return z.array(GitLabTodoSchema).parse(data);
1111
+ }
1112
+ async function markTodoDone(id) {
1113
+ const response = await fetch(`${getEffectiveApiUrl()}/todos/${id}/mark_as_done`, {
1114
+ ...getFetchConfig(),
1115
+ method: "POST",
1116
+ });
1117
+ await handleGitLabError(response);
1118
+ const data = await response.json();
1119
+ return GitLabTodoSchema.parse(data);
1120
+ }
1121
+ async function markAllTodosDone() {
1122
+ const response = await fetch(`${getEffectiveApiUrl()}/todos/mark_as_done`, {
1123
+ ...getFetchConfig(),
1124
+ method: "POST",
1125
+ });
1126
+ await handleGitLabError(response);
1127
+ }
1100
1128
  /**
1101
1129
  * List merge requests globally or for a specific GitLab project
1102
1130
  *
@@ -4769,6 +4797,38 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
4769
4797
  }
4770
4798
  return fullTrace;
4771
4799
  }
4800
+ async function validateCiLint(projectId, options) {
4801
+ projectId = decodeURIComponent(projectId);
4802
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
4803
+ const response = await fetch(url.toString(), {
4804
+ ...getFetchConfig(),
4805
+ method: "POST",
4806
+ body: JSON.stringify(options),
4807
+ });
4808
+ await handleGitLabError(response);
4809
+ const data = await response.json();
4810
+ return GitLabCiLintResultSchema.parse(data);
4811
+ }
4812
+ async function validateProjectCiLint(projectId, options) {
4813
+ projectId = decodeURIComponent(projectId);
4814
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
4815
+ Object.entries(options).forEach(([key, value]) => {
4816
+ if (value !== undefined) {
4817
+ if (typeof value === "boolean") {
4818
+ url.searchParams.append(key, value ? "true" : "false");
4819
+ }
4820
+ else {
4821
+ url.searchParams.append(key, value.toString());
4822
+ }
4823
+ }
4824
+ });
4825
+ const response = await fetch(url.toString(), {
4826
+ ...getFetchConfig(),
4827
+ });
4828
+ await handleGitLabError(response);
4829
+ const data = await response.json();
4830
+ return GitLabCiLintResultSchema.parse(data);
4831
+ }
4772
4832
  /**
4773
4833
  * List artifact files in a job's artifacts archive
4774
4834
  *
@@ -5791,7 +5851,7 @@ async function getTagSignature(projectId, tagName) {
5791
5851
  async function handleToolCall(params) {
5792
5852
  try {
5793
5853
  if (!params.arguments) {
5794
- throw new Error("Arguments are required");
5854
+ params.arguments = {};
5795
5855
  }
5796
5856
  // Ensure session is established for every request if cookie authentication is enabled
5797
5857
  if (GITLAB_AUTH_COOKIE_PATH) {
@@ -6150,6 +6210,35 @@ async function handleToolCall(params) {
6150
6210
  await deleteRestAwardEmoji(path);
6151
6211
  return { content: [{ type: "text", text: "Issue note emoji reaction deleted successfully" }] };
6152
6212
  }
6213
+ case "list_todos": {
6214
+ const args = ListTodosSchema.parse(params.arguments);
6215
+ const todos = await listTodos(args);
6216
+ return {
6217
+ content: [{ type: "text", text: JSON.stringify(todos, null, 2) }],
6218
+ };
6219
+ }
6220
+ case "mark_todo_done": {
6221
+ const args = MarkTodoDoneSchema.parse(params.arguments);
6222
+ const todo = await markTodoDone(args.id);
6223
+ return {
6224
+ content: [{ type: "text", text: JSON.stringify(todo, null, 2) }],
6225
+ };
6226
+ }
6227
+ case "mark_all_todos_done": {
6228
+ MarkAllTodosDoneSchema.parse(params.arguments);
6229
+ await markAllTodosDone();
6230
+ return {
6231
+ content: [
6232
+ {
6233
+ type: "text",
6234
+ text: JSON.stringify({
6235
+ status: "success",
6236
+ message: "All pending to-do items marked as done",
6237
+ }, null, 2),
6238
+ },
6239
+ ],
6240
+ };
6241
+ }
6153
6242
  case "get_merge_request": {
6154
6243
  const args = GetMergeRequestSchema.parse(params.arguments);
6155
6244
  const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
@@ -6902,6 +6991,22 @@ async function handleToolCall(params) {
6902
6991
  ],
6903
6992
  };
6904
6993
  }
6994
+ case "validate_ci_lint": {
6995
+ const args = ValidateCiLintSchema.parse(params.arguments);
6996
+ const { project_id, ...options } = args;
6997
+ const result = await validateCiLint(project_id, options);
6998
+ return {
6999
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7000
+ };
7001
+ }
7002
+ case "validate_project_ci_lint": {
7003
+ const args = ValidateProjectCiLintSchema.parse(params.arguments);
7004
+ const { project_id, ...options } = args;
7005
+ const result = await validateProjectCiLint(project_id, options);
7006
+ return {
7007
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7008
+ };
7009
+ }
6905
7010
  case "create_pipeline": {
6906
7011
  const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
6907
7012
  const pipeline = await createPipeline(project_id, ref, variables, inputs);
package/build/schemas.js CHANGED
@@ -275,6 +275,34 @@ export const ListPipelineTriggerJobsSchema = z
275
275
  .describe("The scope of trigger jobs to show"),
276
276
  })
277
277
  .merge(PaginationOptionsSchema);
278
+ export const GitLabCiLintResultSchema = z.object({
279
+ valid: z.coerce.boolean(),
280
+ errors: z.array(z.string()),
281
+ warnings: z.array(z.string()).optional(),
282
+ merged_yaml: z.string().optional(),
283
+ includes: z.array(z.unknown()).optional(),
284
+ jobs: z.array(z.unknown()).optional(),
285
+ });
286
+ export const ValidateCiLintSchema = z.object({
287
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
288
+ content: z.string().describe("GitLab CI/CD YAML content to validate"),
289
+ dry_run: z.coerce.boolean().optional().describe("Run pipeline creation simulation"),
290
+ include_jobs: z.coerce.boolean().optional().describe("Include jobs in the lint response"),
291
+ ref: z.string().optional().describe("Branch or tag context for dry_run validation"),
292
+ });
293
+ export const ValidateProjectCiLintSchema = z.object({
294
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
295
+ content_ref: z
296
+ .string()
297
+ .optional()
298
+ .describe("Commit SHA, branch, or tag to read the existing CI config from"),
299
+ dry_run: z.coerce.boolean().optional().describe("Run pipeline creation simulation"),
300
+ dry_run_ref: z
301
+ .string()
302
+ .optional()
303
+ .describe("Branch or tag context for dry_run validation"),
304
+ include_jobs: z.coerce.boolean().optional().describe("Include jobs in the lint response"),
305
+ });
278
306
  // Deployment related schemas
279
307
  export const GitLabDeploymentSchema = z.object({
280
308
  id: z.coerce.string(),
@@ -1234,6 +1262,60 @@ export const CreateIssueSchema = ProjectParamsSchema.extend({
1234
1262
  .describe("The type of issue. One of issue, incident, test_case or task."),
1235
1263
  weight: z.coerce.number().optional().describe("Weight of the issue (numeric, typically hours of work)"),
1236
1264
  });
1265
+ export const GitLabTodoSchema = z.object({
1266
+ id: z.coerce.number(),
1267
+ project: z.unknown().optional(),
1268
+ author: z.unknown().optional(),
1269
+ action_name: z.string().optional(),
1270
+ target_type: z.string().optional(),
1271
+ target: z.unknown().optional(),
1272
+ target_url: z.string().optional(),
1273
+ body: z.string().optional(),
1274
+ state: z.string(),
1275
+ created_at: z.string().optional(),
1276
+ updated_at: z.string().optional(),
1277
+ });
1278
+ export const ListTodosSchema = z
1279
+ .object({
1280
+ action: z
1281
+ .enum([
1282
+ "assigned",
1283
+ "mentioned",
1284
+ "build_failed",
1285
+ "marked",
1286
+ "approval_required",
1287
+ "unmergeable",
1288
+ "directly_addressed",
1289
+ "merge_train_removed",
1290
+ "member_access_requested",
1291
+ ])
1292
+ .optional()
1293
+ .describe("Filter by to-do action"),
1294
+ author_id: z.coerce.number().optional().describe("Filter by author ID"),
1295
+ project_id: z.coerce.number().optional().describe("Filter by project ID"),
1296
+ group_id: z.coerce.number().optional().describe("Filter by group ID"),
1297
+ state: z.enum(["pending", "done"]).optional().describe("Filter by to-do state"),
1298
+ type: z
1299
+ .enum([
1300
+ "Issue",
1301
+ "MergeRequest",
1302
+ "Commit",
1303
+ "Epic",
1304
+ "DesignManagement::Design",
1305
+ "AlertManagement::Alert",
1306
+ "Project",
1307
+ "Namespace",
1308
+ "Vulnerability",
1309
+ "WikiPage::Meta",
1310
+ ])
1311
+ .optional()
1312
+ .describe("Filter by to-do target type"),
1313
+ })
1314
+ .merge(PaginationOptionsSchema);
1315
+ export const MarkTodoDoneSchema = z.object({
1316
+ id: z.coerce.number().describe("The ID of the to-do item"),
1317
+ });
1318
+ export const MarkAllTodosDoneSchema = z.object({});
1237
1319
  const MergeRequestOptionsSchema = {
1238
1320
  title: z.string().describe("Merge request title"),
1239
1321
  description: z.string().optional().describe("Merge request description"),
@@ -0,0 +1,191 @@
1
+ import { describe, test, before, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { spawn } from "child_process";
4
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
+ const MOCK_TOKEN = "glpat-ci-lint-test-token";
6
+ const TEST_PROJECT_ID = "123";
7
+ async function callTool(toolName, args, env) {
8
+ return new Promise((resolve, reject) => {
9
+ const proc = spawn("node", ["build/index.js"], {
10
+ stdio: ["pipe", "pipe", "pipe"],
11
+ env: {
12
+ ...process.env,
13
+ ...env,
14
+ USE_PIPELINE: "true",
15
+ },
16
+ });
17
+ let output = "";
18
+ let errorOutput = "";
19
+ proc.stdout?.on("data", (d) => (output += d));
20
+ proc.stderr?.on("data", (d) => (errorOutput += d));
21
+ proc.on("close", code => {
22
+ if (code !== 0) {
23
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
24
+ }
25
+ const line = output.split("\n").find(l => l.startsWith("{"));
26
+ if (!line)
27
+ return reject(new Error("No JSON output found"));
28
+ try {
29
+ const response = JSON.parse(line);
30
+ if (response.error) {
31
+ reject(response.error);
32
+ }
33
+ else {
34
+ const content = response.result?.content?.[0]?.text;
35
+ resolve(content ? JSON.parse(content) : response.result);
36
+ }
37
+ }
38
+ catch (e) {
39
+ reject(e);
40
+ }
41
+ });
42
+ proc.stdin?.end(JSON.stringify({
43
+ jsonrpc: "2.0",
44
+ id: 1,
45
+ method: "tools/call",
46
+ params: { name: toolName, arguments: args },
47
+ }) + "\n");
48
+ });
49
+ }
50
+ async function listToolNames(env) {
51
+ return new Promise((resolve, reject) => {
52
+ const proc = spawn("node", ["build/index.js"], {
53
+ stdio: ["pipe", "pipe", "pipe"],
54
+ env: {
55
+ ...process.env,
56
+ ...env,
57
+ },
58
+ });
59
+ let output = "";
60
+ let errorOutput = "";
61
+ proc.stdout?.on("data", (d) => (output += d));
62
+ proc.stderr?.on("data", (d) => (errorOutput += d));
63
+ proc.on("close", code => {
64
+ if (code !== 0) {
65
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
66
+ }
67
+ const line = output.split("\n").find(l => l.startsWith("{"));
68
+ if (!line)
69
+ return reject(new Error("No JSON output found"));
70
+ try {
71
+ const response = JSON.parse(line);
72
+ if (response.error) {
73
+ reject(response.error);
74
+ }
75
+ else {
76
+ resolve(response.result.tools.map((tool) => tool.name));
77
+ }
78
+ }
79
+ catch (e) {
80
+ reject(e);
81
+ }
82
+ });
83
+ proc.stdin?.end(JSON.stringify({
84
+ jsonrpc: "2.0",
85
+ id: 1,
86
+ method: "tools/list",
87
+ params: {},
88
+ }) + "\n");
89
+ });
90
+ }
91
+ describe("GitLab CI lint tools", () => {
92
+ let mockGitLab;
93
+ let mockGitLabUrl;
94
+ before(async () => {
95
+ const mockPort = await findMockServerPort(9260);
96
+ mockGitLab = new MockGitLabServer({
97
+ port: mockPort,
98
+ validTokens: [MOCK_TOKEN],
99
+ });
100
+ mockGitLab.addMockHandler("post", `/projects/${TEST_PROJECT_ID}/ci/lint`, (req, res) => {
101
+ assert.strictEqual(req.body.content, "stages: [test]\ntest:\n script: echo ok");
102
+ assert.strictEqual(req.body.dry_run, true);
103
+ assert.strictEqual(req.body.include_jobs, true);
104
+ assert.strictEqual(req.body.ref, "main");
105
+ res.json({
106
+ valid: true,
107
+ errors: [],
108
+ warnings: [],
109
+ merged_yaml: "test:\n script: echo ok\n",
110
+ includes: [],
111
+ jobs: [{ name: "test", stage: "test" }],
112
+ });
113
+ });
114
+ mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/ci/lint`, (req, res) => {
115
+ assert.strictEqual(req.query.content_ref, "feature/test");
116
+ assert.strictEqual(req.query.dry_run, "true");
117
+ assert.strictEqual(req.query.dry_run_ref, "main");
118
+ assert.strictEqual(req.query.include_jobs, "true");
119
+ res.json({
120
+ valid: true,
121
+ errors: [],
122
+ warnings: [],
123
+ merged_yaml: "include-job:\n script: echo included\n",
124
+ includes: [{ type: "local", location: "include.yml" }],
125
+ jobs: [{ name: "include-job", stage: "test" }],
126
+ });
127
+ });
128
+ await mockGitLab.start();
129
+ mockGitLabUrl = mockGitLab.getUrl();
130
+ });
131
+ after(async () => {
132
+ await mockGitLab.stop();
133
+ });
134
+ test("validate_ci_lint posts CI YAML content and returns lint result", async () => {
135
+ const result = await callTool("validate_ci_lint", {
136
+ project_id: TEST_PROJECT_ID,
137
+ content: "stages: [test]\ntest:\n script: echo ok",
138
+ dry_run: true,
139
+ include_jobs: true,
140
+ ref: "main",
141
+ }, {
142
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
143
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
144
+ });
145
+ assert.strictEqual(result.valid, true);
146
+ assert.deepStrictEqual(result.errors, []);
147
+ assert.strictEqual(result.jobs[0].name, "test");
148
+ });
149
+ test("validate_ci_lint surfaces invalid lint responses", async () => {
150
+ mockGitLab.addMockHandler("post", `/projects/999/ci/lint`, (req, res) => {
151
+ res.json({
152
+ valid: false,
153
+ errors: ["jobs config should contain at least one visible job"],
154
+ warnings: [],
155
+ });
156
+ });
157
+ const result = await callTool("validate_ci_lint", {
158
+ project_id: "999",
159
+ content: ".hidden:\n script: echo hidden",
160
+ }, {
161
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
162
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
163
+ });
164
+ assert.strictEqual(result.valid, false);
165
+ assert.deepStrictEqual(result.errors, ["jobs config should contain at least one visible job"]);
166
+ });
167
+ test("validate_project_ci_lint sends GET query parameters", async () => {
168
+ const result = await callTool("validate_project_ci_lint", {
169
+ project_id: TEST_PROJECT_ID,
170
+ content_ref: "feature/test",
171
+ dry_run: true,
172
+ dry_run_ref: "main",
173
+ include_jobs: true,
174
+ }, {
175
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
176
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
177
+ });
178
+ assert.strictEqual(result.valid, true);
179
+ assert.strictEqual(result.includes[0].location, "include.yml");
180
+ assert.strictEqual(result.jobs[0].name, "include-job");
181
+ });
182
+ test("CI lint tools are visible by default in read-only mode", async () => {
183
+ const tools = await listToolNames({
184
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
185
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
186
+ GITLAB_READ_ONLY_MODE: "true",
187
+ });
188
+ assert.ok(tools.includes("validate_ci_lint"));
189
+ assert.ok(tools.includes("validate_project_ci_lint"));
190
+ });
191
+ });
@@ -0,0 +1,195 @@
1
+ import { describe, test, before, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { spawn } from "child_process";
4
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
+ const MOCK_TOKEN = "glpat-todos-test-token";
6
+ async function callTool(toolName, args, env) {
7
+ return new Promise((resolve, reject) => {
8
+ const proc = spawn("node", ["build/index.js"], {
9
+ stdio: ["pipe", "pipe", "pipe"],
10
+ env: {
11
+ ...process.env,
12
+ ...env,
13
+ },
14
+ });
15
+ let output = "";
16
+ let errorOutput = "";
17
+ proc.stdout?.on("data", (d) => (output += d));
18
+ proc.stderr?.on("data", (d) => (errorOutput += d));
19
+ proc.on("close", code => {
20
+ if (code !== 0) {
21
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
22
+ }
23
+ const line = output.split("\n").find(l => l.startsWith("{"));
24
+ if (!line)
25
+ return reject(new Error("No JSON output found"));
26
+ try {
27
+ const response = JSON.parse(line);
28
+ if (response.error) {
29
+ reject(response.error);
30
+ }
31
+ else {
32
+ const content = response.result?.content?.[0]?.text;
33
+ resolve(content ? JSON.parse(content) : response.result);
34
+ }
35
+ }
36
+ catch (e) {
37
+ reject(e);
38
+ }
39
+ });
40
+ proc.stdin?.end(JSON.stringify({
41
+ jsonrpc: "2.0",
42
+ id: 1,
43
+ method: "tools/call",
44
+ params: args === undefined ? { name: toolName } : { name: toolName, arguments: args },
45
+ }) + "\n");
46
+ });
47
+ }
48
+ async function listToolNames(env) {
49
+ return new Promise((resolve, reject) => {
50
+ const proc = spawn("node", ["build/index.js"], {
51
+ stdio: ["pipe", "pipe", "pipe"],
52
+ env: {
53
+ ...process.env,
54
+ ...env,
55
+ },
56
+ });
57
+ let output = "";
58
+ let errorOutput = "";
59
+ proc.stdout?.on("data", (d) => (output += d));
60
+ proc.stderr?.on("data", (d) => (errorOutput += d));
61
+ proc.on("close", code => {
62
+ if (code !== 0) {
63
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
64
+ }
65
+ const line = output.split("\n").find(l => l.startsWith("{"));
66
+ if (!line)
67
+ return reject(new Error("No JSON output found"));
68
+ try {
69
+ const response = JSON.parse(line);
70
+ if (response.error) {
71
+ reject(response.error);
72
+ }
73
+ else {
74
+ resolve(response.result.tools.map((tool) => tool.name));
75
+ }
76
+ }
77
+ catch (e) {
78
+ reject(e);
79
+ }
80
+ });
81
+ proc.stdin?.end(JSON.stringify({
82
+ jsonrpc: "2.0",
83
+ id: 1,
84
+ method: "tools/list",
85
+ params: {},
86
+ }) + "\n");
87
+ });
88
+ }
89
+ function todoFixture(id, state = "pending") {
90
+ return {
91
+ id,
92
+ project: { id: 123, path_with_namespace: "group/project" },
93
+ author: { id: 1, username: "root" },
94
+ action_name: "marked",
95
+ target_type: "MergeRequest",
96
+ target: { id: 34, iid: 7, project_id: 123, title: "Review this MR" },
97
+ target_url: "https://gitlab.example.com/group/project/-/merge_requests/7",
98
+ body: "Review this MR",
99
+ state,
100
+ created_at: "2026-01-01T00:00:00.000Z",
101
+ updated_at: "2026-01-01T00:00:00.000Z",
102
+ };
103
+ }
104
+ describe("GitLab todos tools", () => {
105
+ let mockGitLab;
106
+ let mockGitLabUrl;
107
+ before(async () => {
108
+ const mockPort = await findMockServerPort(9250);
109
+ mockGitLab = new MockGitLabServer({
110
+ port: mockPort,
111
+ validTokens: [MOCK_TOKEN],
112
+ });
113
+ mockGitLab.addMockHandler("get", "/todos", (req, res) => {
114
+ assert.strictEqual(req.query.state, "pending");
115
+ assert.strictEqual(req.query.action, "assigned");
116
+ assert.strictEqual(req.query.project_id, "123");
117
+ assert.strictEqual(req.query.page, "2");
118
+ assert.strictEqual(req.query.per_page, "5");
119
+ res.json([todoFixture(102)]);
120
+ });
121
+ mockGitLab.addMockHandler("post", "/todos/102/mark_as_done", (req, res) => {
122
+ res.json(todoFixture(102, "done"));
123
+ });
124
+ mockGitLab.addMockHandler("post", "/todos/mark_as_done", (req, res) => {
125
+ res.status(204).send();
126
+ });
127
+ await mockGitLab.start();
128
+ mockGitLabUrl = mockGitLab.getUrl();
129
+ });
130
+ after(async () => {
131
+ await mockGitLab.stop();
132
+ });
133
+ test("list_todos sends filters and returns todos", async () => {
134
+ const result = await callTool("list_todos", {
135
+ state: "pending",
136
+ action: "assigned",
137
+ project_id: 123,
138
+ page: 2,
139
+ per_page: 5,
140
+ }, {
141
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
142
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
143
+ });
144
+ assert.ok(Array.isArray(result));
145
+ assert.strictEqual(result[0].id, 102);
146
+ assert.strictEqual(result[0].state, "pending");
147
+ });
148
+ test("mark_todo_done marks one todo done", async () => {
149
+ const result = await callTool("mark_todo_done", { id: 102 }, {
150
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
151
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
152
+ });
153
+ assert.strictEqual(result.id, 102);
154
+ assert.strictEqual(result.state, "done");
155
+ });
156
+ test("mark_all_todos_done reports success", async () => {
157
+ const result = await callTool("mark_all_todos_done", {}, {
158
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
159
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
160
+ });
161
+ assert.deepStrictEqual(result, {
162
+ status: "success",
163
+ message: "All pending to-do items marked as done",
164
+ });
165
+ });
166
+ test("mark_all_todos_done accepts omitted arguments", async () => {
167
+ const result = await callTool("mark_all_todos_done", undefined, {
168
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
169
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
170
+ });
171
+ assert.deepStrictEqual(result, {
172
+ status: "success",
173
+ message: "All pending to-do items marked as done",
174
+ });
175
+ });
176
+ test("todo tools are visible in the default issues toolset", async () => {
177
+ const tools = await listToolNames({
178
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
179
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
180
+ });
181
+ assert.ok(tools.includes("list_todos"));
182
+ assert.ok(tools.includes("mark_todo_done"));
183
+ assert.ok(tools.includes("mark_all_todos_done"));
184
+ });
185
+ test("read-only mode keeps list_todos and hides todo mutations", async () => {
186
+ const tools = await listToolNames({
187
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
188
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
189
+ GITLAB_READ_ONLY_MODE: "true",
190
+ });
191
+ assert.ok(tools.includes("list_todos"));
192
+ assert.ok(!tools.includes("mark_todo_done"));
193
+ assert.ok(!tools.includes("mark_all_todos_done"));
194
+ });
195
+ });
@@ -17,11 +17,12 @@ const MCP_PORT_BASE = 3200;
17
17
  // Known tool counts per toolset (from TOOLSET_DEFINITIONS)
18
18
  const TOOLSET_TOOL_COUNTS = {
19
19
  merge_requests: 40,
20
- issues: 20,
20
+ issues: 23,
21
21
  repositories: 7,
22
22
  branches: 4,
23
23
  projects: 8,
24
24
  labels: 5,
25
+ ci: 2,
25
26
  pipelines: 19,
26
27
  milestones: 9,
27
28
  wiki: 10,
@@ -32,6 +33,7 @@ const TOOLSET_TOOL_COUNTS = {
32
33
  workitems: 18,
33
34
  webhooks: 3,
34
35
  };
36
+ const LEGACY_PIPELINE_TOOL_COUNT = TOOLSET_TOOL_COUNTS.pipelines + TOOLSET_TOOL_COUNTS.ci;
35
37
  const DEFAULT_TOOLSETS = [
36
38
  "merge_requests",
37
39
  "issues",
@@ -39,6 +41,7 @@ const DEFAULT_TOOLSETS = [
39
41
  "branches",
40
42
  "projects",
41
43
  "labels",
44
+ "ci",
42
45
  "users",
43
46
  ];
44
47
  const NON_DEFAULT_TOOLSETS = [
@@ -58,11 +61,12 @@ const ALL_TOOLSET_TOOL_COUNT = Object.values(TOOLSET_TOOL_COUNTS).reduce((sum, c
58
61
  // Representative tools per toolset for spot-checking
59
62
  const TOOLSET_SAMPLE_TOOLS = {
60
63
  merge_requests: ["merge_merge_request", "create_merge_request_thread", "list_draft_notes"],
61
- issues: ["create_issue", "list_issues", "create_note"],
64
+ issues: ["create_issue", "list_issues", "create_note", "list_todos"],
62
65
  repositories: ["search_repositories", "get_file_contents", "push_files"],
63
66
  branches: ["create_branch", "list_commits"],
64
67
  projects: ["get_project", "list_namespaces", "list_group_iterations"],
65
68
  labels: ["list_labels", "create_label"],
69
+ ci: ["validate_ci_lint", "validate_project_ci_lint"],
66
70
  pipelines: ["list_pipelines", "create_pipeline", "cancel_pipeline_job", "list_deployments", "list_job_artifacts"],
67
71
  milestones: ["list_milestones", "create_milestone", "get_milestone_burndown_events"],
68
72
  wiki: ["list_wiki_pages", "create_wiki_page", "list_group_wiki_pages", "create_group_wiki_page"],
@@ -260,7 +264,7 @@ describe("Toolset Filtering", { concurrency: 1 }, () => {
260
264
  });
261
265
  after(() => cleanupServers([server]));
262
266
  test("returns issue tools + all pipeline tools + discover_tools", () => {
263
- assert.strictEqual(tools.length, TOOLSET_TOOL_COUNTS.issues + TOOLSET_TOOL_COUNTS.pipelines + DISCOVER_TOOLS_COUNT);
267
+ assert.strictEqual(tools.length, TOOLSET_TOOL_COUNTS.issues + LEGACY_PIPELINE_TOOL_COUNT + DISCOVER_TOOLS_COUNT);
264
268
  });
265
269
  test("includes all pipeline tools via legacy flag", () => {
266
270
  assertContainsAll(tools, TOOLSET_SAMPLE_TOOLS.pipelines, "pipelines");
@@ -297,6 +301,7 @@ describe("Toolset Filtering", { concurrency: 1 }, () => {
297
301
  "list_issue_links",
298
302
  "list_issue_discussions",
299
303
  "get_issue_link",
304
+ "list_todos",
300
305
  "list_issue_emoji_reactions",
301
306
  "list_issue_note_emoji_reactions",
302
307
  ];
@@ -304,6 +309,8 @@ describe("Toolset Filtering", { concurrency: 1 }, () => {
304
309
  "create_issue",
305
310
  "update_issue",
306
311
  "delete_issue",
312
+ "mark_todo_done",
313
+ "mark_all_todos_done",
307
314
  "create_issue_note",
308
315
  "update_issue_note",
309
316
  "create_issue_link",
@@ -396,7 +403,7 @@ describe("Toolset Filtering", { concurrency: 1 }, () => {
396
403
  });
397
404
  after(() => cleanupServers([server]));
398
405
  test("returns exactly pipeline tool count + discover_tools (no duplicates)", () => {
399
- assert.strictEqual(tools.length, TOOLSET_TOOL_COUNTS.pipelines + DISCOVER_TOOLS_COUNT);
406
+ assert.strictEqual(tools.length, LEGACY_PIPELINE_TOOL_COUNT + DISCOVER_TOOLS_COUNT);
400
407
  });
401
408
  });
402
409
  // ---- 12. GITLAB_TOOLS with tool already in enabled toolset (no dupes) ----
@@ -1,7 +1,7 @@
1
1
  import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { toJSONSchema } from "../utils/schema.js";
3
3
  import { USE_GITLAB_WIKI, USE_MILESTONE, USE_PIPELINE, } from "../config.js";
4
- import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListCommitsSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, } from "../schemas.js";
4
+ import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListCommitsSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, } from "../schemas.js";
5
5
  // Define all available tools
6
6
  export const allTools = [
7
7
  {
@@ -316,6 +316,21 @@ export const allTools = [
316
316
  description: "Delete an issue",
317
317
  inputSchema: toJSONSchema(DeleteIssueSchema),
318
318
  },
319
+ {
320
+ name: "list_todos",
321
+ description: "List GitLab to-do items for the current user",
322
+ inputSchema: toJSONSchema(ListTodosSchema),
323
+ },
324
+ {
325
+ name: "mark_todo_done",
326
+ description: "Mark a GitLab to-do item as done",
327
+ inputSchema: toJSONSchema(MarkTodoDoneSchema),
328
+ },
329
+ {
330
+ name: "mark_all_todos_done",
331
+ description: "Mark all pending GitLab to-do items as done for the current user",
332
+ inputSchema: toJSONSchema(MarkAllTodosDoneSchema),
333
+ },
319
334
  {
320
335
  name: "list_issue_links",
321
336
  description: "List all issue links for a specific issue",
@@ -506,6 +521,16 @@ export const allTools = [
506
521
  description: "Get the output/trace of a pipeline job with optional pagination",
507
522
  inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
508
523
  },
524
+ {
525
+ name: "validate_ci_lint",
526
+ description: "Validate provided GitLab CI/CD YAML content for a project",
527
+ inputSchema: toJSONSchema(ValidateCiLintSchema),
528
+ },
529
+ {
530
+ name: "validate_project_ci_lint",
531
+ description: "Validate an existing .gitlab-ci.yml configuration for a project",
532
+ inputSchema: toJSONSchema(ValidateProjectCiLintSchema),
533
+ },
509
534
  {
510
535
  name: "create_pipeline",
511
536
  description: "Create a new pipeline for a branch or tag",
@@ -867,6 +892,7 @@ export const readOnlyTools = new Set([
867
892
  "list_draft_notes",
868
893
  "mr_discussions",
869
894
  "list_issues",
895
+ "list_todos",
870
896
  "my_issues",
871
897
  "list_merge_requests",
872
898
  "get_issue",
@@ -889,6 +915,8 @@ export const readOnlyTools = new Set([
889
915
  "list_pipeline_trigger_jobs",
890
916
  "get_pipeline_job",
891
917
  "get_pipeline_job_output",
918
+ "validate_ci_lint",
919
+ "validate_project_ci_lint",
892
920
  "list_job_artifacts",
893
921
  "download_job_artifacts",
894
922
  "get_job_artifact_file",
@@ -998,6 +1026,8 @@ export const pipelineToolNames = new Set([
998
1026
  "list_pipeline_trigger_jobs",
999
1027
  "get_pipeline_job",
1000
1028
  "get_pipeline_job_output",
1029
+ "validate_ci_lint",
1030
+ "validate_project_ci_lint",
1001
1031
  "create_pipeline",
1002
1032
  "retry_pipeline",
1003
1033
  "cancel_pipeline",
@@ -1065,6 +1095,9 @@ export const TOOLSET_DEFINITIONS = [
1065
1095
  "get_issue",
1066
1096
  "update_issue",
1067
1097
  "delete_issue",
1098
+ "list_todos",
1099
+ "mark_todo_done",
1100
+ "mark_all_todos_done",
1068
1101
  "create_issue_note",
1069
1102
  "update_issue_note",
1070
1103
  "list_issue_links",
@@ -1129,6 +1162,11 @@ export const TOOLSET_DEFINITIONS = [
1129
1162
  "delete_label",
1130
1163
  ]),
1131
1164
  },
1165
+ {
1166
+ id: "ci",
1167
+ isDefault: true,
1168
+ tools: new Set(["validate_ci_lint", "validate_project_ci_lint"]),
1169
+ },
1132
1170
  {
1133
1171
  id: "pipelines",
1134
1172
  isDefault: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [
@@ -51,7 +51,7 @@
51
51
  "changelog": "auto-changelog -p",
52
52
  "test": "npm run test:all",
53
53
  "test:all": "npm run build && npm run test:mock && npm run test:live",
54
- "test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
54
+ "test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
55
55
  "test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
56
56
  "test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
57
57
  "test:live": "node test/validate-api.js",