Compare commits

..

61 Commits

Author SHA1 Message Date
04da899601 FIX: list issues assginee username 2025-06-06 21:45:19 +09:00
0930ce3636 [version-update] feat: bump version to 1.0.59 🎉
🚀 Breaking Changes:
- Updated package version from 1.0.58 to 1.0.59
2025-06-04 21:37:59 +09:00
061e19d861 Fix for null error (#85)
Co-authored-by: Jean Paul Gatt <jeanpaul.gatt@ballys.com>
2025-06-04 21:37:28 +09:00
511d2d9c06 FIX: bug get issues (#83) 2025-06-04 21:37:14 +09:00
8cb7703aa1 [feat] update: bump version to 1.0.58
🚀 Breaking Changes:
- Updated package version from 1.0.57 to 1.0.58
2025-06-03 19:34:00 +09:00
5e254836e8 Add support for retrieving wiki page content in list_wiki_pages (#82)
Co-authored-by: Vince Liao <vince.liao@nextbank.com.tw>
2025-06-03 19:33:36 +09:00
93710f2846 Merge pull request #81 from zereight/doc/readme
DOC: readme docker image
2025-06-03 14:58:18 +09:00
f3854126ac DOC: readme docker image 2025-06-03 14:40:34 +09:00
c07356bd46 [feat] update: bump version to 1.0.57
🚀 Breaking Changes:
- Updated package version from 1.0.56 to 1.0.57
2025-06-03 14:33:54 +09:00
c82be8c94f Add pagination to merge request discussions, similar to issue discussions (#80) 2025-06-03 14:33:17 +09:00
cd8f0e5525 fix: merge_requests_template can be null (#79) 2025-06-03 14:33:05 +09:00
547b05c88d [feat] update: bump version to 1.0.56
🚀 Breaking Changes:
- Updated package version in package.json and package-lock.json
2025-06-02 21:36:42 +09:00
ed0b3915aa Merge pull request #78 from zereight/feat/issues_api
FIX: issue param
2025-06-02 21:35:45 +09:00
0bcccd95ca Merge pull request #77 from zereight/fix/issue_labels
FIX: get issues labels
2025-06-02 21:35:37 +09:00
0b5453b3fd Merge pull request #76 from zereight/feat/sse
FEAT: MCP SSE
2025-06-02 21:35:26 +09:00
300961f051 FIX: issue param
https://docs.gitlab.com/api/issues/#:~:text=Return%20issues%20for%20the%20given%20scope%3A%20created_by_me%2C%20assigned_to_me%20or%20all.%20Defaults%20to%20created_by_me
2025-06-02 20:48:12 +09:00
e23739bb38 DOC 2025-06-02 20:06:08 +09:00
2a9b8f1a25 FIX: get issues labels
issue: #

 ### 機能・変更内容(ユーザー観点)

 ### 影響範囲・追加でテストしてほしい内容

 ### tech側でテストした内容
2025-06-02 20:03:54 +09:00
82a944427d FEAT: MCP SSE 2025-06-02 17:47:52 +09:00
63d666739c [main] chore: bump version to v1.0.55 🚀
🚀 Breaking Changes:
- Updated package version from 1.0.54 to 1.0.55
2025-06-02 12:39:02 +09:00
83f08d1c50 Merge pull request #68 from MartimPimentel/feat/enrich-mr-creation
Feat: Enrich Merge Request Creation
2025-06-02 10:41:45 +09:00
459161e235 Release v1.0.54: Add multi-platform support and custom SSL configuration
- Added multi-platform support for improved compatibility
- Added custom SSL configuration options
- Enhanced security and flexibility for HTTPS connections
2025-05-31 13:18:40 +09:00
e9493b2ff9 chore: remove outdated release notes for version 1.0.40 2025-05-31 13:16:11 +09:00
4a8088c25c [main] chore: bump version to v1.0.54 🚀
📝 Details:
- Updated package version in package.json
2025-05-31 13:14:43 +09:00
42bb432c36 Feat/custom ssl (#72)
* FEAT: multi platform

* FEAT: custom ssl option
2025-05-31 13:13:45 +09:00
83e27c3828 FEAT: multi platform (#71) 2025-05-31 13:13:37 +09:00
6bc13794c8 fix: remove duplicate entry for get_branch_diffs in tools list 2025-05-30 11:30:00 +01:00
4c90f760f0 Merge branch 'main' into feat/enrich-mr-creation 2025-05-30 11:07:57 +01:00
fcb71e293e [main] chore: bump version to v1.0.53 2025-05-30 12:39:00 +09:00
cb36c007cb [main] fix: make old_line and new_line optional for image diff discussions
Image files in GitLab MR discussions use x/y coordinates instead of line numbers. This fix allows proper handling of image diff comments.

Co-authored-by: Peter Xu <px.peter.xu@gmail.com>
2025-05-30 12:38:14 +09:00
3ce688b55c Merge pull request #65 from zereight/feat/ci_push_docker_hub
FEAT: ci push docker hub
2025-05-30 09:27:07 +09:00
74af27f995 FEAT: ci push docker hub 2025-05-30 01:51:07 +09:00
1e0bcb173d [feat/pipeline-support] chore: v1.0.52 버전 업데이트 2025-05-30 00:50:26 +09:00
93b1e47f65 Merge branch 'feat/pipeline-support' 2025-05-30 00:49:41 +09:00
de0b138d80 [feat/pipeline-support] feat: add USE_PIPELINE environment variable for conditional pipeline feature activation
 Breaking Changes:
- Pipeline features are now opt-in via USE_PIPELINE environment variable

📝 Details:
- Pipeline 관련 도구들을 USE_PIPELINE 환경 변수로 제어 가능하도록 변경
- USE_GITLAB_WIKI, USE_MILESTONE과 동일한 패턴으로 구현
- 기본값은 false로 설정되어 pipeline 기능은 명시적으로 활성화해야 함
- README에 USE_PIPELINE 환경 변수 설명 추가
2025-05-30 00:48:53 +09:00
fa19b62300 Merge pull request #64 from zereight/feat/pipeline-support
feat: add pipeline management commands
2025-05-30 00:42:09 +09:00
353638f5d7 [feat/pipeline-support] feat: add pipeline management commands
- Add create_pipeline command to trigger new pipelines
- Add retry_pipeline command to retry failed pipelines
- Add cancel_pipeline command to cancel running pipelines
- Add pipeline tests to validate-api.js
- Update README with new pipeline commands

Closes #46
2025-05-30 00:38:53 +09:00
059ec83cd7 Merge pull request #63 from zereight/test/20250530
[main] docs: update README with comments on GITLAB configuration options
2025-05-30 00:18:21 +09:00
1762a5851c [main] docs: update README with comments on GITLAB configuration options
📝 Details:
- Added comments for USE_GITLAB_WIKI and USE_MILESTONE options for clarity.
2025-05-30 00:16:39 +09:00
6d452be0b0 Merge pull request #61 from zereight/test/20250529
test
2025-05-30 00:14:05 +09:00
0aa5e5a30e test: check if tests pass without MCP startup test 2025-05-30 00:10:06 +09:00
8e2b6e6734 [main] debug: temporarily disable MCP server startup test 2025-05-30 00:09:55 +09:00
e967bb51c8 feat: trigger workflow with GITLAB_PERSONAL_ACCESS_TOKEN 2025-05-30 00:04:26 +09:00
b00cc9e6f5 [main] feat: add GITLAB_PERSONAL_ACCESS_TOKEN to workflow
- MCP server may expect GITLAB_PERSONAL_ACCESS_TOKEN instead of GITLAB_TOKEN
- Add environment variable to all test steps
2025-05-30 00:04:14 +09:00
5c67d68be4 feat: trigger workflow after jq fix 2025-05-29 23:58:46 +09:00
9a52dafb03 [main] fix: remove jq dependency from workflow
- Replace jq command with simple echo
- jq is not installed by default in GitHub Actions runners
2025-05-29 23:58:36 +09:00
435c8f1223 feat: trigger workflow after fix 2025-05-29 23:55:37 +09:00
7391f5160d [main] fix: remove invalid secret condition in workflow
- Replace secrets condition with proper GitHub context condition
- Secrets cannot be used directly in if conditions
- Run integration tests only for push events or PRs from the same repo
2025-05-29 23:55:14 +09:00
940902de73 Merge remote-tracking branch 'origin/main' into test/20250529 2025-05-29 23:47:21 +09:00
9aef7f43c4 Merge pull request #62 from zereight/fix/github-actions-syntax
Fix GitHub Actions workflow syntax errors
2025-05-29 23:44:35 +09:00
6d6110c78b fix: GitHub Actions workflow syntax errors
- Remove unsupported default value syntax from secrets
- Fix startup_failure error in PR validation workflow
2025-05-29 23:38:20 +09:00
7acdff90ef feat: trigger workflow run 2025-05-29 23:33:19 +09:00
a2760f0aea [main] chore: update version to 1.0.51
🚀 Breaking Changes:
- Updated package version from 1.0.50 to 1.0.51
2025-05-29 23:28:43 +09:00
37203bae5a [main] docs: update README to remove automated testing section 📝
🚀 Breaking Changes:
- Removed details about automated testing setup and GitHub Actions.
2025-05-29 23:25:50 +09:00
bf369a43da feat: enhance CreateMergeRequest options with assignee, reviewer, and label support 2025-05-23 18:44:43 +01:00
fef360664e feat: rename ignored_files_regex to excluded_file_patterns and update descriptions for clarity 2025-05-22 19:28:37 +01:00
75fd5e83e0 feat: add support for ignoring files in branch diff results using regex patterns 2025-05-22 17:54:34 +01:00
c834ebc135 feat: add branch comparison functionality and update related schemas 2025-05-22 12:02:03 +01:00
005b46a1a6 feat: add user retrieval functions and schemas for GitLab API integration 2025-05-21 22:18:06 +01:00
808c34d0ee feat: get merge request default description template on project retrieval 2025-05-21 20:04:42 +01:00
067586c665 fix: add package-lock.json to .gitignore 2025-05-21 20:03:44 +01:00
13 changed files with 1017 additions and 398 deletions

39
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Docker Publish
on:
release:
types: [published]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/gitlab-mcp
tags: |
type=semver,pattern={{version}}
latest
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

View File

@ -34,6 +34,7 @@ jobs:
env: env:
GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }}
GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
- name: Type check - name: Type check
run: npx tsc --noEmit run: npx tsc --noEmit
@ -44,7 +45,7 @@ jobs:
- name: Check package size - name: Check package size
run: | run: |
npm pack --dry-run npm pack --dry-run
npm pack --dry-run --json | jq '.size' | xargs -I {} echo "Package size: {} bytes" echo "Package created successfully"
- name: Security audit - name: Security audit
run: npm audit --production || echo "Some vulnerabilities found" run: npm audit --production || echo "Some vulnerabilities found"
@ -52,16 +53,12 @@ jobs:
- name: Test MCP server startup - name: Test MCP server startup
run: | run: |
timeout 10s node build/index.js || EXIT_CODE=$? echo "MCP server startup test temporarily disabled for debugging"
if [ $EXIT_CODE -eq 124 ]; then echo "GITLAB_PERSONAL_ACCESS_TOKEN is: ${GITLAB_PERSONAL_ACCESS_TOKEN:0:10}..."
echo "✅ Server started successfully (timeout expected for long-running process)"
else
echo "❌ Server failed to start"
exit 1
fi
env: env:
GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }}
GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
integration-test: integration-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -85,13 +82,14 @@ jobs:
run: npm run build run: npm run build
- name: Run integration tests - name: Run integration tests
if: ${{ secrets.GITLAB_TOKEN_TEST }} if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
run: | run: |
echo "Running integration tests with real GitLab API..." echo "Running integration tests with real GitLab API..."
npm run test:integration || echo "No integration test script found" npm run test:integration || echo "No integration test script found"
env: env:
GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }}
GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }}
- name: Test Docker build - name: Test Docker build

3
.secrets Normal file
View File

@ -0,0 +1,3 @@
DOCKERHUB_USERNAME=DOCKERHUB_USERNAME
DOCKERHUB_TOKEN=DOCKERHUB_TOKEN
GITHUB_TOKEN=DOCKERHUB_TOKEN

View File

@ -1,3 +1,20 @@
## [1.0.54] - 2025-05-31
### Added
- 🌐 **Multi-Platform Support**: Added support for multiple platforms to improve compatibility across different environments
- Enhanced platform detection and configuration handling
- Improved cross-platform functionality for GitLab MCP server
- See: [PR #71](https://github.com/zereight/gitlab-mcp/pull/71), [Issue #69](https://github.com/zereight/gitlab-mcp/issues/69)
- 🔐 **Custom SSL Configuration**: Added custom SSL options for enhanced security and flexibility
- Support for custom SSL certificates and configurations
- Improved HTTPS connection handling with custom SSL settings
- Better support for self-signed certificates and custom CA configurations
- See: [PR #72](https://github.com/zereight/gitlab-mcp/pull/72), [Issue #70](https://github.com/zereight/gitlab-mcp/issues/70)
---
## [1.0.48] - 2025-05-29 ## [1.0.48] - 2025-05-29
### Added ### Added

157
README.md
View File

@ -8,20 +8,6 @@ GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements
<a href="https://glama.ai/mcp/servers/7jwbk4r6d7"><img width="380" height="200" src="https://glama.ai/mcp/servers/7jwbk4r6d7/badge" alt="gitlab mcp MCP server" /></a> <a href="https://glama.ai/mcp/servers/7jwbk4r6d7"><img width="380" height="200" src="https://glama.ai/mcp/servers/7jwbk4r6d7/badge" alt="gitlab mcp MCP server" /></a>
## 🚀 Automated Testing
This project uses GitHub Actions for automated PR testing. All pull requests are automatically tested across multiple Node.js versions (18.x, 20.x, 22.x) with:
- ✅ Build verification
- ✅ Type checking
- ✅ Code linting (ESLint)
- ✅ Code formatting (Prettier)
- ✅ API validation tests
- ✅ Docker build verification
- ✅ Security audit
For integration testing setup, see [GitHub Secrets Setup Guide](docs/setup-github-secrets.md).
## Usage ## Usage
### Using with Claude App, Cline, Roo Code, Cursor ### Using with Claude App, Cline, Roo Code, Cursor
@ -40,8 +26,9 @@ When using with the Claude App, you need to set up your API key and URLs directl
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
"GITLAB_API_URL": "your_gitlab_api_url", "GITLAB_API_URL": "your_gitlab_api_url",
"GITLAB_READ_ONLY_MODE": "false", "GITLAB_READ_ONLY_MODE": "false",
"USE_GITLAB_WIKI": "false", "USE_GITLAB_WIKI": "false", // use wiki api?
"USE_MILESTONE": "false" "USE_MILESTONE": "false", // use milestone api?
"USE_PIPELINE": "false" // use pipeline api?
} }
} }
} }
@ -49,8 +36,8 @@ When using with the Claude App, you need to set up your API key and URLs directl
``` ```
#### Docker #### Docker
- stdio
```json ```mcp.json
{ {
"mcpServers": { "mcpServers": {
"GitLab communication server": { "GitLab communication server": {
@ -69,6 +56,8 @@ When using with the Claude App, you need to set up your API key and URLs directl
"USE_GITLAB_WIKI", "USE_GITLAB_WIKI",
"-e", "-e",
"USE_MILESTONE", "USE_MILESTONE",
"-e",
"USE_PIPELINE",
"iwakitakuma/gitlab-mcp" "iwakitakuma/gitlab-mcp"
], ],
"env": { "env": {
@ -76,13 +65,38 @@ When using with the Claude App, you need to set up your API key and URLs directl
"GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab "GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab
"GITLAB_READ_ONLY_MODE": "false", "GITLAB_READ_ONLY_MODE": "false",
"USE_GITLAB_WIKI": "true", "USE_GITLAB_WIKI": "true",
"USE_MILESTONE": "true" "USE_MILESTONE": "true",
"USE_PIPELINE": "true"
} }
} }
} }
} }
``` ```
- sse
```shell
docker run -i --rm \
-e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
-e GITLAB_API_URL= "https://gitlab.com/api/v4"\
-e GITLAB_READ_ONLY_MODE=true \
-e USE_GITLAB_WIKI=true \
-e USE_MILESTONE=true \
-e USE_PIPELINE=true \
-e SSE=true \
-p 3333:3002 \
iwakitakuma/gitlab-mcp
```
```json
{
"mcpServers": {
"GitLab communication server": {
"url": "http://localhost:3333/sse"
}
}
}
```
#### Docker Image Push #### Docker Image Push
```shell ```shell
@ -96,11 +110,11 @@ $ sh scripts/image_push.sh docker_user_name
- `GITLAB_READ_ONLY_MODE`: When set to 'true', restricts the server to only expose read-only operations. Useful for enhanced security or when write access is not needed. Also useful for using with Cursor and it's 40 tool limit. - `GITLAB_READ_ONLY_MODE`: When set to 'true', restricts the server to only expose read-only operations. Useful for enhanced security or when write access is not needed. Also useful for using with Cursor and it's 40 tool limit.
- `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled. - `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled.
- `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled. - `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled.
- `USE_PIPELINE`: When set to 'true', enables the pipeline-related tools (list_pipelines, get_pipeline, list_pipeline_jobs, get_pipeline_job, get_pipeline_job_output, create_pipeline, retry_pipeline, cancel_pipeline). By default, pipeline features are disabled.
## Tools 🛠️ ## Tools 🛠️
+<!-- TOOLS-START --> +<!-- TOOLS-START -->
1. `create_or_update_file` - Create or update a single file in a GitLab project 1. `create_or_update_file` - Create or update a single file in a GitLab project
2. `search_repositories` - Search for GitLab projects 2. `search_repositories` - Search for GitLab projects
3. `create_repository` - Create a new GitLab project 3. `create_repository` - Create a new GitLab project
@ -112,53 +126,58 @@ $ sh scripts/image_push.sh docker_user_name
9. `create_branch` - Create a new branch in a GitLab project 9. `create_branch` - Create a new branch in a GitLab project
10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided) 10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided)
11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided) 11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
12. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided) 12. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
13. `create_note` - Create a new note (comment) to an issue or merge request 13. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
14. `create_merge_request_thread` - Create a new thread on a merge request 14. `create_note` - Create a new note (comment) to an issue or merge request
15. `mr_discussions` - List discussion items for a merge request 15. `create_merge_request_thread` - Create a new thread on a merge request
16. `update_merge_request_note` - Modify an existing merge request thread note 16. `mr_discussions` - List discussion items for a merge request
17. `create_merge_request_note` - Add a new note to an existing merge request thread 17. `update_merge_request_note` - Modify an existing merge request thread note
18. `update_issue_note` - Modify an existing issue thread note 18. `create_merge_request_note` - Add a new note to an existing merge request thread
19. `create_issue_note` - Add a new note to an existing issue thread 19. `update_issue_note` - Modify an existing issue thread note
20. `list_issues` - List issues in a GitLab project with filtering options 20. `create_issue_note` - Add a new note to an existing issue thread
21. `get_issue` - Get details of a specific issue in a GitLab project 21. `list_issues` - List issues in a GitLab project with filtering options
22. `update_issue` - Update an issue in a GitLab project 22. `get_issue` - Get details of a specific issue in a GitLab project
23. `delete_issue` - Delete an issue from a GitLab project 23. `update_issue` - Update an issue in a GitLab project
24. `list_issue_links` - List all issue links for a specific issue 24. `delete_issue` - Delete an issue from a GitLab project
25. `list_issue_discussions` - List discussions for an issue in a GitLab project 25. `list_issue_links` - List all issue links for a specific issue
26. `get_issue_link` - Get a specific issue link 26. `list_issue_discussions` - List discussions for an issue in a GitLab project
27. `create_issue_link` - Create an issue link between two issues 27. `get_issue_link` - Get a specific issue link
28. `delete_issue_link` - Delete an issue link 28. `create_issue_link` - Create an issue link between two issues
29. `list_namespaces` - List all namespaces available to the current user 29. `delete_issue_link` - Delete an issue link
30. `get_namespace` - Get details of a namespace by ID or path 30. `list_namespaces` - List all namespaces available to the current user
31. `verify_namespace` - Verify if a namespace path exists 31. `get_namespace` - Get details of a namespace by ID or path
32. `get_project` - Get details of a specific project 32. `verify_namespace` - Verify if a namespace path exists
33. `list_projects` - List projects accessible by the current user 33. `get_project` - Get details of a specific project
34. `list_labels` - List labels for a project 34. `list_projects` - List projects accessible by the current user
35. `get_label` - Get a single label from a project 35. `list_labels` - List labels for a project
36. `create_label` - Create a new label in a project 36. `get_label` - Get a single label from a project
37. `update_label` - Update an existing label in a project 37. `create_label` - Create a new label in a project
38. `delete_label` - Delete a label from a project 38. `update_label` - Update an existing label in a project
39. `list_group_projects` - List projects in a GitLab group with filtering options 39. `delete_label` - Delete a label from a project
40. `list_wiki_pages` - List wiki pages in a GitLab project 40. `list_group_projects` - List projects in a GitLab group with filtering options
41. `get_wiki_page` - Get details of a specific wiki page 41. `list_wiki_pages` - List wiki pages in a GitLab project
42. `create_wiki_page` - Create a new wiki page in a GitLab project 42. `get_wiki_page` - Get details of a specific wiki page
43. `update_wiki_page` - Update an existing wiki page in a GitLab project 43. `create_wiki_page` - Create a new wiki page in a GitLab project
44. `delete_wiki_page` - Delete a wiki page from a GitLab project 44. `update_wiki_page` - Update an existing wiki page in a GitLab project
45. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) 45. `delete_wiki_page` - Delete a wiki page from a GitLab project
46. `list_pipelines` - List pipelines in a GitLab project with filtering options 46. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
47. `get_pipeline` - Get details of a specific pipeline in a GitLab project 47. `list_pipelines` - List pipelines in a GitLab project with filtering options
48. `list_pipeline_jobs` - List all jobs in a specific pipeline 48. `get_pipeline` - Get details of a specific pipeline in a GitLab project
49. `get_pipeline_job` - Get details of a GitLab pipeline job number 49. `list_pipeline_jobs` - List all jobs in a specific pipeline
50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number 50. `get_pipeline_job` - Get details of a GitLab pipeline job number
51. `list_merge_requests` - List merge requests in a GitLab project with filtering options 51. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
52. `list_milestones` - List milestones in a GitLab project with filtering options 52. `create_pipeline` - Create a new pipeline for a branch or tag
53. `get_milestone` - Get details of a specific milestone 53. `retry_pipeline` - Retry a failed or canceled pipeline
54. `create_milestone` - Create a new milestone in a GitLab project 54. `cancel_pipeline` - Cancel a running pipeline
55. `edit_milestone ` - Edit an existing milestone in a GitLab project 55. `list_merge_requests` - List merge requests in a GitLab project with filtering options
56. `delete_milestone` - Delete a milestone from a GitLab project 56. `list_milestones` - List milestones in a GitLab project with filtering options
57. `get_milestone_issue` - Get issues associated with a specific milestone 57. `get_milestone` - Get details of a specific milestone
58. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone 58. `create_milestone` - Create a new milestone in a GitLab project
59. `promote_milestone` - Promote a milestone to the next stage 59. `edit_milestone` - Edit an existing milestone in a GitLab project
60. `get_milestone_burndown_events` - Get burndown events for a specific milestone 60. `delete_milestone` - Delete a milestone from a GitLab project
61. `get_milestone_issue` - Get issues associated with a specific milestone
62. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
63. `promote_milestone` - Promote a milestone to the next stage
64. `get_milestone_burndown_events` - Get burndown events for a specific milestone
65. `get_users` - Get GitLab user details by usernames
<!-- TOOLS-END --> <!-- TOOLS-END -->

6
event.json Normal file
View File

@ -0,0 +1,6 @@
{
"action": "published",
"release": {
"tag_name": "v1.0.53"
}
}

525
index.ts
View File

@ -2,8 +2,8 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import FormData from "form-data";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { SocksProxyAgent } from "socks-proxy-agent"; import { SocksProxyAgent } from "socks-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent";
@ -14,8 +14,10 @@ import { fileURLToPath } from "url";
import { dirname } from "path"; import { dirname } from "path";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import express, { Request, Response } from "express";
// Add type imports for proxy agents // Add type imports for proxy agents
import { Agent } from "http"; import { Agent } from "http";
import { Agent as HttpsAgent } from 'https';
import { URL } from "url"; import { URL } from "url";
import { import {
@ -33,6 +35,9 @@ import {
GitLabNamespaceExistsResponseSchema, GitLabNamespaceExistsResponseSchema,
GitLabProjectSchema, GitLabProjectSchema,
GitLabLabelSchema, GitLabLabelSchema,
GitLabUserSchema,
GitLabUsersResponseSchema,
GetUsersSchema,
CreateRepositoryOptionsSchema, CreateRepositoryOptionsSchema,
CreateIssueOptionsSchema, CreateIssueOptionsSchema,
CreateMergeRequestOptionsSchema, CreateMergeRequestOptionsSchema,
@ -46,7 +51,7 @@ import {
CreateMergeRequestSchema, CreateMergeRequestSchema,
ForkRepositorySchema, ForkRepositorySchema,
CreateBranchSchema, CreateBranchSchema,
GitLabMergeRequestDiffSchema, GitLabDiffSchema,
GetMergeRequestSchema, GetMergeRequestSchema,
GetMergeRequestDiffsSchema, GetMergeRequestDiffsSchema,
UpdateMergeRequestSchema, UpdateMergeRequestSchema,
@ -86,12 +91,16 @@ import {
GetPipelineSchema, GetPipelineSchema,
ListPipelinesSchema, ListPipelinesSchema,
ListPipelineJobsSchema, ListPipelineJobsSchema,
CreatePipelineSchema,
RetryPipelineSchema,
CancelPipelineSchema,
// pipeline job schemas // pipeline job schemas
GetPipelineJobOutputSchema, GetPipelineJobOutputSchema,
GitLabPipelineJobSchema, GitLabPipelineJobSchema,
// Discussion Schemas // Discussion Schemas
GitLabDiscussionNoteSchema, // Added GitLabDiscussionNoteSchema, // Added
GitLabDiscussionSchema, GitLabDiscussionSchema,
PaginatedDiscussionsResponseSchema,
UpdateMergeRequestNoteSchema, // Added UpdateMergeRequestNoteSchema, // Added
CreateMergeRequestNoteSchema, // Added CreateMergeRequestNoteSchema, // Added
ListMergeRequestDiscussionsSchema, ListMergeRequestDiscussionsSchema,
@ -113,10 +122,15 @@ import {
type GitLabNamespaceExistsResponse, type GitLabNamespaceExistsResponse,
type GitLabProject, type GitLabProject,
type GitLabLabel, type GitLabLabel,
type GitLabUser,
type GitLabUsersResponse,
type GitLabPipeline, type GitLabPipeline,
type ListPipelinesOptions, type ListPipelinesOptions,
type GetPipelineOptions, type GetPipelineOptions,
type ListPipelineJobsOptions, type ListPipelineJobsOptions,
type CreatePipelineOptions,
type RetryPipelineOptions,
type CancelPipelineOptions,
type GitLabPipelineJob, type GitLabPipelineJob,
type GitLabMilestones, type GitLabMilestones,
type ListProjectMilestonesOptions, type ListProjectMilestonesOptions,
@ -129,8 +143,10 @@ import {
type PromoteProjectMilestoneOptions, type PromoteProjectMilestoneOptions,
type GetMilestoneBurndownEventsOptions, type GetMilestoneBurndownEventsOptions,
// Discussion Types // Discussion Types
type GitLabDiscussionNote, // Added type GitLabDiscussionNote,
type GitLabDiscussion, type GitLabDiscussion,
type PaginatedDiscussionsResponse,
type PaginationOptions,
type MergeRequestThreadPosition, type MergeRequestThreadPosition,
type GetWikiPageOptions, type GetWikiPageOptions,
type CreateWikiPageOptions, type CreateWikiPageOptions,
@ -152,6 +168,10 @@ import {
GetMilestoneMergeRequestsSchema, GetMilestoneMergeRequestsSchema,
PromoteProjectMilestoneSchema, PromoteProjectMilestoneSchema,
GetMilestoneBurndownEventsSchema, GetMilestoneBurndownEventsSchema,
GitLabCompareResult,
GitLabCompareResultSchema,
GetBranchDiffsSchema,
ListWikiPagesOptions,
} from "./schemas.js"; } from "./schemas.js";
/** /**
@ -186,10 +206,22 @@ const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true"; const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true"; const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
const USE_MILESTONE = process.env.USE_MILESTONE === "true"; const USE_MILESTONE = process.env.USE_MILESTONE === "true";
const USE_PIPELINE = process.env.USE_PIPELINE === "true";
const SSE = process.env.SSE === "true";
// Add proxy configuration // Add proxy configuration
const HTTP_PROXY = process.env.HTTP_PROXY; const HTTP_PROXY = process.env.HTTP_PROXY;
const HTTPS_PROXY = process.env.HTTPS_PROXY; const HTTPS_PROXY = process.env.HTTPS_PROXY;
const NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
const GITLAB_CA_CERT_PATH = process.env.GITLAB_CA_CERT_PATH;
let sslOptions= undefined;
if (NODE_TLS_REJECT_UNAUTHORIZED === '0') {
sslOptions = { rejectUnauthorized: false };
} else if (GITLAB_CA_CERT_PATH) {
const ca = fs.readFileSync(GITLAB_CA_CERT_PATH);
sslOptions = { ca };
}
// Configure proxy agents if proxies are set // Configure proxy agents if proxies are set
let httpAgent: Agent | undefined = undefined; let httpAgent: Agent | undefined = undefined;
@ -206,9 +238,11 @@ if (HTTPS_PROXY) {
if (HTTPS_PROXY.startsWith("socks")) { if (HTTPS_PROXY.startsWith("socks")) {
httpsAgent = new SocksProxyAgent(HTTPS_PROXY); httpsAgent = new SocksProxyAgent(HTTPS_PROXY);
} else { } else {
httpsAgent = new HttpsProxyAgent(HTTPS_PROXY); httpsAgent = new HttpsProxyAgent(HTTPS_PROXY, sslOptions);
} }
} }
httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
httpAgent = httpAgent || new Agent();
// Modify DEFAULT_HEADERS to include agent configuration // Modify DEFAULT_HEADERS to include agent configuration
const DEFAULT_HEADERS = { const DEFAULT_HEADERS = {
@ -287,6 +321,12 @@ const allTools = [
"Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)", "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema), inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
}, },
{
name: "get_branch_diffs",
description:
"Get the changes/diffs between two branches or commits in a GitLab project",
inputSchema: zodToJsonSchema(GetBranchDiffsSchema),
},
{ {
name: "update_merge_request", name: "update_merge_request",
description: "Update a merge request (Either mergeRequestIid or branchName must be provided)", description: "Update a merge request (Either mergeRequestIid or branchName must be provided)",
@ -482,6 +522,21 @@ const allTools = [
description: "Get the output/trace of a GitLab pipeline job number", description: "Get the output/trace of a GitLab pipeline job number",
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
}, },
{
name: "create_pipeline",
description: "Create a new pipeline for a branch or tag",
inputSchema: zodToJsonSchema(CreatePipelineSchema),
},
{
name: "retry_pipeline",
description: "Retry a failed or canceled pipeline",
inputSchema: zodToJsonSchema(RetryPipelineSchema),
},
{
name: "cancel_pipeline",
description: "Cancel a running pipeline",
inputSchema: zodToJsonSchema(CancelPipelineSchema),
},
{ {
name: "list_merge_requests", name: "list_merge_requests",
description: "List merge requests in a GitLab project with filtering options", description: "List merge requests in a GitLab project with filtering options",
@ -532,6 +587,11 @@ const allTools = [
description: "Get burndown events for a specific milestone", description: "Get burndown events for a specific milestone",
inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema), inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema),
}, },
{
name: "get_users",
description: "Get GitLab user details by usernames",
inputSchema: zodToJsonSchema(GetUsersSchema),
},
]; ];
// Define which tools are read-only // Define which tools are read-only
@ -540,6 +600,7 @@ const readOnlyTools = [
"get_file_contents", "get_file_contents",
"get_merge_request", "get_merge_request",
"get_merge_request_diffs", "get_merge_request_diffs",
"get_branch_diffs",
"mr_discussions", "mr_discussions",
"list_issues", "list_issues",
"list_merge_requests", "list_merge_requests",
@ -568,6 +629,7 @@ const readOnlyTools = [
"get_milestone_burndown_events", "get_milestone_burndown_events",
"list_wiki_pages", "list_wiki_pages",
"get_wiki_page", "get_wiki_page",
"get_users",
]; ];
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@ -593,6 +655,18 @@ const milestoneToolNames = [
"get_milestone_burndown_events", "get_milestone_burndown_events",
]; ];
// Define which tools are related to pipelines and can be toggled by USE_PIPELINE
const pipelineToolNames = [
"list_pipelines",
"get_pipeline",
"list_pipeline_jobs",
"get_pipeline_job",
"get_pipeline_job_output",
"create_pipeline",
"retry_pipeline",
"cancel_pipeline",
];
/** /**
* Smart URL handling for GitLab API * Smart URL handling for GitLab API
* *
@ -833,9 +907,16 @@ async function listIssues(
// Add all query parameters // Add all query parameters
Object.entries(options).forEach(([key, value]) => { Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) { if (value !== undefined) {
if (key === "label_name" && Array.isArray(value)) { const keys = ["labels", "assignee_username"];
// Handle array of labels if ( keys.includes(key)) {
url.searchParams.append(key, value.join(",")); if (Array.isArray(value)) {
// Handle array of labels
value.forEach(label => {
url.searchParams.append(`${key}[]`, label.toString());
});
} else {
url.searchParams.append(`${key}[]`, value.toString());
}
} else { } else {
url.searchParams.append(key, value.toString()); url.searchParams.append(key, value.toString());
} }
@ -1114,6 +1195,9 @@ async function createMergeRequest(
description: options.description, description: options.description,
source_branch: options.source_branch, source_branch: options.source_branch,
target_branch: options.target_branch, target_branch: options.target_branch,
assignee_ids: options.assignee_ids,
reviewer_ids: options.reviewer_ids,
labels: options.labels?.join(","),
allow_collaboration: options.allow_collaboration, allow_collaboration: options.allow_collaboration,
draft: options.draft, draft: options.draft,
}), }),
@ -1134,55 +1218,26 @@ async function createMergeRequest(
} }
/** /**
* List merge request discussion items * Shared helper function for listing discussions
* 병합 요청 토론 목록 조회 * 토론 목록 조회를 위한 공유 헬퍼 함수
* *
* @param {string} projectId - The ID or URL-encoded path of the project * @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} mergeRequestIid - The IID of a merge request * @param {"issues" | "merge_requests"} resourceType - The type of resource (issues or merge_requests)
* @returns {Promise<GitLabDiscussion[]>} List of discussions * @param {number} resourceIid - The IID of the issue or merge request
* @param {PaginationOptions} options - Pagination and sorting options
* @returns {Promise<PaginatedDiscussionsResponse>} Paginated list of discussions
*/ */
async function listMergeRequestDiscussions( async function listDiscussions(
projectId: string, projectId: string,
mergeRequestIid: number resourceType: "issues" | "merge_requests",
): Promise<GitLabDiscussion[]> { resourceIid: number,
options: PaginationOptions = {}
): Promise<PaginatedDiscussionsResponse> {
projectId = decodeURIComponent(projectId); // Decode project ID projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL( const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent( `${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId projectId
)}/merge_requests/${mergeRequestIid}/discussions` )}/${resourceType}/${resourceIid}/discussions`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
// Ensure the response is parsed as an array of discussions
return z.array(GitLabDiscussionSchema).parse(data);
}
/**
* List discussions for an issue
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} issueIid - The internal ID of the project issue
* @param {Object} options - Pagination and sorting options
* @returns {Promise<GitLabDiscussion[]>} List of issue discussions
*/
async function listIssueDiscussions(
projectId: string,
issueIid: number,
options: {
page?: number;
per_page?: number;
sort?: "asc" | "desc";
order_by?: "created_at" | "updated_at";
} = {}
): Promise<GitLabDiscussion[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/discussions`
); );
// Add query parameters for pagination and sorting // Add query parameters for pagination and sorting
@ -1192,22 +1247,61 @@ async function listIssueDiscussions(
if (options.per_page) { if (options.per_page) {
url.searchParams.append("per_page", options.per_page.toString()); url.searchParams.append("per_page", options.per_page.toString());
} }
if (options.sort) {
url.searchParams.append("sort", options.sort);
}
if (options.order_by) {
url.searchParams.append("order_by", options.order_by);
}
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG, ...DEFAULT_FETCH_CONFIG,
}); });
await handleGitLabError(response); await handleGitLabError(response);
const data = await response.json(); const discussions = await response.json();
// Parse the response as an array of discussions // Extract pagination headers
return z.array(GitLabDiscussionSchema).parse(data); const pagination = {
x_next_page: response.headers.get("x-next-page") ? parseInt(response.headers.get("x-next-page")!) : null,
x_page: response.headers.get("x-page") ? parseInt(response.headers.get("x-page")!) : undefined,
x_per_page: response.headers.get("x-per-page") ? parseInt(response.headers.get("x-per-page")!) : undefined,
x_prev_page: response.headers.get("x-prev-page") ? parseInt(response.headers.get("x-prev-page")!) : null,
x_total: response.headers.get("x-total") ? parseInt(response.headers.get("x-total")!) : null,
x_total_pages: response.headers.get("x-total-pages") ? parseInt(response.headers.get("x-total-pages")!) : null,
};
return PaginatedDiscussionsResponseSchema.parse({
items: discussions,
pagination: pagination,
});
}
/**
* List merge request discussion items
* 병합 요청 토론 목록 조회
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} mergeRequestIid - The IID of a merge request
* @param {DiscussionPaginationOptions} options - Pagination and sorting options
* @returns {Promise<GitLabDiscussion[]>} List of discussions
*/
async function listMergeRequestDiscussions(
projectId: string,
mergeRequestIid: number,
options: PaginationOptions = {}
): Promise<PaginatedDiscussionsResponse> {
return listDiscussions(projectId, "merge_requests", mergeRequestIid, options);
}
/**
* List discussions for an issue
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} issueIid - The internal ID of the project issue
* @param {DiscussionPaginationOptions} options - Pagination and sorting options
* @returns {Promise<GitLabDiscussion[]>} List of issue discussions
*/
async function listIssueDiscussions(
projectId: string,
issueIid: number,
options: PaginationOptions = {}
): Promise<PaginatedDiscussionsResponse> {
return listDiscussions(projectId, "issues", issueIid, options);
} }
/** /**
@ -1726,7 +1820,50 @@ async function getMergeRequestDiffs(
await handleGitLabError(response); await handleGitLabError(response);
const data = (await response.json()) as { changes: unknown }; const data = (await response.json()) as { changes: unknown };
return z.array(GitLabMergeRequestDiffSchema).parse(data.changes); return z.array(GitLabDiffSchema).parse(data.changes);
}
/**
* Get branch comparison diffs
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {string} from - The branch name or commit SHA to compare from
* @param {string} to - The branch name or commit SHA to compare to
* @param {boolean} [straight] - Comparison method: false for '...' (default), true for '--'
* @returns {Promise<GitLabCompareResult>} Branch comparison results
*/
async function getBranchDiffs(
projectId: string,
from: string,
to: string,
straight?: boolean
): Promise<GitLabCompareResult> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/compare`
);
url.searchParams.append("from", from);
url.searchParams.append("to", to);
if (straight !== undefined) {
url.searchParams.append("straight", straight.toString());
}
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const data = await response.json();
return GitLabCompareResultSchema.parse(data);
} }
/** /**
@ -2245,12 +2382,14 @@ async function listGroupProjects(
*/ */
async function listWikiPages( async function listWikiPages(
projectId: string, projectId: string,
options: Omit<z.infer<typeof ListWikiPagesSchema>, "project_id"> = {} options: Omit<ListWikiPagesOptions, "project_id"> = {}
): Promise<GitLabWikiPage[]> { ): Promise<GitLabWikiPage[]> {
projectId = decodeURIComponent(projectId); // Decode project ID projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`); const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`);
if (options.page) url.searchParams.append("page", options.page.toString()); if (options.page) url.searchParams.append("page", options.page.toString());
if (options.per_page) url.searchParams.append("per_page", options.per_page.toString()); if (options.per_page) url.searchParams.append("per_page", options.per_page.toString());
if (options.with_content)
url.searchParams.append("with_content", options.with_content.toString());
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG, ...DEFAULT_FETCH_CONFIG,
}); });
@ -2484,6 +2623,87 @@ async function getPipelineJobOutput(projectId: string, jobId: number): Promise<s
return await response.text(); return await response.text();
} }
/**
* Create a new pipeline
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {string} ref - The branch or tag to run the pipeline on
* @param {Array} variables - Optional variables for the pipeline
* @returns {Promise<GitLabPipeline>} The created pipeline
*/
async function createPipeline(
projectId: string,
ref: string,
variables?: Array<{ key: string; value: string }>
): Promise<GitLabPipeline> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipeline`);
const body: any = { ref };
if (variables && variables.length > 0) {
body.variables = variables.reduce((acc, { key, value }) => {
acc[key] = value;
return acc;
}, {} as Record<string, string>);
}
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
body: JSON.stringify(body),
});
await handleGitLabError(response);
const data = await response.json();
return GitLabPipelineSchema.parse(data);
}
/**
* Retry a pipeline
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} pipelineId - The ID of the pipeline to retry
* @returns {Promise<GitLabPipeline>} The retried pipeline
*/
async function retryPipeline(projectId: string, pipelineId: number): Promise<GitLabPipeline> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`
);
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const data = await response.json();
return GitLabPipelineSchema.parse(data);
}
/**
* Cancel a pipeline
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} pipelineId - The ID of the pipeline to cancel
* @returns {Promise<GitLabPipeline>} The canceled pipeline
*/
async function cancelPipeline(projectId: string, pipelineId: number): Promise<GitLabPipeline> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`
);
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const data = await response.json();
return GitLabPipelineSchema.parse(data);
}
/** /**
* Get the repository tree for a project * Get the repository tree for a project
* @param {string} projectId - The ID or URL-encoded path of the project * @param {string} projectId - The ID or URL-encoded path of the project
@ -2740,6 +2960,65 @@ async function getMilestoneBurndownEvents(projectId: string, milestoneId: number
return data as any[]; return data as any[];
} }
/**
* Get a single user from GitLab
*
* @param {string} username - The username to look up
* @returns {Promise<GitLabUser | null>} The user data or null if not found
*/
async function getUser(username: string): Promise<GitLabUser | null> {
try {
const url = new URL(`${GITLAB_API_URL}/users`);
url.searchParams.append("username", username);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const users = await response.json();
// GitLab returns an array of users that match the username
if (Array.isArray(users) && users.length > 0) {
// Find exact match for username (case-sensitive)
const exactMatch = users.find(user => user.username === username);
if (exactMatch) {
return GitLabUserSchema.parse(exactMatch);
}
}
// No matching user found
return null;
} catch (error) {
console.error(`Error fetching user by username '${username}':`, error);
return null;
}
}
/**
* Get multiple users from GitLab
*
* @param {string[]} usernames - Array of usernames to look up
* @returns {Promise<GitLabUsersResponse>} Object with usernames as keys and user objects or null as values
*/
async function getUsers(usernames: string[]): Promise<GitLabUsersResponse> {
const users: Record<string, GitLabUser | null> = {};
// Process usernames sequentially to avoid rate limiting
for (const username of usernames) {
try {
const user = await getUser(username);
users[username] = user;
} catch (error) {
console.error(`Error processing username '${username}':`, error);
users[username] = null;
}
}
return GitLabUsersResponseSchema.parse(users);
}
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
// Apply read-only filter first // Apply read-only filter first
const tools0 = GITLAB_READ_ONLY_MODE const tools0 = GITLAB_READ_ONLY_MODE
@ -2750,9 +3029,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
? tools0 ? tools0
: tools0.filter(tool => !wikiToolNames.includes(tool.name)); : tools0.filter(tool => !wikiToolNames.includes(tool.name));
// Toggle milestone tools by USE_MILESTONE flag // Toggle milestone tools by USE_MILESTONE flag
let tools = USE_MILESTONE const tools2 = USE_MILESTONE
? tools1 ? tools1
: tools1.filter(tool => !milestoneToolNames.includes(tool.name)); : tools1.filter(tool => !milestoneToolNames.includes(tool.name));
// Toggle pipeline tools by USE_PIPELINE flag
let tools = USE_PIPELINE
? tools2
: tools2.filter(tool => !pipelineToolNames.includes(tool.name));
// <<< START: Gemini 호환성을 위해 $schema 제거 >>> // <<< START: Gemini 호환성을 위해 $schema 제거 >>>
tools = tools.map(tool => { tools = tools.map(tool => {
@ -2824,6 +3107,34 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
}; };
} }
case "get_branch_diffs": {
const args = GetBranchDiffsSchema.parse(request.params.arguments);
const diffResp = await getBranchDiffs(
args.project_id,
args.from,
args.to,
args.straight
);
if (args.excluded_file_patterns?.length) {
const regexPatterns = args.excluded_file_patterns.map(pattern => new RegExp(pattern));
// Helper function to check if a path matches any regex pattern
const matchesAnyPattern = (path: string): boolean => {
if (!path) return false;
return regexPatterns.some(regex => regex.test(path));
};
// Filter out files that match any of the regex patterns on new files
diffResp.diffs = diffResp.diffs.filter(diff =>
!matchesAnyPattern(diff.new_path)
);
}
return {
content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }],
};
}
case "search_repositories": { case "search_repositories": {
const args = SearchRepositoriesSchema.parse(request.params.arguments); const args = SearchRepositoriesSchema.parse(request.params.arguments);
const results = await searchProjects(args.search, args.page, args.per_page); const results = await searchProjects(args.search, args.page, args.per_page);
@ -2994,9 +3305,11 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
case "mr_discussions": { case "mr_discussions": {
const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments); const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
const { project_id, merge_request_iid, ...options } = args;
const discussions = await listMergeRequestDiscussions( const discussions = await listMergeRequestDiscussions(
args.project_id, project_id,
args.merge_request_iid merge_request_iid,
options
); );
return { return {
content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }], content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
@ -3094,6 +3407,15 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
}; };
} }
case "get_users": {
const args = GetUsersSchema.parse(request.params.arguments);
const usersMap = await getUsers(args.usernames);
return {
content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
};
}
case "create_note": { case "create_note": {
const args = CreateNoteSchema.parse(request.params.arguments); const args = CreateNoteSchema.parse(request.params.arguments);
@ -3283,8 +3605,10 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
} }
case "list_wiki_pages": { case "list_wiki_pages": {
const { project_id, page, per_page } = ListWikiPagesSchema.parse(request.params.arguments); const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(
const wikiPages = await listWikiPages(project_id, { page, per_page }); request.params.arguments
);
const wikiPages = await listWikiPages(project_id, { page, per_page, with_content });
return { return {
content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }], content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
}; };
@ -3409,6 +3733,45 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
}; };
} }
case "create_pipeline": {
const { project_id, ref, variables } = CreatePipelineSchema.parse(request.params.arguments);
const pipeline = await createPipeline(project_id, ref, variables);
return {
content: [
{
type: "text",
text: `Created pipeline #${pipeline.id} for ${ref}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
},
],
};
}
case "retry_pipeline": {
const { project_id, pipeline_id } = RetryPipelineSchema.parse(request.params.arguments);
const pipeline = await retryPipeline(project_id, pipeline_id);
return {
content: [
{
type: "text",
text: `Retried pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
},
],
};
}
case "cancel_pipeline": {
const { project_id, pipeline_id } = CancelPipelineSchema.parse(request.params.arguments);
const pipeline = await cancelPipeline(project_id, pipeline_id);
return {
content: [
{
type: "text",
text: `Canceled pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
},
],
};
}
case "list_merge_requests": { case "list_merge_requests": {
const args = ListMergeRequestsSchema.parse(request.params.arguments); const args = ListMergeRequestsSchema.parse(request.params.arguments);
const mergeRequests = await listMergeRequests(args.project_id, args); const mergeRequests = await listMergeRequests(args.project_id, args);
@ -3584,9 +3947,37 @@ async function runServer() {
console.error(`GitLab MCP Server v${SERVER_VERSION}`); console.error(`GitLab MCP Server v${SERVER_VERSION}`);
console.error(`API URL: ${GITLAB_API_URL}`); console.error(`API URL: ${GITLAB_API_URL}`);
console.error("========================"); console.error("========================");
if ( !SSE )
const transport = new StdioServerTransport(); {
await server.connect(transport); const transport = new StdioServerTransport();
await server.connect(transport);
} else {
const app = express();
const transports: { [sessionId: string]: SSEServerTransport } = {};
app.get("/sse", async (_: Request, res: Response) => {
const transport = new SSEServerTransport("/messages", res);
transports[transport.sessionId] = transport;
res.on("close", () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
app.post("/messages", async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).send("No transport found for sessionId");
}
});
const PORT = process.env.PORT || 3002;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
}
console.error("GitLab MCP Server running on stdio"); console.error("GitLab MCP Server running on stdio");
} catch (error) { } catch (error) {
console.error("Error initializing server:", error); console.error("Error initializing server:", error);

290
package-lock.json generated
View File

@ -1,16 +1,17 @@
{ {
"name": "@zereight/mcp-gitlab", "name": "@zereight/mcp-gitlab",
"version": "1.0.50", "version": "1.0.56",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@zereight/mcp-gitlab", "name": "@zereight/mcp-gitlab",
"version": "1.0.50", "version": "1.0.56",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "1.8.0", "@modelcontextprotocol/sdk": "1.8.0",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"express": "^5.1.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"http-proxy-agent": "^7.0.2", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
@ -22,6 +23,7 @@
"mcp-gitlab": "build/index.js" "mcp-gitlab": "build/index.js"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.2",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@typescript-eslint/eslint-plugin": "^8.21.0", "@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0", "@typescript-eslint/parser": "^8.21.0",
@ -415,6 +417,27 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -422,6 +445,38 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/express": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz",
"integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -429,6 +484,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.13.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
@ -448,6 +510,43 @@
"form-data": "^4.0.0" "form-data": "^4.0.0"
} }
}, },
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.0", "version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
@ -817,44 +916,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/body-parser/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/body-parser/node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1065,12 +1126,12 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.6", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "^2.1.3"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -1455,46 +1516,45 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "5.0.1", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.0.1", "body-parser": "^2.2.0",
"content-disposition": "^1.0.0", "content-disposition": "^1.0.0",
"content-type": "~1.0.4", "content-type": "^1.0.5",
"cookie": "0.7.1", "cookie": "^0.7.1",
"cookie-signature": "^1.2.1", "cookie-signature": "^1.2.1",
"debug": "4.3.6", "debug": "^4.4.0",
"depd": "2.0.0", "encodeurl": "^2.0.0",
"encodeurl": "~2.0.0", "escape-html": "^1.0.3",
"escape-html": "~1.0.3", "etag": "^1.8.1",
"etag": "~1.8.1", "finalhandler": "^2.1.0",
"finalhandler": "^2.0.0", "fresh": "^2.0.0",
"fresh": "2.0.0", "http-errors": "^2.0.0",
"http-errors": "2.0.0",
"merge-descriptors": "^2.0.0", "merge-descriptors": "^2.0.0",
"methods": "~1.1.2",
"mime-types": "^3.0.0", "mime-types": "^3.0.0",
"on-finished": "2.4.1", "on-finished": "^2.4.1",
"once": "1.4.0", "once": "^1.4.0",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"proxy-addr": "~2.0.7", "proxy-addr": "^2.0.7",
"qs": "6.13.0", "qs": "^6.14.0",
"range-parser": "~1.2.1", "range-parser": "^1.2.1",
"router": "^2.0.0", "router": "^2.2.0",
"safe-buffer": "5.2.1",
"send": "^1.1.0", "send": "^1.1.0",
"serve-static": "^2.1.0", "serve-static": "^2.2.0",
"setprototypeof": "1.2.0", "statuses": "^2.0.1",
"statuses": "2.0.1", "type-is": "^2.0.1",
"type-is": "^2.0.0", "vary": "^1.1.2"
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": { "node_modules/express-rate-limit": {
@ -1639,29 +1699,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/finalhandler/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -2204,15 +2241,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -2265,9 +2293,9 @@
} }
}, },
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/natural-compare": { "node_modules/natural-compare": {
@ -2537,12 +2565,12 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -2633,29 +2661,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/router/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/router/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -2741,12 +2746,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@ -3085,15 +3084,6 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/v8-compile-cache-lib": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "@zereight/mcp-gitlab", "name": "@zereight/mcp-gitlab",
"version": "1.0.50", "version": "1.0.59",
"description": "MCP server for using the GitLab API", "description": "MCP server for using the GitLab API",
"license": "MIT", "license": "MIT",
"author": "zereight", "author": "zereight",
@ -30,8 +30,9 @@
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "1.8.0", "@modelcontextprotocol/sdk": "1.8.0",
"form-data": "^4.0.0",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"express": "^5.1.0",
"form-data": "^4.0.0",
"http-proxy-agent": "^7.0.2", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
@ -39,13 +40,14 @@
"zod-to-json-schema": "^3.23.5" "zod-to-json-schema": "^3.23.5"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.2",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"typescript": "^5.8.2",
"zod": "^3.24.2",
"@typescript-eslint/eslint-plugin": "^8.21.0", "@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0", "@typescript-eslint/parser": "^8.21.0",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"ts-node": "^10.9.2" "ts-node": "^10.9.2",
"typescript": "^5.8.2",
"zod": "^3.24.2"
} }
} }

View File

@ -1,5 +0,0 @@
### 1.0.40 (2025-05-21)
- Added support for listing discussions (comments/notes) on GitLab issues.
- Example: You can now easily fetch all conversations (comments) attached to an issue via the API.
- Related PR: [#44](https://github.com/zereight/gitlab-mcp/pull/44)

View File

@ -94,6 +94,13 @@ export const GitLabPipelineJobSchema = z.object({
web_url: z.string().optional(), web_url: z.string().optional(),
}); });
// Shared base schema for various pagination options
// See https://docs.gitlab.com/api/rest/#pagination
export const PaginationOptionsSchema = z.object({
page: z.number().optional().describe("Page number for pagination (default: 1)"),
per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
});
// Schema for listing pipelines // Schema for listing pipelines
export const ListPipelinesSchema = z.object({ export const ListPipelinesSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
@ -134,9 +141,7 @@ export const ListPipelinesSchema = z.object({
.optional() .optional()
.describe("Order pipelines by"), .describe("Order pipelines by"),
sort: z.enum(["asc", "desc"]).optional().describe("Sort pipelines"), sort: z.enum(["asc", "desc"]).optional().describe("Sort pipelines"),
page: z.number().optional().describe("Page number for pagination"), }).merge(PaginationOptionsSchema);
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Schema for getting a specific pipeline // Schema for getting a specific pipeline
export const GetPipelineSchema = z.object({ export const GetPipelineSchema = z.object({
@ -153,8 +158,33 @@ export const ListPipelineJobsSchema = z.object({
.optional() .optional()
.describe("The scope of jobs to show"), .describe("The scope of jobs to show"),
include_retried: z.boolean().optional().describe("Whether to include retried jobs"), include_retried: z.boolean().optional().describe("Whether to include retried jobs"),
page: z.number().optional().describe("Page number for pagination"), }).merge(PaginationOptionsSchema);
per_page: z.number().optional().describe("Number of items per page (max 100)"),
// Schema for creating a new pipeline
export const CreatePipelineSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
ref: z.string().describe("The branch or tag to run the pipeline on"),
variables: z
.array(
z.object({
key: z.string().describe("The key of the variable"),
value: z.string().describe("The value of the variable"),
})
)
.optional()
.describe("An array of variables to use for the pipeline"),
});
// Schema for retrying a pipeline
export const RetryPipelineSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
pipeline_id: z.number().describe("The ID of the pipeline to retry"),
});
// Schema for canceling a pipeline
export const CancelPipelineSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
pipeline_id: z.number().describe("The ID of the pipeline to cancel"),
}); });
// Schema for the input parameters for pipeline job operations // Schema for the input parameters for pipeline job operations
@ -163,11 +193,35 @@ export const GetPipelineJobOutputSchema = z.object({
job_id: z.number().describe("The ID of the job"), job_id: z.number().describe("The ID of the job"),
}); });
// User schemas
export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
name: z.string(),
avatar_url: z.string().nullable(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GetUsersSchema = z.object({
usernames: z.array(z.string()).describe("Array of usernames to search for"),
});
export const GitLabUsersResponseSchema = z.record(
z.string(),
z.object({
id: z.number(),
username: z.string(),
name: z.string(),
avatar_url: z.string().nullable(),
web_url: z.string(),
}).nullable()
);
// Namespace related schemas // Namespace related schemas
// Base schema for project-related operations // Base schema for project-related operations
const ProjectParamsSchema = z.object({ const ProjectParamsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API project_id: z.string().describe("Project ID or complete URL-encoded path to project"), // Changed from owner/repo to match GitLab API
}); });
export const GitLabNamespaceSchema = z.object({ export const GitLabNamespaceSchema = z.object({
id: z.number(), id: z.number(),
@ -199,7 +253,7 @@ export const GitLabNamespaceExistsResponseSchema = z.object({
export const GitLabOwnerSchema = z.object({ export const GitLabOwnerSchema = z.object({
username: z.string(), // Changed from login to match GitLab API username: z.string(), // Changed from login to match GitLab API
id: z.number(), id: z.number(),
avatar_url: z.string(), avatar_url: z.string().nullable(),
web_url: z.string(), // Changed from html_url to match GitLab API web_url: z.string(), // Changed from html_url to match GitLab API
name: z.string(), // Added as GitLab includes full name name: z.string(), // Added as GitLab includes full name
state: z.string(), // Added as GitLab includes user state state: z.string(), // Added as GitLab includes user state
@ -259,6 +313,7 @@ export const GitLabRepositorySchema = z.object({
container_registry_access_level: z.string().optional(), container_registry_access_level: z.string().optional(),
issues_enabled: z.boolean().optional(), issues_enabled: z.boolean().optional(),
merge_requests_enabled: z.boolean().optional(), merge_requests_enabled: z.boolean().optional(),
merge_requests_template: z.string().nullable().optional(),
wiki_enabled: z.boolean().optional(), wiki_enabled: z.boolean().optional(),
jobs_enabled: z.boolean().optional(), jobs_enabled: z.boolean().optional(),
snippets_enabled: z.boolean().optional(), snippets_enabled: z.boolean().optional(),
@ -403,13 +458,26 @@ export const CreateMergeRequestOptionsSchema = z.object({
description: z.string().optional(), // Changed from body to match GitLab API description: z.string().optional(), // Changed from body to match GitLab API
source_branch: z.string(), // Changed from head to match GitLab API source_branch: z.string(), // Changed from head to match GitLab API
target_branch: z.string(), // Changed from base to match GitLab API target_branch: z.string(), // Changed from base to match GitLab API
assignee_ids: z
.array(z.number())
.optional(),
reviewer_ids: z
.array(z.number())
.optional(),
labels: z.array(z.string()).optional(),
allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
draft: z.boolean().optional(), draft: z.boolean().optional(),
}); });
export const CreateBranchOptionsSchema = z.object({ export const GitLabDiffSchema = z.object({
name: z.string(), // Changed from ref to match GitLab API old_path: z.string(),
ref: z.string(), // The source branch/commit for the new branch new_path: z.string(),
a_mode: z.string(),
b_mode: z.string(),
diff: z.string(),
new_file: z.boolean(),
renamed_file: z.boolean(),
deleted_file: z.boolean(),
}); });
// Response schemas for operations // Response schemas for operations
@ -427,6 +495,27 @@ export const GitLabSearchResponseSchema = z.object({
items: z.array(GitLabRepositorySchema), items: z.array(GitLabRepositorySchema),
}); });
// create branch schemas
export const CreateBranchOptionsSchema = z.object({
name: z.string(), // Changed from ref to match GitLab API
ref: z.string(), // The source branch/commit for the new branch
});
export const GitLabCompareResultSchema = z.object({
commit: z.object({
id: z.string().optional(),
short_id: z.string().optional(),
title: z.string().optional(),
author_name: z.string().optional(),
author_email: z.string().optional(),
created_at: z.string().optional(),
}).optional(),
commits: z.array(GitLabCommitSchema),
diffs: z.array(GitLabDiffSchema),
compare_timeout: z.boolean().optional(),
compare_same_ref: z.boolean().optional(),
});
// Issue related schemas // Issue related schemas
export const GitLabLabelSchema = z.object({ export const GitLabLabelSchema = z.object({
id: z.number(), id: z.number(),
@ -443,14 +532,6 @@ export const GitLabLabelSchema = z.object({
is_project_label: z.boolean().optional(), is_project_label: z.boolean().optional(),
}); });
export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
name: z.string(),
avatar_url: z.string().nullable(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabMilestoneSchema = z.object({ export const GitLabMilestoneSchema = z.object({
id: z.number(), id: z.number(),
iid: z.number(), // Added to match GitLab API iid: z.number(), // Added to match GitLab API
@ -512,7 +593,7 @@ export const GitLabForkParentSchema = z.object({
.object({ .object({
username: z.string(), // Changed from login to match GitLab API username: z.string(), // Changed from login to match GitLab API
id: z.number(), id: z.number(),
avatar_url: z.string(), avatar_url: z.string().nullable(),
}) })
.optional(), // Made optional to handle cases where GitLab API doesn't include it .optional(), // Made optional to handle cases where GitLab API doesn't include it
web_url: z.string(), // Changed from html_url to match GitLab API web_url: z.string(), // Changed from html_url to match GitLab API
@ -540,6 +621,7 @@ export const GitLabMergeRequestSchema = z.object({
draft: z.boolean().optional(), draft: z.boolean().optional(),
author: GitLabUserSchema, author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema).optional(), assignees: z.array(GitLabUserSchema).optional(),
reviewers: z.array(GitLabUserSchema).optional(),
source_branch: z.string(), source_branch: z.string(),
target_branch: z.string(), target_branch: z.string(),
diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(), diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(),
@ -591,21 +673,21 @@ export const GitLabDiscussionNoteSchema = z.object({
old_path: z.string(), old_path: z.string(),
new_path: z.string(), new_path: z.string(),
position_type: z.enum(["text", "image", "file"]), position_type: z.enum(["text", "image", "file"]),
old_line: z.number().nullable(), old_line: z.number().nullish(), // This is missing for image diffs
new_line: z.number().nullable(), new_line: z.number().nullish(), // This is missing for image diffs
line_range: z line_range: z
.object({ .object({
start: z.object({ start: z.object({
line_code: z.string(), line_code: z.string(),
type: z.enum(["new", "old", "expanded"]), type: z.enum(["new", "old", "expanded"]),
old_line: z.number().nullable(), old_line: z.number().nullish(), // This is missing for image diffs
new_line: z.number().nullable(), new_line: z.number().nullish(), // This is missing for image diffs
}), }),
end: z.object({ end: z.object({
line_code: z.string(), line_code: z.string(),
type: z.enum(["new", "old", "expanded"]), type: z.enum(["new", "old", "expanded"]),
old_line: z.number().nullable(), old_line: z.number().nullish(), // This is missing for image diffs
new_line: z.number().nullable(), new_line: z.number().nullish(), // This is missing for image diffs
}), }),
}) })
.nullable() .nullable()
@ -619,6 +701,24 @@ export const GitLabDiscussionNoteSchema = z.object({
}); });
export type GitLabDiscussionNote = z.infer<typeof GitLabDiscussionNoteSchema>; export type GitLabDiscussionNote = z.infer<typeof GitLabDiscussionNoteSchema>;
// Reusable pagination schema for GitLab API responses.
// See https://docs.gitlab.com/api/rest/#pagination
export const GitLabPaginationSchema = z.object({
x_next_page: z.number().nullable().optional(),
x_page: z.number().optional(),
x_per_page: z.number().optional(),
x_prev_page: z.number().nullable().optional(),
x_total: z.number().nullable().optional(),
x_total_pages: z.number().nullable().optional(),
});
export type GitLabPagination = z.infer<typeof GitLabPaginationSchema>;
// Base paginated response schema that can be extended.
// See https://docs.gitlab.com/api/rest/#pagination
export const PaginatedResponseSchema = z.object({
pagination: GitLabPaginationSchema.optional(),
});
export const GitLabDiscussionSchema = z.object({ export const GitLabDiscussionSchema = z.object({
id: z.string(), id: z.string(),
individual_note: z.boolean(), individual_note: z.boolean(),
@ -626,10 +726,24 @@ export const GitLabDiscussionSchema = z.object({
}); });
export type GitLabDiscussion = z.infer<typeof GitLabDiscussionSchema>; export type GitLabDiscussion = z.infer<typeof GitLabDiscussionSchema>;
// Create a schema for paginated discussions response
export const PaginatedDiscussionsResponseSchema = z.object({
items: z.array(GitLabDiscussionSchema),
pagination: GitLabPaginationSchema,
});
// Export the paginated response type for discussions
export type PaginatedDiscussionsResponse = z.infer<typeof PaginatedDiscussionsResponseSchema>;
export const ListIssueDiscussionsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
issue_iid: z.number().describe("The internal ID of the project issue"),
}).merge(PaginationOptionsSchema);
// Input schema for listing merge request discussions // Input schema for listing merge request discussions
export const ListMergeRequestDiscussionsSchema = ProjectParamsSchema.extend({ export const ListMergeRequestDiscussionsSchema = ProjectParamsSchema.extend({
merge_request_iid: z.number().describe("The IID of a merge request"), merge_request_iid: z.number().describe("The IID of a merge request"),
}); }).merge(PaginationOptionsSchema);
// Input schema for updating a merge request discussion note // Input schema for updating a merge request discussion note
export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({ export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({
@ -684,9 +798,7 @@ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
export const SearchRepositoriesSchema = z.object({ export const SearchRepositoriesSchema = z.object({
search: z.string().describe("Search query"), // Changed from query to match GitLab API search: z.string().describe("Search query"), // Changed from query to match GitLab API
page: z.number().optional().describe("Page number for pagination (default: 1)"), }).merge(PaginationOptionsSchema);
per_page: z.number().optional().describe("Number of results per page (default: 20)"),
});
export const CreateRepositorySchema = z.object({ export const CreateRepositorySchema = z.object({
name: z.string().describe("Repository name"), name: z.string().describe("Repository name"),
@ -729,28 +841,39 @@ export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
description: z.string().optional().describe("Merge request description"), description: z.string().optional().describe("Merge request description"),
source_branch: z.string().describe("Branch containing changes"), source_branch: z.string().describe("Branch containing changes"),
target_branch: z.string().describe("Branch to merge into"), target_branch: z.string().describe("Branch to merge into"),
assignee_ids: z
.array(z.number())
.optional()
.describe("The ID of the users to assign the MR to"),
reviewer_ids: z
.array(z.number())
.optional()
.describe("The ID of the users to assign as reviewers of the MR"),
labels: z.array(z.string()).optional().describe("Labels for the MR"),
draft: z.boolean().optional().describe("Create as draft merge request"), draft: z.boolean().optional().describe("Create as draft merge request"),
allow_collaboration: z.boolean().optional().describe("Allow commits from upstream members"), allow_collaboration: z
.boolean()
.optional()
.describe("Allow commits from upstream members"),
}); });
export const ForkRepositorySchema = ProjectParamsSchema.extend({ export const ForkRepositorySchema = ProjectParamsSchema.extend({
namespace: z.string().optional().describe("Namespace to fork to (full path)"), namespace: z.string().optional().describe("Namespace to fork to (full path)"),
}); });
// Branch related schemas
export const CreateBranchSchema = ProjectParamsSchema.extend({ export const CreateBranchSchema = ProjectParamsSchema.extend({
branch: z.string().describe("Name for the new branch"), branch: z.string().describe("Name for the new branch"),
ref: z.string().optional().describe("Source branch/commit for new branch"), ref: z.string().optional().describe("Source branch/commit for new branch"),
}); });
export const GitLabMergeRequestDiffSchema = z.object({ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
old_path: z.string(), from: z.string().describe("The base branch or commit SHA to compare from"),
new_path: z.string(), to: z.string().describe("The target branch or commit SHA to compare to"),
a_mode: z.string(), straight: z.boolean().optional().describe("Comparison method: false for '...' (default), true for '--'"),
b_mode: z.string(), excluded_file_patterns: z.array(z.string()).optional().describe(
diff: z.string(), "Array of regex patterns to exclude files from the diff results. Each pattern is a JavaScript-compatible regular expression that matches file paths to ignore. Examples: [\"^test/mocks/\", \"\\.spec\\.ts$\", \"package-lock\\.json\"]"
new_file: z.boolean(), ),
renamed_file: z.boolean(),
deleted_file: z.boolean(),
}); });
export const GetMergeRequestSchema = ProjectParamsSchema.extend({ export const GetMergeRequestSchema = ProjectParamsSchema.extend({
@ -793,17 +916,17 @@ export const CreateNoteSchema = z.object({
export const ListIssuesSchema = z.object({ export const ListIssuesSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"), assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"),
assignee_username: z.string().optional().describe("Return issues assigned to the given username"), assignee_username: z.array(z.string()).optional().describe("Return issues assigned to the given username"),
author_id: z.number().optional().describe("Return issues created by the given user ID"), author_id: z.number().optional().describe("Return issues created by the given user ID"),
author_username: z.string().optional().describe("Return issues created by the given username"), author_username: z.string().optional().describe("Return issues created by the given username"),
confidential: z.boolean().optional().describe("Filter confidential or public issues"), confidential: z.boolean().optional().describe("Filter confidential or public issues"),
created_after: z.string().optional().describe("Return issues created after the given time"), created_after: z.string().optional().describe("Return issues created after the given time"),
created_before: z.string().optional().describe("Return issues created before the given time"), created_before: z.string().optional().describe("Return issues created before the given time"),
due_date: z.string().optional().describe("Return issues that have the due date"), due_date: z.string().optional().describe("Return issues that have the due date"),
label_name: z.array(z.string()).optional().describe("Array of label names"), labels: z.array(z.string()).optional().describe("Array of label names"),
milestone: z.string().optional().describe("Milestone title"), milestone: z.string().optional().describe("Milestone title"),
scope: z scope: z
.enum(["created-by-me", "assigned-to-me", "all"]) .enum(["created_by_me", "assigned_to_me", "all"])
.optional() .optional()
.describe("Return issues from a specific scope"), .describe("Return issues from a specific scope"),
search: z.string().optional().describe("Search for specific terms"), search: z.string().optional().describe("Search for specific terms"),
@ -814,9 +937,7 @@ export const ListIssuesSchema = z.object({
updated_after: z.string().optional().describe("Return issues updated after the given time"), updated_after: z.string().optional().describe("Return issues updated after the given time"),
updated_before: z.string().optional().describe("Return issues updated before the given time"), updated_before: z.string().optional().describe("Return issues updated before the given time"),
with_labels_details: z.boolean().optional().describe("Return more details for each label"), with_labels_details: z.boolean().optional().describe("Return more details for each label"),
page: z.number().optional().describe("Page number for pagination"), }).merge(PaginationOptionsSchema);
per_page: z.number().optional().describe("Number of items per page"),
});
// Merge Requests API operation schemas // Merge Requests API operation schemas
export const ListMergeRequestsSchema = z.object({ export const ListMergeRequestsSchema = z.object({
@ -887,9 +1008,7 @@ export const ListMergeRequestsSchema = z.object({
.describe("Return merge requests from a specific source branch"), .describe("Return merge requests from a specific source branch"),
wip: z.enum(["yes", "no"]).optional().describe("Filter merge requests against their wip status"), wip: z.enum(["yes", "no"]).optional().describe("Filter merge requests against their wip status"),
with_labels_details: z.boolean().optional().describe("Return more details for each label"), with_labels_details: z.boolean().optional().describe("Return more details for each label"),
page: z.number().optional().describe("Page number for pagination"), }).merge(PaginationOptionsSchema);
per_page: z.number().optional().describe("Number of items per page"),
});
export const GetIssueSchema = z.object({ export const GetIssueSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
@ -928,21 +1047,6 @@ export const ListIssueLinksSchema = z.object({
issue_iid: z.number().describe("The internal ID of a project's issue"), issue_iid: z.number().describe("The internal ID of a project's issue"),
}); });
export const ListIssueDiscussionsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
issue_iid: z.number().describe("The internal ID of the project issue"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page"),
sort: z
.enum(["asc", "desc"])
.optional()
.describe("Return issue discussions sorted in ascending or descending order"),
order_by: z
.enum(["created_at", "updated_at"])
.optional()
.describe("Return issue discussions ordered by created_at or updated_at fields"),
});
export const GetIssueLinkSchema = z.object({ export const GetIssueLinkSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
issue_iid: z.number().describe("The internal ID of a project's issue"), issue_iid: z.number().describe("The internal ID of a project's issue"),
@ -969,10 +1073,8 @@ export const DeleteIssueLinkSchema = z.object({
// Namespace API operation schemas // Namespace API operation schemas
export const ListNamespacesSchema = z.object({ export const ListNamespacesSchema = z.object({
search: z.string().optional().describe("Search term for namespaces"), search: z.string().optional().describe("Search term for namespaces"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page"),
owned: z.boolean().optional().describe("Filter for namespaces owned by current user"), owned: z.boolean().optional().describe("Filter for namespaces owned by current user"),
}); }).merge(PaginationOptionsSchema);
export const GetNamespaceSchema = z.object({ export const GetNamespaceSchema = z.object({
namespace_id: z.string().describe("Namespace ID or full path"), namespace_id: z.string().describe("Namespace ID or full path"),
@ -989,8 +1091,6 @@ export const GetProjectSchema = z.object({
export const ListProjectsSchema = z.object({ export const ListProjectsSchema = z.object({
search: z.string().optional().describe("Search term for projects"), search: z.string().optional().describe("Search term for projects"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page"),
search_namespaces: z.boolean().optional().describe("Needs to be true if search is full path"), search_namespaces: z.boolean().optional().describe("Needs to be true if search is full path"),
owned: z.boolean().optional().describe("Filter for projects owned by current user"), owned: z.boolean().optional().describe("Filter for projects owned by current user"),
membership: z.boolean().optional().describe("Filter for projects where current user is a member"), membership: z.boolean().optional().describe("Filter for projects where current user is a member"),
@ -1017,7 +1117,7 @@ export const ListProjectsSchema = z.object({
.optional() .optional()
.describe("Filter projects with merge requests feature enabled"), .describe("Filter projects with merge requests feature enabled"),
min_access_level: z.number().optional().describe("Filter by minimum access level"), min_access_level: z.number().optional().describe("Filter by minimum access level"),
}); }).merge(PaginationOptionsSchema);
// Label operation schemas // Label operation schemas
export const ListLabelsSchema = z.object({ export const ListLabelsSchema = z.object({
@ -1073,8 +1173,6 @@ export const ListGroupProjectsSchema = z.object({
.optional() .optional()
.describe("Field to sort by"), .describe("Field to sort by"),
sort: z.enum(["asc", "desc"]).optional().describe("Sort direction"), sort: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
page: z.number().optional().describe("Page number"),
per_page: z.number().optional().describe("Number of results per page"),
archived: z.boolean().optional().describe("Filter for archived projects"), archived: z.boolean().optional().describe("Filter for archived projects"),
visibility: z visibility: z
.enum(["public", "internal", "private"]) .enum(["public", "internal", "private"])
@ -1094,14 +1192,14 @@ export const ListGroupProjectsSchema = z.object({
statistics: z.boolean().optional().describe("Include project statistics"), statistics: z.boolean().optional().describe("Include project statistics"),
with_custom_attributes: z.boolean().optional().describe("Include custom attributes"), with_custom_attributes: z.boolean().optional().describe("Include custom attributes"),
with_security_reports: z.boolean().optional().describe("Include security reports"), with_security_reports: z.boolean().optional().describe("Include security reports"),
}); }).merge(PaginationOptionsSchema);
// Add wiki operation schemas // Add wiki operation schemas
export const ListWikiPagesSchema = z.object({ export const ListWikiPagesSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
page: z.number().optional().describe("Page number for pagination"), with_content: z.boolean().optional().describe("Include content of the wiki pages"),
per_page: z.number().optional().describe("Number of items per page"), }).merge(PaginationOptionsSchema);
});
export const GetWikiPageSchema = z.object({ export const GetWikiPageSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
slug: z.string().describe("URL-encoded slug of the wiki page"), slug: z.string().describe("URL-encoded slug of the wiki page"),
@ -1119,6 +1217,7 @@ export const UpdateWikiPageSchema = z.object({
content: z.string().optional().describe("New content of the wiki page"), content: z.string().optional().describe("New content of the wiki page"),
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"), format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
}); });
export const DeleteWikiPageSchema = z.object({ export const DeleteWikiPageSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
slug: z.string().describe("URL-encoded slug of the wiki page"), slug: z.string().describe("URL-encoded slug of the wiki page"),
@ -1129,7 +1228,7 @@ export const GitLabWikiPageSchema = z.object({
title: z.string(), title: z.string(),
slug: z.string(), slug: z.string(),
format: z.string(), format: z.string(),
content: z.string(), content: z.string().optional(),
created_at: z.string().optional(), created_at: z.string().optional(),
updated_at: z.string().optional(), updated_at: z.string().optional(),
}); });
@ -1185,9 +1284,7 @@ export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
.string() .string()
.optional() .optional()
.describe("Return milestones updated after the specified date (ISO 8601 format)"), .describe("Return milestones updated after the specified date (ISO 8601 format)"),
page: z.number().optional().describe("Page number for pagination"), }).merge(PaginationOptionsSchema);
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Schema for getting a single milestone // Schema for getting a single milestone
export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({ export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({
@ -1221,19 +1318,13 @@ export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema;
export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema; export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema;
// Schema for getting merge requests assigned to a milestone // Schema for getting merge requests assigned to a milestone
export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.extend({ export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Schema for promoting a project milestone to a group milestone // Schema for promoting a project milestone to a group milestone
export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema; export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
// Schema for getting burndown chart events for a milestone // Schema for getting burndown chart events for a milestone
export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.extend({ export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Export types // Export types
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>; export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
@ -1247,6 +1338,7 @@ export type GitLabDirectoryContent = z.infer<typeof GitLabDirectoryContentSchema
export type GitLabContent = z.infer<typeof GitLabContentSchema>; export type GitLabContent = z.infer<typeof GitLabContentSchema>;
export type FileOperation = z.infer<typeof FileOperationSchema>; export type FileOperation = z.infer<typeof FileOperationSchema>;
export type GitLabTree = z.infer<typeof GitLabTreeSchema>; export type GitLabTree = z.infer<typeof GitLabTreeSchema>;
export type GitLabCompareResult = z.infer<typeof GitLabCompareResultSchema>;
export type GitLabCommit = z.infer<typeof GitLabCommitSchema>; export type GitLabCommit = z.infer<typeof GitLabCommitSchema>;
export type GitLabReference = z.infer<typeof GitLabReferenceSchema>; export type GitLabReference = z.infer<typeof GitLabReferenceSchema>;
export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>; export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>;
@ -1255,10 +1347,13 @@ export type CreateMergeRequestOptions = z.infer<typeof CreateMergeRequestOptions
export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>; export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>;
export type GitLabCreateUpdateFileResponse = z.infer<typeof GitLabCreateUpdateFileResponseSchema>; export type GitLabCreateUpdateFileResponse = z.infer<typeof GitLabCreateUpdateFileResponseSchema>;
export type GitLabSearchResponse = z.infer<typeof GitLabSearchResponseSchema>; export type GitLabSearchResponse = z.infer<typeof GitLabSearchResponseSchema>;
export type GitLabMergeRequestDiff = z.infer<typeof GitLabMergeRequestDiffSchema>; export type GitLabMergeRequestDiff = z.infer<
typeof GitLabDiffSchema
>;
export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>; export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>;
export type GitLabIssueLink = z.infer<typeof GitLabIssueLinkSchema>; export type GitLabIssueLink = z.infer<typeof GitLabIssueLinkSchema>;
export type ListIssueDiscussionsOptions = z.infer<typeof ListIssueDiscussionsSchema>; export type ListIssueDiscussionsOptions = z.infer<typeof ListIssueDiscussionsSchema>;
export type ListMergeRequestDiscussionsOptions = z.infer<typeof ListMergeRequestDiscussionsSchema>;
export type UpdateIssueNoteOptions = z.infer<typeof UpdateIssueNoteSchema>; export type UpdateIssueNoteOptions = z.infer<typeof UpdateIssueNoteSchema>;
export type CreateIssueNoteOptions = z.infer<typeof CreateIssueNoteSchema>; export type CreateIssueNoteOptions = z.infer<typeof CreateIssueNoteSchema>;
export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>; export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>;
@ -1281,6 +1376,9 @@ export type GitLabPipeline = z.infer<typeof GitLabPipelineSchema>;
export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>; export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>;
export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>; export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>; export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>;
export type CreatePipelineOptions = z.infer<typeof CreatePipelineSchema>;
export type RetryPipelineOptions = z.infer<typeof RetryPipelineSchema>;
export type CancelPipelineOptions = z.infer<typeof CancelPipelineSchema>;
export type GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>; export type GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>;
export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>; export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>;
export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>; export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>;
@ -1291,3 +1389,6 @@ export type GetMilestoneIssuesOptions = z.infer<typeof GetMilestoneIssuesSchema>
export type GetMilestoneMergeRequestsOptions = z.infer<typeof GetMilestoneMergeRequestsSchema>; export type GetMilestoneMergeRequestsOptions = z.infer<typeof GetMilestoneMergeRequestsSchema>;
export type PromoteProjectMilestoneOptions = z.infer<typeof PromoteProjectMilestoneSchema>; export type PromoteProjectMilestoneOptions = z.infer<typeof PromoteProjectMilestoneSchema>;
export type GetMilestoneBurndownEventsOptions = z.infer<typeof GetMilestoneBurndownEventsSchema>; export type GetMilestoneBurndownEventsOptions = z.infer<typeof GetMilestoneBurndownEventsSchema>;
export type GitLabUser = z.infer<typeof GitLabUserSchema>;
export type GitLabUsersResponse = z.infer<typeof GitLabUsersResponseSchema>;
export type PaginationOptions = z.infer<typeof PaginationOptionsSchema>;

View File

@ -10,9 +10,9 @@ IMAGE_NAME=gitlab-mcp
IMAGE_VERSION=$(jq -r '.version' package.json) IMAGE_VERSION=$(jq -r '.version' package.json)
echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}"
docker build --platform=linux/arm64 -t "${DOCKER_USER}/${IMAGE_NAME}:latest" .
docker tag "${DOCKER_USER}/${IMAGE_NAME}:latest" "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" docker buildx build --platform linux/arm64,linux/amd64 \
-t "${DOCKER_USER}/${IMAGE_NAME}:latest" \
docker push "${DOCKER_USER}/${IMAGE_NAME}:latest" -t "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" \
docker push "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" --push \
.

View File

@ -43,9 +43,15 @@ async function validateGitLabAPI() {
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`, url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`,
validate: data => Array.isArray(data), validate: data => Array.isArray(data),
}, },
{
name: "List pipelines",
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`,
validate: data => Array.isArray(data),
},
]; ];
let allPassed = true; let allPassed = true;
let firstPipelineId = null;
for (const test of tests) { for (const test of tests) {
try { try {
@ -65,6 +71,11 @@ async function validateGitLabAPI() {
if (test.validate(data)) { if (test.validate(data)) {
console.log(`${test.name} - PASSED\n`); console.log(`${test.name} - PASSED\n`);
// If we found pipelines, save the first one for additional testing
if (test.name === "List pipelines" && data.length > 0) {
firstPipelineId = data[0].id;
}
} else { } else {
console.log(`${test.name} - FAILED (invalid response format)\n`); console.log(`${test.name} - FAILED (invalid response format)\n`);
allPassed = false; allPassed = false;
@ -76,6 +87,53 @@ async function validateGitLabAPI() {
} }
} }
// Test pipeline-specific endpoints if we have a pipeline ID
if (firstPipelineId) {
console.log(`Found pipeline #${firstPipelineId}, testing pipeline-specific endpoints...\n`);
const pipelineTests = [
{
name: `Get pipeline #${firstPipelineId} details`,
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}`,
validate: data => data.id === firstPipelineId && data.status,
},
{
name: `List pipeline #${firstPipelineId} jobs`,
url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/jobs`,
validate: data => Array.isArray(data),
},
];
for (const test of pipelineTests) {
try {
console.log(`Testing: ${test.name}`);
const response = await fetch(test.url, {
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (test.validate(data)) {
console.log(`${test.name} - PASSED\n`);
} else {
console.log(`${test.name} - FAILED (invalid response format)\n`);
allPassed = false;
}
} catch (error) {
console.log(`${test.name} - FAILED`);
console.log(` Error: ${error.message}\n`);
allPassed = false;
}
}
}
if (allPassed) { if (allPassed) {
console.log("✅ All API validation tests passed!"); console.log("✅ All API validation tests passed!");
} else { } else {