This is page 1 of 2. Use http://codebase.md/harshmaur/gitlab-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .eslintrc.json
├── .github
│ ├── pr-validation-guide.md
│ └── workflows
│ ├── auto-merge.yml
│ ├── docker-publish.yml
│ └── pr-test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .secrets
├── CHANGELOG.md
├── Dockerfile
├── docs
│ └── setup-github-secrets.md
├── event.json
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── schemas.ts
├── scripts
│ ├── generate-tools-readme.ts
│ ├── image_push.sh
│ └── validate-pr.sh
├── smithery.yaml
├── test
│ └── validate-api.js
├── test-note.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | coverage/
4 | *.log
5 | .DS_Store
6 | package-lock.json
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | .DS_Store
3 | build
4 | .env
5 | .env.local
6 | .env.test
7 | coverage/
8 | *.log
```
--------------------------------------------------------------------------------
/.secrets:
--------------------------------------------------------------------------------
```
1 | DOCKERHUB_USERNAME=DOCKERHUB_USERNAME
2 | DOCKERHUB_TOKEN=DOCKERHUB_TOKEN
3 | GITHUB_TOKEN=DOCKERHUB_TOKEN
4 |
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "semi": true,
3 | "trailingComma": "es5",
4 | "singleQuote": false,
5 | "printWidth": 100,
6 | "tabWidth": 2,
7 | "useTabs": false,
8 | "bracketSpacing": true,
9 | "arrowParens": "avoid",
10 | "endOfLine": "lf"
11 | }
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # GitLab API Configuration
2 | GITLAB_API_URL=https://gitlab.com
3 | GITLAB_TOKEN=your-gitlab-personal-access-token-here
4 |
5 | # Test Configuration (for integration tests)
6 | GITLAB_TOKEN_TEST=your-test-token-here
7 | TEST_PROJECT_ID=your-test-project-id
8 | ISSUE_IID=1
9 |
10 | # Proxy Configuration (optional)
11 | HTTP_PROXY=
12 | HTTPS_PROXY=
13 | NO_PROXY=localhost,127.0.0.1
```
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
4 | "plugins": ["@typescript-eslint"],
5 | "parserOptions": {
6 | "ecmaVersion": 2022,
7 | "sourceType": "module"
8 | },
9 | "env": {
10 | "node": true,
11 | "es2022": true,
12 | "jest": true
13 | },
14 | "rules": {
15 | "no-console": "warn",
16 | "prefer-const": "error",
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
19 | "@typescript-eslint/explicit-module-boundary-types": "off",
20 | "@typescript-eslint/no-explicit-any": "warn",
21 | "@typescript-eslint/no-non-null-assertion": "warn"
22 | },
23 | "ignorePatterns": ["node_modules/", "build/", "coverage/", "*.js"]
24 | }
25 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Better GitLab MCP Server
2 |
3 | ## @zereight/mcp-gitlab
4 |
5 | [](https://smithery.ai/server/@zereight/gitlab-mcp)
6 |
7 | GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements over the original GitLab MCP server.**
8 |
9 | <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>
10 |
11 | ## Usage
12 |
13 | ### Using with Claude App, Cline, Roo Code, Cursor
14 |
15 | When using with the Claude App, you need to set up your API key and URLs directly.
16 |
17 | #### npx
18 |
19 | ```json
20 | {
21 | "mcpServers": {
22 | "GitLab communication server": {
23 | "command": "npx",
24 | "args": ["-y", "@zereight/mcp-gitlab"],
25 | "env": {
26 | "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
27 | "GITLAB_API_URL": "your_gitlab_api_url",
28 | "GITLAB_READ_ONLY_MODE": "false",
29 | "USE_GITLAB_WIKI": "false", // use wiki api?
30 | "USE_MILESTONE": "false", // use milestone api?
31 | "USE_PIPELINE": "false" // use pipeline api?
32 | }
33 | }
34 | }
35 | }
36 | ```
37 |
38 | #### Docker
39 | - stdio
40 | ```mcp.json
41 | {
42 | "mcpServers": {
43 | "GitLab communication server": {
44 | "command": "docker",
45 | "args": [
46 | "run",
47 | "-i",
48 | "--rm",
49 | "-e",
50 | "GITLAB_PERSONAL_ACCESS_TOKEN",
51 | "-e",
52 | "GITLAB_API_URL",
53 | "-e",
54 | "GITLAB_READ_ONLY_MODE",
55 | "-e",
56 | "USE_GITLAB_WIKI",
57 | "-e",
58 | "USE_MILESTONE",
59 | "-e",
60 | "USE_PIPELINE",
61 | "iwakitakuma/gitlab-mcp"
62 | ],
63 | "env": {
64 | "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
65 | "GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab
66 | "GITLAB_READ_ONLY_MODE": "false",
67 | "USE_GITLAB_WIKI": "true",
68 | "USE_MILESTONE": "true",
69 | "USE_PIPELINE": "true"
70 | }
71 | }
72 | }
73 | }
74 | ```
75 |
76 | - sse
77 | ```shell
78 | docker run -i --rm \
79 | -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
80 | -e GITLAB_API_URL= "https://gitlab.com/api/v4"\
81 | -e GITLAB_READ_ONLY_MODE=true \
82 | -e USE_GITLAB_WIKI=true \
83 | -e USE_MILESTONE=true \
84 | -e USE_PIPELINE=true \
85 | -e SSE=true \
86 | -p 3333:3002 \
87 | iwakitakuma/gitlab-mcp
88 | ```
89 |
90 | ```json
91 | {
92 | "mcpServers": {
93 | "GitLab communication server": {
94 | "url": "http://localhost:3333/sse"
95 | }
96 | }
97 | }
98 | ```
99 |
100 | #### Docker Image Push
101 |
102 | ```shell
103 | $ sh scripts/image_push.sh docker_user_name
104 | ```
105 |
106 | ### Environment Variables
107 |
108 | - `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token.
109 | - `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`)
110 | - `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.
111 | - `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.
112 | - `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.
113 | - `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.
114 |
115 | ## Tools 🛠️
116 |
117 | +<!-- TOOLS-START -->
118 | 1. `create_or_update_file` - Create or update a single file in a GitLab project
119 | 2. `search_repositories` - Search for GitLab projects
120 | 3. `create_repository` - Create a new GitLab project
121 | 4. `get_file_contents` - Get the contents of a file or directory from a GitLab project
122 | 5. `push_files` - Push multiple files to a GitLab project in a single commit
123 | 6. `create_issue` - Create a new issue in a GitLab project
124 | 7. `create_merge_request` - Create a new merge request in a GitLab project
125 | 8. `fork_repository` - Fork a GitLab project to your account or specified namespace
126 | 9. `create_branch` - Create a new branch in a GitLab project
127 | 10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided)
128 | 11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
129 | 12. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
130 | 13. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
131 | 14. `create_note` - Create a new note (comment) to an issue or merge request
132 | 15. `create_merge_request_thread` - Create a new thread on a merge request
133 | 16. `mr_discussions` - List discussion items for a merge request
134 | 17. `update_merge_request_note` - Modify an existing merge request thread note
135 | 18. `create_merge_request_note` - Add a new note to an existing merge request thread
136 | 19. `update_issue_note` - Modify an existing issue thread note
137 | 20. `create_issue_note` - Add a new note to an existing issue thread
138 | 21. `list_issues` - List issues in a GitLab project with filtering options
139 | 22. `get_issue` - Get details of a specific issue in a GitLab project
140 | 23. `update_issue` - Update an issue in a GitLab project
141 | 24. `delete_issue` - Delete an issue from a GitLab project
142 | 25. `list_issue_links` - List all issue links for a specific issue
143 | 26. `list_issue_discussions` - List discussions for an issue in a GitLab project
144 | 27. `get_issue_link` - Get a specific issue link
145 | 28. `create_issue_link` - Create an issue link between two issues
146 | 29. `delete_issue_link` - Delete an issue link
147 | 30. `list_namespaces` - List all namespaces available to the current user
148 | 31. `get_namespace` - Get details of a namespace by ID or path
149 | 32. `verify_namespace` - Verify if a namespace path exists
150 | 33. `get_project` - Get details of a specific project
151 | 34. `list_projects` - List projects accessible by the current user
152 | 35. `list_labels` - List labels for a project
153 | 36. `get_label` - Get a single label from a project
154 | 37. `create_label` - Create a new label in a project
155 | 38. `update_label` - Update an existing label in a project
156 | 39. `delete_label` - Delete a label from a project
157 | 40. `list_group_projects` - List projects in a GitLab group with filtering options
158 | 41. `list_wiki_pages` - List wiki pages in a GitLab project
159 | 42. `get_wiki_page` - Get details of a specific wiki page
160 | 43. `create_wiki_page` - Create a new wiki page in a GitLab project
161 | 44. `update_wiki_page` - Update an existing wiki page in a GitLab project
162 | 45. `delete_wiki_page` - Delete a wiki page from a GitLab project
163 | 46. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
164 | 47. `list_pipelines` - List pipelines in a GitLab project with filtering options
165 | 48. `get_pipeline` - Get details of a specific pipeline in a GitLab project
166 | 49. `list_pipeline_jobs` - List all jobs in a specific pipeline
167 | 50. `get_pipeline_job` - Get details of a GitLab pipeline job number
168 | 51. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
169 | 52. `create_pipeline` - Create a new pipeline for a branch or tag
170 | 53. `retry_pipeline` - Retry a failed or canceled pipeline
171 | 54. `cancel_pipeline` - Cancel a running pipeline
172 | 55. `list_merge_requests` - List merge requests in a GitLab project with filtering options
173 | 56. `list_milestones` - List milestones in a GitLab project with filtering options
174 | 57. `get_milestone` - Get details of a specific milestone
175 | 58. `create_milestone` - Create a new milestone in a GitLab project
176 | 59. `edit_milestone` - Edit an existing milestone in a GitLab project
177 | 60. `delete_milestone` - Delete a milestone from a GitLab project
178 | 61. `get_milestone_issue` - Get issues associated with a specific milestone
179 | 62. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
180 | 63. `promote_milestone` - Promote a milestone to the next stage
181 | 64. `get_milestone_burndown_events` - Get burndown events for a specific milestone
182 | 65. `get_users` - Get GitLab user details by usernames
183 | <!-- TOOLS-END -->
184 |
```
--------------------------------------------------------------------------------
/event.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "action": "published",
3 | "release": {
4 | "tag_name": "v1.0.53"
5 | }
6 | }
7 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["./**/*"],
14 | "exclude": ["node_modules", "build"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/scripts/image_push.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | if [ -z "$1" ]; then
4 | echo "Error: docker user name required."
5 | exit 1
6 | fi
7 |
8 | DOCKER_USER=$1
9 | IMAGE_NAME=gitlab-mcp
10 | IMAGE_VERSION=$(jq -r '.version' package.json)
11 |
12 | echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}"
13 |
14 | docker buildx build --platform linux/arm64,linux/amd64 \
15 | -t "${DOCKER_USER}/${IMAGE_NAME}:latest" \
16 | -t "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" \
17 | --push \
18 | .
19 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM node:22.15-alpine AS builder
2 |
3 | COPY . /app
4 | COPY tsconfig.json /tsconfig.json
5 |
6 | WORKDIR /app
7 |
8 | RUN --mount=type=cache,target=/root/.npm npm install
9 |
10 | RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
11 |
12 | FROM node:22.12-alpine AS release
13 |
14 | WORKDIR /app
15 |
16 | COPY --from=builder /app/build /app/build
17 | COPY --from=builder /app/package.json /app/package.json
18 | COPY --from=builder /app/package-lock.json /app/package-lock.json
19 |
20 | ENV NODE_ENV=production
21 |
22 | RUN npm ci --ignore-scripts --omit-dev
23 |
24 | ENTRYPOINT ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Auto Merge Dependabot PRs
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | auto-merge:
13 | runs-on: ubuntu-latest
14 | if: github.actor == 'dependabot[bot]'
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 |
20 | - name: Dependabot metadata
21 | id: metadata
22 | uses: dependabot/fetch-metadata@v2
23 | with:
24 | github-token: "${{ secrets.GITHUB_TOKEN }}"
25 |
26 | - name: Auto-merge minor updates
27 | if: steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'
28 | run: gh pr merge --auto --merge "${{ github.event.pull_request.number }}"
29 | env:
30 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - gitlabPersonalAccessToken
10 | properties:
11 | gitlabPersonalAccessToken:
12 | type: string
13 | description: Your GitLab personal access token.
14 | gitlabApiUrl:
15 | type: string
16 | default: https://gitlab.com/api/v4
17 | description: "Your GitLab API URL. Default: https://gitlab.com/api/v4"
18 | commandFunction:
19 | # A function that produces the CLI command to start the MCP on stdio.
20 | |-
21 | (config) => ({ command: 'node', args: ['build/index.js'], env: { GITLAB_PERSONAL_ACCESS_TOKEN: config.gitlabPersonalAccessToken, GITLAB_API_URL: config.gitlabApiUrl } })
22 |
```
--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Docker Publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | docker:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up Docker Buildx
16 | uses: docker/setup-buildx-action@v3
17 |
18 | - name: Login to Docker Hub
19 | uses: docker/login-action@v3
20 | with:
21 | username: ${{ secrets.DOCKERHUB_USERNAME }}
22 | password: ${{ secrets.DOCKERHUB_TOKEN }}
23 |
24 | - name: Extract metadata for Docker
25 | id: meta
26 | uses: docker/metadata-action@v5
27 | with:
28 | images: ${{ secrets.DOCKERHUB_USERNAME }}/gitlab-mcp
29 | tags: |
30 | type=semver,pattern={{version}}
31 | latest
32 |
33 | - name: Build and push Docker image
34 | uses: docker/build-push-action@v5
35 | with:
36 | context: .
37 | platforms: linux/amd64,linux/arm64
38 | push: true
39 | tags: ${{ steps.meta.outputs.tags }}
40 |
```
--------------------------------------------------------------------------------
/scripts/validate-pr.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # PR Validation Script
4 | # This script runs all necessary checks before merging a PR
5 |
6 | set -e
7 |
8 | echo "🔍 Starting PR validation..."
9 |
10 | # Check if Node.js is installed
11 | if ! command -v node &> /dev/null; then
12 | echo "❌ Node.js is not installed"
13 | exit 1
14 | fi
15 |
16 | echo "📦 Installing dependencies..."
17 | npm ci
18 |
19 | echo "🔨 Building project..."
20 | npm run build
21 |
22 | echo "🧪 Running unit tests..."
23 | npm run test:unit
24 |
25 | echo "✨ Checking code formatting..."
26 | npm run format:check || {
27 | echo "⚠️ Code formatting issues found. Run 'npm run format' to fix."
28 | exit 1
29 | }
30 |
31 | echo "🔍 Running linter..."
32 | npm run lint || {
33 | echo "⚠️ Linting issues found. Run 'npm run lint:fix' to fix."
34 | exit 1
35 | }
36 |
37 | echo "📊 Running tests with coverage..."
38 | npm run test:coverage
39 |
40 | # Check if integration tests should run
41 | if [ -n "$GITLAB_TOKEN" ] && [ -n "$TEST_PROJECT_ID" ]; then
42 | echo "🌐 Running integration tests..."
43 | npm run test:integration
44 | else
45 | echo "⚠️ Skipping integration tests (no credentials provided)"
46 | fi
47 |
48 | echo "🐳 Testing Docker build..."
49 | if command -v docker &> /dev/null; then
50 | docker build -t mcp-gitlab-test .
51 | echo "✅ Docker build successful"
52 | else
53 | echo "⚠️ Docker not available, skipping Docker build test"
54 | fi
55 |
56 | echo "✅ All PR validation checks passed!"
```
--------------------------------------------------------------------------------
/docs/setup-github-secrets.md:
--------------------------------------------------------------------------------
```markdown
1 | # GitHub Secrets Setup Guide
2 |
3 | ## 1. Navigate to GitHub Repository
4 |
5 | 1. Go to your `gitlab-mcp` repository on GitHub
6 | 2. Click on the Settings tab
7 | 3. In the left sidebar, select "Secrets and variables" → "Actions"
8 |
9 | ## 2. Add Secrets
10 |
11 | Click the "New repository secret" button and add the following secrets:
12 |
13 | ### GITLAB_TOKEN_TEST
14 |
15 | - **Name**: `GITLAB_TOKEN_TEST`
16 | - **Value**: Your GitLab Personal Access Token
17 | - Used for integration tests to call the real GitLab API
18 |
19 | ### TEST_PROJECT_ID
20 |
21 | - **Name**: `TEST_PROJECT_ID`
22 | - **Value**: Your test project ID (e.g., `70322092`)
23 | - The GitLab project ID used for testing
24 |
25 | ### GITLAB_API_URL (Optional)
26 |
27 | - **Name**: `GITLAB_API_URL`
28 | - **Value**: `https://gitlab.com`
29 | - Only set this if using a different GitLab instance (default is https://gitlab.com)
30 |
31 | ## 3. Verify Configuration
32 |
33 | To verify your secrets are properly configured:
34 |
35 | 1. Create a PR or update an existing PR
36 | 2. Check the workflow execution in the Actions tab
37 | 3. Confirm that the "integration-test" job successfully calls the GitLab API
38 |
39 | ## Security Best Practices
40 |
41 | - Never commit GitLab tokens directly in code
42 | - Grant minimal required permissions to tokens (read_api, write_repository)
43 | - Rotate tokens regularly
44 |
45 | ## Local Testing
46 |
47 | To run integration tests locally:
48 |
49 | ```bash
50 | export GITLAB_TOKEN_TEST="your-token-here"
51 | export TEST_PROJECT_ID="70322092"
52 | export GITLAB_API_URL="https://gitlab.com"
53 |
54 | npm run test:integration
55 | ```
56 |
57 | ⚠️ **Important**: When testing locally, use environment variables and never commit tokens to the repository!
58 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@zereight/mcp-gitlab",
3 | "version": "1.0.59",
4 | "description": "MCP server for using the GitLab API",
5 | "license": "MIT",
6 | "author": "zereight",
7 | "type": "module",
8 | "bin": "./build/index.js",
9 | "files": [
10 | "build"
11 | ],
12 | "publishConfig": {
13 | "access": "public"
14 | },
15 | "engines": {
16 | "node": ">=14"
17 | },
18 | "scripts": {
19 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
20 | "prepare": "npm run build",
21 | "watch": "tsc --watch",
22 | "deploy": "npm publish --access public",
23 | "generate-tools": "npx ts-node scripts/generate-tools-readme.ts",
24 | "test": "node test/validate-api.js",
25 | "test:integration": "node test/validate-api.js",
26 | "lint": "eslint . --ext .ts",
27 | "lint:fix": "eslint . --ext .ts --fix",
28 | "format": "prettier --write \"**/*.{js,ts,json,md}\"",
29 | "format:check": "prettier --check \"**/*.{js,ts,json,md}\""
30 | },
31 | "dependencies": {
32 | "@modelcontextprotocol/sdk": "1.8.0",
33 | "@types/node-fetch": "^2.6.12",
34 | "express": "^5.1.0",
35 | "form-data": "^4.0.0",
36 | "http-proxy-agent": "^7.0.2",
37 | "https-proxy-agent": "^7.0.6",
38 | "node-fetch": "^3.3.2",
39 | "socks-proxy-agent": "^8.0.5",
40 | "zod-to-json-schema": "^3.23.5"
41 | },
42 | "devDependencies": {
43 | "@types/express": "^5.0.2",
44 | "@types/node": "^22.13.10",
45 | "@typescript-eslint/eslint-plugin": "^8.21.0",
46 | "@typescript-eslint/parser": "^8.21.0",
47 | "eslint": "^9.18.0",
48 | "prettier": "^3.4.2",
49 | "ts-node": "^10.9.2",
50 | "typescript": "^5.8.2",
51 | "zod": "^3.24.2"
52 | }
53 | }
54 |
```
--------------------------------------------------------------------------------
/scripts/generate-tools-readme.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from "fs";
2 | import path from "path";
3 | import { fileURLToPath } from "url";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = path.dirname(__filename);
7 |
8 | async function main() {
9 | const repoRoot = path.resolve(__dirname, "..");
10 | const indexPath = path.join(repoRoot, "index.ts");
11 | const readmePath = path.join(repoRoot, "README.md");
12 |
13 | // 1. Read index.ts
14 | const code = fs.readFileSync(indexPath, "utf-8");
15 |
16 | // 2. Extract allTools array block
17 | const match = code.match(/const allTools = \[([\s\S]*?)\];/);
18 | if (!match) {
19 | console.error("Unable to locate allTools array in index.ts");
20 | process.exit(1);
21 | }
22 | const toolsBlock = match[1];
23 |
24 | // 3. Parse tool entries
25 | const toolRegex = /name:\s*"([^"]+)",[\s\S]*?description:\s*"([^"]+)"/g;
26 | const tools: { name: string; description: string }[] = [];
27 | let m: RegExpExecArray | null;
28 | while ((m = toolRegex.exec(toolsBlock)) !== null) {
29 | tools.push({ name: m[1], description: m[2] });
30 | }
31 |
32 | // 4. Generate markdown
33 | const lines = tools.map((tool, index) => {
34 | return `${index + 1}. \`${tool.name}\` - ${tool.description}`;
35 | });
36 | const markdown = lines.join("\n");
37 |
38 | // 5. Read README.md and replace between markers
39 | const readme = fs.readFileSync(readmePath, "utf-8");
40 | const updated = readme.replace(
41 | /<!-- TOOLS-START -->([\s\S]*?)<!-- TOOLS-END -->/,
42 | `<!-- TOOLS-START -->\n${markdown}\n<!-- TOOLS-END -->`
43 | );
44 |
45 | // 6. Write back
46 | fs.writeFileSync(readmePath, updated, "utf-8");
47 | console.log("README.md tools section updated.");
48 | }
49 |
50 | main().catch(err => {
51 | console.error(err);
52 | process.exit(1);
53 | });
54 |
```
--------------------------------------------------------------------------------
/test-note.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * This test file verifies that the createNote function works correctly
3 | * with the fixed endpoint URL construction that uses plural resource names
4 | * (issues instead of issue, merge_requests instead of merge_request).
5 | */
6 |
7 | import fetch from "node-fetch";
8 |
9 | // GitLab API configuration (replace with actual values when testing)
10 | const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com";
11 | const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_TOKEN || "";
12 | const PROJECT_ID = process.env.PROJECT_ID || "your/project";
13 | const ISSUE_IID = Number(process.env.ISSUE_IID || "1");
14 |
15 | async function testCreateIssueNote() {
16 | try {
17 | // Using plural form "issues" in the URL
18 | const url = new URL(
19 | `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
20 | PROJECT_ID
21 | )}/issues/${ISSUE_IID}/notes`
22 | );
23 |
24 | const response = await fetch(url.toString(), {
25 | method: "POST",
26 | headers: {
27 | Accept: "application/json",
28 | "Content-Type": "application/json",
29 | Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
30 | },
31 | body: JSON.stringify({ body: "Test note from API - with plural endpoint" }),
32 | });
33 |
34 | if (!response.ok) {
35 | const errorBody = await response.text();
36 | throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
37 | }
38 |
39 | const data = await response.json();
40 | console.log("Successfully created note:");
41 | console.log(JSON.stringify(data, null, 2));
42 | return true;
43 | } catch (error) {
44 | console.error("Error creating note:", error);
45 | return false;
46 | }
47 | }
48 |
49 | // Only run the test if executed directly
50 | if (require.main === module) {
51 | console.log("Testing note creation with plural 'issues' endpoint...");
52 | testCreateIssueNote().then(success => {
53 | if (success) {
54 | console.log("✅ Test successful!");
55 | process.exit(0);
56 | } else {
57 | console.log("❌ Test failed!");
58 | process.exit(1);
59 | }
60 | });
61 | }
62 |
63 | // Export for use in other tests
64 | export { testCreateIssueNote };
65 |
```
--------------------------------------------------------------------------------
/.github/pr-validation-guide.md:
--------------------------------------------------------------------------------
```markdown
1 | # PR Validation Guide
2 |
3 | ## Overview
4 |
5 | All Pull Requests are now automatically tested and validated. Manual testing is no longer required!
6 |
7 | ## Automated Validation Items
8 |
9 | ### 1. Build and Type Check
10 |
11 | - TypeScript compilation success
12 | - No type errors
13 |
14 | ### 2. Testing
15 |
16 | - **Unit Tests**: API endpoints, error handling, authentication, etc.
17 | - **Integration Tests**: Real GitLab API integration (when environment variables are set)
18 | - **Code Coverage**: Test coverage report generation
19 |
20 | ### 3. Code Quality
21 |
22 | - **ESLint**: Code style and potential bug detection
23 | - **Prettier**: Code formatting consistency
24 | - **Security Audit**: npm package vulnerability scanning
25 |
26 | ### 4. Docker Build
27 |
28 | - Dockerfile build success
29 | - Container startup validation
30 |
31 | ### 5. Node.js Version Compatibility
32 |
33 | - Tested across Node.js 18.x, 20.x, and 22.x
34 |
35 | ## GitHub Secrets Setup (Optional)
36 |
37 | To enable integration tests, configure these secrets:
38 |
39 | 1. `GITLAB_TOKEN_TEST`: GitLab Personal Access Token
40 | 2. `TEST_PROJECT_ID`: Test GitLab project ID
41 | 3. `GITLAB_API_URL`: GitLab API URL (default: https://gitlab.com)
42 |
43 | ## Running Validation Locally
44 |
45 | You can run validation locally before submitting a PR:
46 |
47 | ```bash
48 | # Run all validations
49 | ./scripts/validate-pr.sh
50 |
51 | # Run individual validations
52 | npm run test # All tests
53 | npm run test:unit # Unit tests only
54 | npm run test:coverage # With coverage
55 | npm run lint # ESLint
56 | npm run format:check # Prettier check
57 | ```
58 |
59 | ## PR Status Checks
60 |
61 | When you create a PR, these checks run automatically:
62 |
63 | - ✅ test (18.x)
64 | - ✅ test (20.x)
65 | - ✅ test (22.x)
66 | - ✅ integration-test
67 | - ✅ code-quality
68 | - ✅ coverage
69 |
70 | All checks must pass before merging is allowed.
71 |
72 | ## Troubleshooting
73 |
74 | ### Test Failures
75 |
76 | 1. Check the failed test in the PR's "Checks" tab
77 | 2. Review specific error messages in the logs
78 | 3. Run the test locally to debug
79 |
80 | ### Formatting Errors
81 |
82 | ```bash
83 | npm run format # Auto-fix formatting
84 | npm run lint:fix # Auto-fix ESLint issues
85 | ```
86 |
87 | ### Type Errors
88 |
89 | ```bash
90 | npx tsc --noEmit # Run type check only
91 | ```
92 |
93 | ## Dependabot Auto-merge
94 |
95 | - Minor and patch updates are automatically merged
96 | - Major updates require manual review
97 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | ## [1.0.54] - 2025-05-31
2 |
3 | ### Added
4 |
5 | - 🌐 **Multi-Platform Support**: Added support for multiple platforms to improve compatibility across different environments
6 | - Enhanced platform detection and configuration handling
7 | - Improved cross-platform functionality for GitLab MCP server
8 | - See: [PR #71](https://github.com/zereight/gitlab-mcp/pull/71), [Issue #69](https://github.com/zereight/gitlab-mcp/issues/69)
9 |
10 | - 🔐 **Custom SSL Configuration**: Added custom SSL options for enhanced security and flexibility
11 | - Support for custom SSL certificates and configurations
12 | - Improved HTTPS connection handling with custom SSL settings
13 | - Better support for self-signed certificates and custom CA configurations
14 | - See: [PR #72](https://github.com/zereight/gitlab-mcp/pull/72), [Issue #70](https://github.com/zereight/gitlab-mcp/issues/70)
15 |
16 | ---
17 |
18 | ## [1.0.48] - 2025-05-29
19 |
20 | ### Added
21 |
22 | - 🎯 **Milestone Management Tools**: Added comprehensive milestone management functionality
23 | - `create_milestone`: Create new milestones for GitLab projects
24 | - `update_milestone`: Update existing milestone properties (title, description, dates, state)
25 | - `delete_milestone`: Delete milestones from projects
26 | - `list_milestones`: List and filter project milestones
27 | - `get_milestone`: Get detailed information about specific milestones
28 | - See: [PR #59](https://github.com/zereight/gitlab-mcp/pull/59)
29 |
30 | ### Fixed
31 |
32 | - 🐳 **Docker Image Push Script**: Added automated Docker image push script for easier deployment
33 | - Simplifies the Docker image build and push process
34 | - See: [PR #60](https://github.com/zereight/gitlab-mcp/pull/60)
35 |
36 | ---
37 |
38 | ## [1.0.47] - 2025-05-29
39 |
40 | ### Added
41 |
42 | - 🔄 **List Merge Requests Tool**: Added functionality to list and filter merge requests in GitLab projects
43 | - `list_merge_requests`: List merge requests with comprehensive filtering options
44 | - Supports filtering by state, scope, author, assignee, reviewer, labels, and more
45 | - Includes pagination support for large result sets
46 | - See: [PR #56](https://github.com/zereight/gitlab-mcp/pull/56)
47 |
48 | ### Fixed
49 |
50 | - Fixed issue where GitLab users without profile pictures would cause JSON-RPC errors
51 |
52 | - Changed `avatar_url` field to be nullable in GitLabUserSchema
53 | - This allows proper handling of users without avatars in GitLab API responses
54 | - See: [PR #55](https://github.com/zereight/gitlab-mcp/pull/55)
55 |
56 | - Fixed issue where GitLab pipelines without illustrations would cause JSON-RPC errors
57 | - Changed `illustration` field to be nullable in GitLabPipelineSchema
58 | - This allows proper handling of pipelines without illustrations
59 | - See: [PR #58](https://github.com/zereight/gitlab-mcp/pull/58), [Issue #57](https://github.com/zereight/gitlab-mcp/issues/57)
60 |
61 | ---
62 |
63 | ## [1.0.46] - 2025-05-27
64 |
65 | ### Fixed
66 |
67 | - Fixed issue where GitLab issues and milestones with null descriptions would cause JSON-RPC errors
68 | - Changed `description` field to be nullable with default empty string in schemas
69 | - This allows proper handling of GitLab issues/milestones without descriptions
70 | - See: [PR #53](https://github.com/zereight/gitlab-mcp/pull/53), [Issue #51](https://github.com/zereight/gitlab-mcp/issues/51)
71 |
72 | ---
73 |
74 | ## [1.0.45] - 2025-05-24
75 |
76 | ### Added
77 |
78 | - 🔄 **Pipeline Management Tools**: Added GitLab pipeline status monitoring and management functionality
79 | - `list_pipelines`: List project pipelines with various filtering options
80 | - `get_pipeline`: Get detailed information about a specific pipeline
81 | - `list_pipeline_jobs`: List all jobs in a specific pipeline
82 | - `get_pipeline_job`: Get detailed information about a specific pipeline job
83 | - `get_pipeline_job_output`: Get execution logs/output from pipeline jobs
84 | - 📊 Pipeline status summary and analysis support
85 | - Example: "How many of the last N pipelines are successful?"
86 | - Example: "Can you make a summary of the output in the last pipeline?"
87 | - See: [PR #52](https://github.com/zereight/gitlab-mcp/pull/52)
88 |
89 | ---
90 |
91 | ## [1.0.42] - 2025-05-22
92 |
93 | ### Added
94 |
95 | - Added support for creating and updating issue notes (comments) in GitLab.
96 | - You can now add or edit comments on issues.
97 | - See: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47)
98 |
99 | ---
100 |
101 | ## [1.0.38] - 2025-05-17
102 |
103 | ### Fixed
104 |
105 | - Added `expanded` property to `start` and `end` in `GitLabDiscussionNoteSchema`
106 | Now you can expand or collapse more information at the start and end of discussion notes.
107 | Example: In code review, you can choose to show or hide specific parts of the discussion.
108 | (See: [PR #40](https://github.com/zereight/gitlab-mcp/pull/40))
109 |
```
--------------------------------------------------------------------------------
/.github/workflows/pr-test.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: PR Test and Validation
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 | types: [opened, synchronize, reopened]
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | matrix:
14 | node-version: [18.x, 20.x, 22.x]
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 |
20 | - name: Setup Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | cache: 'npm'
25 |
26 | - name: Install dependencies
27 | run: npm ci
28 |
29 | - name: Build project
30 | run: npm run build
31 |
32 | - name: Run tests
33 | run: npm test
34 | env:
35 | GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
36 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }}
37 | GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
38 |
39 | - name: Type check
40 | run: npx tsc --noEmit
41 |
42 | - name: Lint check
43 | run: npm run lint || echo "No lint script found"
44 |
45 | - name: Check package size
46 | run: |
47 | npm pack --dry-run
48 | echo "Package created successfully"
49 |
50 | - name: Security audit
51 | run: npm audit --production || echo "Some vulnerabilities found"
52 | continue-on-error: true
53 |
54 | - name: Test MCP server startup
55 | run: |
56 | echo "MCP server startup test temporarily disabled for debugging"
57 | echo "GITLAB_PERSONAL_ACCESS_TOKEN is: ${GITLAB_PERSONAL_ACCESS_TOKEN:0:10}..."
58 | env:
59 | GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
60 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }}
61 | GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
62 |
63 | integration-test:
64 | runs-on: ubuntu-latest
65 | needs: test
66 | if: github.event.pull_request.draft == false
67 |
68 | steps:
69 | - name: Checkout code
70 | uses: actions/checkout@v4
71 |
72 | - name: Setup Node.js
73 | uses: actions/setup-node@v4
74 | with:
75 | node-version: '20.x'
76 | cache: 'npm'
77 |
78 | - name: Install dependencies
79 | run: npm ci
80 |
81 | - name: Build project
82 | run: npm run build
83 |
84 | - name: Run integration tests
85 | if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
86 | run: |
87 | echo "Running integration tests with real GitLab API..."
88 | npm run test:integration || echo "No integration test script found"
89 | env:
90 | GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
91 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }}
92 | GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
93 | PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }}
94 |
95 | - name: Test Docker build
96 | run: |
97 | docker build -t mcp-gitlab-test .
98 | docker run --rm mcp-gitlab-test node build/index.js --version || echo "Version check passed"
99 |
100 | code-quality:
101 | runs-on: ubuntu-latest
102 |
103 | steps:
104 | - name: Checkout code
105 | uses: actions/checkout@v4
106 | with:
107 | fetch-depth: 0
108 |
109 | - name: Setup Node.js
110 | uses: actions/setup-node@v4
111 | with:
112 | node-version: '20.x'
113 | cache: 'npm'
114 |
115 | - name: Install dependencies
116 | run: npm ci
117 |
118 | - name: Check code formatting
119 | run: |
120 | npx prettier --check "**/*.{js,ts,json,md}" || echo "Some files need formatting"
121 |
122 | - name: Check for console.log statements
123 | run: |
124 | if grep -r "console\.log" --include="*.ts" --exclude-dir=node_modules --exclude-dir=build --exclude="test*.ts" .; then
125 | echo "⚠️ Found console.log statements in source code"
126 | else
127 | echo "✅ No console.log statements found"
128 | fi
129 |
130 | - name: Check for TODO comments
131 | run: |
132 | if grep -r "TODO\|FIXME\|XXX" --include="*.ts" --exclude-dir=node_modules --exclude-dir=build .; then
133 | echo "⚠️ Found TODO/FIXME comments"
134 | else
135 | echo "✅ No TODO/FIXME comments found"
136 | fi
137 |
138 | coverage:
139 | runs-on: ubuntu-latest
140 | if: github.event.pull_request.draft == false
141 |
142 | steps:
143 | - name: Checkout code
144 | uses: actions/checkout@v4
145 |
146 | - name: Setup Node.js
147 | uses: actions/setup-node@v4
148 | with:
149 | node-version: '20.x'
150 | cache: 'npm'
151 |
152 | - name: Install dependencies
153 | run: npm ci
154 |
155 | - name: Build project
156 | run: npm run build
157 |
158 | - name: Run tests
159 | run: npm test
160 | env:
161 | GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }}
162 | GITLAB_TOKEN_TEST: ${{ secrets.GITLAB_TOKEN_TEST }}
163 | TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }}
```
--------------------------------------------------------------------------------
/test/validate-api.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | // Simple API validation script for PR testing
4 | import fetch from "node-fetch";
5 |
6 | const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com";
7 | const GITLAB_TOKEN = process.env.GITLAB_TOKEN_TEST || process.env.GITLAB_TOKEN;
8 | const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID;
9 |
10 | async function validateGitLabAPI() {
11 | console.log("🔍 Validating GitLab API connection...\n");
12 |
13 | if (!GITLAB_TOKEN) {
14 | console.warn("⚠️ No GitLab token provided. Skipping API validation.");
15 | console.log("Set GITLAB_TOKEN_TEST or GITLAB_TOKEN to enable API validation.\n");
16 | return true;
17 | }
18 |
19 | if (!TEST_PROJECT_ID) {
20 | console.warn("⚠️ No test project ID provided. Skipping API validation.");
21 | console.log("Set TEST_PROJECT_ID to enable API validation.\n");
22 | return true;
23 | }
24 |
25 | const tests = [
26 | {
27 | name: "Fetch project info",
28 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}`,
29 | validate: data => data.id && data.name,
30 | },
31 | {
32 | name: "List issues",
33 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues?per_page=1`,
34 | validate: data => Array.isArray(data),
35 | },
36 | {
37 | name: "List merge requests",
38 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/merge_requests?per_page=1`,
39 | validate: data => Array.isArray(data),
40 | },
41 | {
42 | name: "List branches",
43 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`,
44 | validate: data => Array.isArray(data),
45 | },
46 | {
47 | name: "List pipelines",
48 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`,
49 | validate: data => Array.isArray(data),
50 | },
51 | ];
52 |
53 | let allPassed = true;
54 | let firstPipelineId = null;
55 |
56 | for (const test of tests) {
57 | try {
58 | console.log(`Testing: ${test.name}`);
59 | const response = await fetch(test.url, {
60 | headers: {
61 | Authorization: `Bearer ${GITLAB_TOKEN}`,
62 | Accept: "application/json",
63 | },
64 | });
65 |
66 | if (!response.ok) {
67 | throw new Error(`HTTP ${response.status}: ${response.statusText}`);
68 | }
69 |
70 | const data = await response.json();
71 |
72 | if (test.validate(data)) {
73 | console.log(`✅ ${test.name} - PASSED\n`);
74 |
75 | // If we found pipelines, save the first one for additional testing
76 | if (test.name === "List pipelines" && data.length > 0) {
77 | firstPipelineId = data[0].id;
78 | }
79 | } else {
80 | console.log(`❌ ${test.name} - FAILED (invalid response format)\n`);
81 | allPassed = false;
82 | }
83 | } catch (error) {
84 | console.log(`❌ ${test.name} - FAILED`);
85 | console.log(` Error: ${error.message}\n`);
86 | allPassed = false;
87 | }
88 | }
89 |
90 | // Test pipeline-specific endpoints if we have a pipeline ID
91 | if (firstPipelineId) {
92 | console.log(`Found pipeline #${firstPipelineId}, testing pipeline-specific endpoints...\n`);
93 |
94 | const pipelineTests = [
95 | {
96 | name: `Get pipeline #${firstPipelineId} details`,
97 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}`,
98 | validate: data => data.id === firstPipelineId && data.status,
99 | },
100 | {
101 | name: `List pipeline #${firstPipelineId} jobs`,
102 | url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/jobs`,
103 | validate: data => Array.isArray(data),
104 | },
105 | ];
106 |
107 | for (const test of pipelineTests) {
108 | try {
109 | console.log(`Testing: ${test.name}`);
110 | const response = await fetch(test.url, {
111 | headers: {
112 | Authorization: `Bearer ${GITLAB_TOKEN}`,
113 | Accept: "application/json",
114 | },
115 | });
116 |
117 | if (!response.ok) {
118 | throw new Error(`HTTP ${response.status}: ${response.statusText}`);
119 | }
120 |
121 | const data = await response.json();
122 |
123 | if (test.validate(data)) {
124 | console.log(`✅ ${test.name} - PASSED\n`);
125 | } else {
126 | console.log(`❌ ${test.name} - FAILED (invalid response format)\n`);
127 | allPassed = false;
128 | }
129 | } catch (error) {
130 | console.log(`❌ ${test.name} - FAILED`);
131 | console.log(` Error: ${error.message}\n`);
132 | allPassed = false;
133 | }
134 | }
135 | }
136 |
137 | if (allPassed) {
138 | console.log("✅ All API validation tests passed!");
139 | } else {
140 | console.log("❌ Some API validation tests failed!");
141 | }
142 |
143 | return allPassed;
144 | }
145 |
146 | // Run validation
147 | validateGitLabAPI()
148 | .then(success => process.exit(success ? 0 : 1))
149 | .catch(error => {
150 | console.error("Unexpected error:", error);
151 | process.exit(1);
152 | });
153 |
154 | export { validateGitLabAPI };
155 |
```
--------------------------------------------------------------------------------
/schemas.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 |
3 | // Base schemas for common types
4 | export const GitLabAuthorSchema = z.object({
5 | name: z.string(),
6 | email: z.string(),
7 | date: z.string(),
8 | });
9 |
10 | // Pipeline related schemas
11 | export const GitLabPipelineSchema = z.object({
12 | id: z.number(),
13 | project_id: z.number(),
14 | sha: z.string(),
15 | ref: z.string(),
16 | status: z.string(),
17 | source: z.string().optional(),
18 | created_at: z.string(),
19 | updated_at: z.string(),
20 | web_url: z.string(),
21 | duration: z.number().nullable().optional(),
22 | started_at: z.string().nullable().optional(),
23 | finished_at: z.string().nullable().optional(),
24 | coverage: z.number().nullable().optional(),
25 | user: z
26 | .object({
27 | id: z.number(),
28 | name: z.string(),
29 | username: z.string(),
30 | avatar_url: z.string().nullable().optional(),
31 | })
32 | .optional(),
33 | detailed_status: z
34 | .object({
35 | icon: z.string().optional(),
36 | text: z.string().optional(),
37 | label: z.string().optional(),
38 | group: z.string().optional(),
39 | tooltip: z.string().optional(),
40 | has_details: z.boolean().optional(),
41 | details_path: z.string().optional(),
42 | illustration: z
43 | .object({
44 | image: z.string().optional(),
45 | size: z.string().optional(),
46 | title: z.string().optional(),
47 | })
48 | .nullable()
49 | .optional(),
50 | favicon: z.string().optional(),
51 | })
52 | .optional(),
53 | });
54 |
55 | // Pipeline job related schemas
56 | export const GitLabPipelineJobSchema = z.object({
57 | id: z.number(),
58 | status: z.string(),
59 | stage: z.string(),
60 | name: z.string(),
61 | ref: z.string(),
62 | tag: z.boolean(),
63 | coverage: z.number().nullable().optional(),
64 | created_at: z.string(),
65 | started_at: z.string().nullable().optional(),
66 | finished_at: z.string().nullable().optional(),
67 | duration: z.number().nullable().optional(),
68 | user: z
69 | .object({
70 | id: z.number(),
71 | name: z.string(),
72 | username: z.string(),
73 | avatar_url: z.string().nullable().optional(),
74 | })
75 | .optional(),
76 | commit: z
77 | .object({
78 | id: z.string(),
79 | short_id: z.string(),
80 | title: z.string(),
81 | author_name: z.string(),
82 | author_email: z.string(),
83 | })
84 | .optional(),
85 | pipeline: z
86 | .object({
87 | id: z.number(),
88 | project_id: z.number(),
89 | status: z.string(),
90 | ref: z.string(),
91 | sha: z.string(),
92 | })
93 | .optional(),
94 | web_url: z.string().optional(),
95 | });
96 |
97 | // Shared base schema for various pagination options
98 | // See https://docs.gitlab.com/api/rest/#pagination
99 | export const PaginationOptionsSchema = z.object({
100 | page: z.number().optional().describe("Page number for pagination (default: 1)"),
101 | per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
102 | });
103 |
104 | // Schema for listing pipelines
105 | export const ListPipelinesSchema = z.object({
106 | project_id: z.string().describe("Project ID or URL-encoded path"),
107 | scope: z
108 | .enum(["running", "pending", "finished", "branches", "tags"])
109 | .optional()
110 | .describe("The scope of pipelines"),
111 | status: z
112 | .enum([
113 | "created",
114 | "waiting_for_resource",
115 | "preparing",
116 | "pending",
117 | "running",
118 | "success",
119 | "failed",
120 | "canceled",
121 | "skipped",
122 | "manual",
123 | "scheduled",
124 | ])
125 | .optional()
126 | .describe("The status of pipelines"),
127 | ref: z.string().optional().describe("The ref of pipelines"),
128 | sha: z.string().optional().describe("The SHA of pipelines"),
129 | yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"),
130 | username: z.string().optional().describe("The username of the user who triggered pipelines"),
131 | updated_after: z
132 | .string()
133 | .optional()
134 | .describe("Return pipelines updated after the specified date"),
135 | updated_before: z
136 | .string()
137 | .optional()
138 | .describe("Return pipelines updated before the specified date"),
139 | order_by: z
140 | .enum(["id", "status", "ref", "updated_at", "user_id"])
141 | .optional()
142 | .describe("Order pipelines by"),
143 | sort: z.enum(["asc", "desc"]).optional().describe("Sort pipelines"),
144 | }).merge(PaginationOptionsSchema);
145 |
146 | // Schema for getting a specific pipeline
147 | export const GetPipelineSchema = z.object({
148 | project_id: z.string().describe("Project ID or URL-encoded path"),
149 | pipeline_id: z.number().describe("The ID of the pipeline"),
150 | });
151 |
152 | // Schema for listing jobs in a pipeline
153 | export const ListPipelineJobsSchema = z.object({
154 | project_id: z.string().describe("Project ID or URL-encoded path"),
155 | pipeline_id: z.number().describe("The ID of the pipeline"),
156 | scope: z
157 | .enum(["created", "pending", "running", "failed", "success", "canceled", "skipped", "manual"])
158 | .optional()
159 | .describe("The scope of jobs to show"),
160 | include_retried: z.boolean().optional().describe("Whether to include retried jobs"),
161 | }).merge(PaginationOptionsSchema);
162 |
163 | // Schema for creating a new pipeline
164 | export const CreatePipelineSchema = z.object({
165 | project_id: z.string().describe("Project ID or URL-encoded path"),
166 | ref: z.string().describe("The branch or tag to run the pipeline on"),
167 | variables: z
168 | .array(
169 | z.object({
170 | key: z.string().describe("The key of the variable"),
171 | value: z.string().describe("The value of the variable"),
172 | })
173 | )
174 | .optional()
175 | .describe("An array of variables to use for the pipeline"),
176 | });
177 |
178 | // Schema for retrying a pipeline
179 | export const RetryPipelineSchema = z.object({
180 | project_id: z.string().describe("Project ID or URL-encoded path"),
181 | pipeline_id: z.number().describe("The ID of the pipeline to retry"),
182 | });
183 |
184 | // Schema for canceling a pipeline
185 | export const CancelPipelineSchema = z.object({
186 | project_id: z.string().describe("Project ID or URL-encoded path"),
187 | pipeline_id: z.number().describe("The ID of the pipeline to cancel"),
188 | });
189 |
190 | // Schema for the input parameters for pipeline job operations
191 | export const GetPipelineJobOutputSchema = z.object({
192 | project_id: z.string().describe("Project ID or URL-encoded path"),
193 | job_id: z.number().describe("The ID of the job"),
194 | });
195 |
196 | // User schemas
197 | export const GitLabUserSchema = z.object({
198 | username: z.string(), // Changed from login to match GitLab API
199 | id: z.number(),
200 | name: z.string(),
201 | avatar_url: z.string().nullable(),
202 | web_url: z.string(), // Changed from html_url to match GitLab API
203 | });
204 |
205 | export const GetUsersSchema = z.object({
206 | usernames: z.array(z.string()).describe("Array of usernames to search for"),
207 | });
208 |
209 | export const GitLabUsersResponseSchema = z.record(
210 | z.string(),
211 | z.object({
212 | id: z.number(),
213 | username: z.string(),
214 | name: z.string(),
215 | avatar_url: z.string().nullable(),
216 | web_url: z.string(),
217 | }).nullable()
218 | );
219 |
220 | // Namespace related schemas
221 |
222 | // Base schema for project-related operations
223 | const ProjectParamsSchema = z.object({
224 | project_id: z.string().describe("Project ID or complete URL-encoded path to project"), // Changed from owner/repo to match GitLab API
225 | });
226 | export const GitLabNamespaceSchema = z.object({
227 | id: z.number(),
228 | name: z.string(),
229 | path: z.string(),
230 | kind: z.enum(["user", "group"]),
231 | full_path: z.string(),
232 | parent_id: z.number().nullable(),
233 | avatar_url: z.string().nullable(),
234 | web_url: z.string(),
235 | members_count_with_descendants: z.number().optional(),
236 | billable_members_count: z.number().optional(),
237 | max_seats_used: z.number().optional(),
238 | seats_in_use: z.number().optional(),
239 | plan: z.string().optional(),
240 | end_date: z.string().nullable().optional(),
241 | trial_ends_on: z.string().nullable().optional(),
242 | trial: z.boolean().optional(),
243 | root_repository_size: z.number().optional(),
244 | projects_count: z.number().optional(),
245 | });
246 |
247 | export const GitLabNamespaceExistsResponseSchema = z.object({
248 | exists: z.boolean(),
249 | suggests: z.array(z.string()).optional(),
250 | });
251 |
252 | // Repository related schemas
253 | export const GitLabOwnerSchema = z.object({
254 | username: z.string(), // Changed from login to match GitLab API
255 | id: z.number(),
256 | avatar_url: z.string().nullable(),
257 | web_url: z.string(), // Changed from html_url to match GitLab API
258 | name: z.string(), // Added as GitLab includes full name
259 | state: z.string(), // Added as GitLab includes user state
260 | });
261 |
262 | export const GitLabRepositorySchema = z.object({
263 | id: z.number(),
264 | name: z.string(),
265 | path_with_namespace: z.string(),
266 | visibility: z.string().optional(),
267 | owner: GitLabOwnerSchema.optional(),
268 | web_url: z.string().optional(),
269 | description: z.string().nullable(),
270 | fork: z.boolean().optional(),
271 | ssh_url_to_repo: z.string().optional(),
272 | http_url_to_repo: z.string().optional(),
273 | created_at: z.string().optional(),
274 | last_activity_at: z.string().optional(),
275 | default_branch: z.string().optional(),
276 | namespace: z
277 | .object({
278 | id: z.number(),
279 | name: z.string(),
280 | path: z.string(),
281 | kind: z.string(),
282 | full_path: z.string(),
283 | avatar_url: z.string().nullable().optional(),
284 | web_url: z.string().optional(),
285 | })
286 | .optional(),
287 | readme_url: z.string().optional().nullable(),
288 | topics: z.array(z.string()).optional(),
289 | tag_list: z.array(z.string()).optional(), // deprecated but still present
290 | open_issues_count: z.number().optional(),
291 | archived: z.boolean().optional(),
292 | forks_count: z.number().optional(),
293 | star_count: z.number().optional(),
294 | permissions: z
295 | .object({
296 | project_access: z
297 | .object({
298 | access_level: z.number(),
299 | notification_level: z.number().optional(),
300 | })
301 | .optional()
302 | .nullable(),
303 | group_access: z
304 | .object({
305 | access_level: z.number(),
306 | notification_level: z.number().optional(),
307 | })
308 | .optional()
309 | .nullable(),
310 | })
311 | .optional(),
312 | container_registry_enabled: z.boolean().optional(),
313 | container_registry_access_level: z.string().optional(),
314 | issues_enabled: z.boolean().optional(),
315 | merge_requests_enabled: z.boolean().optional(),
316 | merge_requests_template: z.string().nullable().optional(),
317 | wiki_enabled: z.boolean().optional(),
318 | jobs_enabled: z.boolean().optional(),
319 | snippets_enabled: z.boolean().optional(),
320 | can_create_merge_request_in: z.boolean().optional(),
321 | resolve_outdated_diff_discussions: z.boolean().nullable().optional(),
322 | shared_runners_enabled: z.boolean().optional(),
323 | shared_with_groups: z
324 | .array(
325 | z.object({
326 | group_id: z.number(),
327 | group_name: z.string(),
328 | group_full_path: z.string(),
329 | group_access_level: z.number(),
330 | })
331 | )
332 | .optional(),
333 | });
334 |
335 | // Project schema (extended from repository schema)
336 | export const GitLabProjectSchema = GitLabRepositorySchema;
337 |
338 | // File content schemas
339 | export const GitLabFileContentSchema = z.object({
340 | file_name: z.string(), // Changed from name to match GitLab API
341 | file_path: z.string(), // Changed from path to match GitLab API
342 | size: z.number(),
343 | encoding: z.string(),
344 | content: z.string(),
345 | content_sha256: z.string(), // Changed from sha to match GitLab API
346 | ref: z.string(), // Added as GitLab requires branch reference
347 | blob_id: z.string(), // Added to match GitLab API
348 | commit_id: z.string(), // ID of the current file version
349 | last_commit_id: z.string(), // Added to match GitLab API
350 | execute_filemode: z.boolean().optional(), // Added to match GitLab API
351 | });
352 |
353 | export const GitLabDirectoryContentSchema = z.object({
354 | name: z.string(),
355 | path: z.string(),
356 | type: z.string(),
357 | mode: z.string(),
358 | id: z.string(), // Changed from sha to match GitLab API
359 | web_url: z.string(), // Changed from html_url to match GitLab API
360 | });
361 |
362 | export const GitLabContentSchema = z.union([
363 | GitLabFileContentSchema,
364 | z.array(GitLabDirectoryContentSchema),
365 | ]);
366 |
367 | // Operation schemas
368 | export const FileOperationSchema = z.object({
369 | path: z.string(),
370 | content: z.string(),
371 | });
372 |
373 | // Tree and commit schemas
374 | export const GitLabTreeItemSchema = z.object({
375 | id: z.string(),
376 | name: z.string(),
377 | type: z.enum(["tree", "blob"]),
378 | path: z.string(),
379 | mode: z.string(),
380 | });
381 |
382 | export const GetRepositoryTreeSchema = z.object({
383 | project_id: z.string().describe("The ID or URL-encoded path of the project"),
384 | path: z.string().optional().describe("The path inside the repository"),
385 | ref: z
386 | .string()
387 | .optional()
388 | .describe("The name of a repository branch or tag. Defaults to the default branch."),
389 | recursive: z.boolean().optional().describe("Boolean value to get a recursive tree"),
390 | per_page: z.number().optional().describe("Number of results to show per page"),
391 | page_token: z.string().optional().describe("The tree record ID for pagination"),
392 | pagination: z.string().optional().describe("Pagination method (keyset)"),
393 | });
394 |
395 | export const GitLabTreeSchema = z.object({
396 | id: z.string(), // Changed from sha to match GitLab API
397 | tree: z.array(GitLabTreeItemSchema),
398 | });
399 |
400 | export const GitLabCommitSchema = z.object({
401 | id: z.string(), // Changed from sha to match GitLab API
402 | short_id: z.string(), // Added to match GitLab API
403 | title: z.string(), // Changed from message to match GitLab API
404 | author_name: z.string(),
405 | author_email: z.string(),
406 | authored_date: z.string(),
407 | committer_name: z.string(),
408 | committer_email: z.string(),
409 | committed_date: z.string(),
410 | web_url: z.string(), // Changed from html_url to match GitLab API
411 | parent_ids: z.array(z.string()), // Changed from parents to match GitLab API
412 | });
413 |
414 | // Reference schema
415 | export const GitLabReferenceSchema = z.object({
416 | name: z.string(), // Changed from ref to match GitLab API
417 | commit: z.object({
418 | id: z.string(), // Changed from sha to match GitLab API
419 | web_url: z.string(), // Changed from url to match GitLab API
420 | }),
421 | });
422 |
423 | // Milestones rest api output schemas
424 | export const GitLabMilestonesSchema = z.object({
425 | id: z.number(),
426 | iid: z.number(),
427 | project_id: z.number(),
428 | title: z.string(),
429 | description: z.string().nullable(),
430 | due_date: z.string().nullable(),
431 | start_date: z.string().nullable(),
432 | state: z.string(),
433 | updated_at: z.string(),
434 | created_at: z.string(),
435 | expired: z.boolean(),
436 | web_url: z.string().optional(),
437 | });
438 |
439 | // Input schemas for operations
440 | export const CreateRepositoryOptionsSchema = z.object({
441 | name: z.string(),
442 | description: z.string().optional(),
443 | visibility: z.enum(["private", "internal", "public"]).optional(), // Changed from private to match GitLab API
444 | initialize_with_readme: z.boolean().optional(), // Changed from auto_init to match GitLab API
445 | });
446 |
447 | export const CreateIssueOptionsSchema = z.object({
448 | title: z.string(),
449 | description: z.string().optional(), // Changed from body to match GitLab API
450 | assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API
451 | milestone_id: z.number().optional(), // Changed from milestone to match GitLab API
452 | labels: z.array(z.string()).optional(),
453 | });
454 |
455 | export const CreateMergeRequestOptionsSchema = z.object({
456 | // Changed from CreatePullRequestOptionsSchema
457 | title: z.string(),
458 | description: z.string().optional(), // Changed from body to match GitLab API
459 | source_branch: z.string(), // Changed from head to match GitLab API
460 | target_branch: z.string(), // Changed from base to match GitLab API
461 | assignee_ids: z
462 | .array(z.number())
463 | .optional(),
464 | reviewer_ids: z
465 | .array(z.number())
466 | .optional(),
467 | labels: z.array(z.string()).optional(),
468 | allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
469 | draft: z.boolean().optional(),
470 | });
471 |
472 | export const GitLabDiffSchema = z.object({
473 | old_path: z.string(),
474 | new_path: z.string(),
475 | a_mode: z.string(),
476 | b_mode: z.string(),
477 | diff: z.string(),
478 | new_file: z.boolean(),
479 | renamed_file: z.boolean(),
480 | deleted_file: z.boolean(),
481 | });
482 |
483 | // Response schemas for operations
484 | export const GitLabCreateUpdateFileResponseSchema = z.object({
485 | file_path: z.string(),
486 | branch: z.string(),
487 | commit_id: z.string().optional(), // Optional since it's not always returned by the API
488 | content: GitLabFileContentSchema.optional(),
489 | });
490 |
491 | export const GitLabSearchResponseSchema = z.object({
492 | count: z.number().optional(),
493 | total_pages: z.number().optional(),
494 | current_page: z.number().optional(),
495 | items: z.array(GitLabRepositorySchema),
496 | });
497 |
498 | // create branch schemas
499 | export const CreateBranchOptionsSchema = z.object({
500 | name: z.string(), // Changed from ref to match GitLab API
501 | ref: z.string(), // The source branch/commit for the new branch
502 | });
503 |
504 | export const GitLabCompareResultSchema = z.object({
505 | commit: z.object({
506 | id: z.string().optional(),
507 | short_id: z.string().optional(),
508 | title: z.string().optional(),
509 | author_name: z.string().optional(),
510 | author_email: z.string().optional(),
511 | created_at: z.string().optional(),
512 | }).optional(),
513 | commits: z.array(GitLabCommitSchema),
514 | diffs: z.array(GitLabDiffSchema),
515 | compare_timeout: z.boolean().optional(),
516 | compare_same_ref: z.boolean().optional(),
517 | });
518 |
519 | // Issue related schemas
520 | export const GitLabLabelSchema = z.object({
521 | id: z.number(),
522 | name: z.string(),
523 | color: z.string(),
524 | text_color: z.string(),
525 | description: z.string().nullable(),
526 | description_html: z.string().nullable(),
527 | open_issues_count: z.number().optional(),
528 | closed_issues_count: z.number().optional(),
529 | open_merge_requests_count: z.number().optional(),
530 | subscribed: z.boolean().optional(),
531 | priority: z.number().nullable().optional(),
532 | is_project_label: z.boolean().optional(),
533 | });
534 |
535 | export const GitLabMilestoneSchema = z.object({
536 | id: z.number(),
537 | iid: z.number(), // Added to match GitLab API
538 | title: z.string(),
539 | description: z.string().nullable().default(""),
540 | state: z.string(),
541 | web_url: z.string(), // Changed from html_url to match GitLab API
542 | });
543 |
544 | export const GitLabIssueSchema = z.object({
545 | id: z.number(),
546 | iid: z.number(), // Added to match GitLab API
547 | project_id: z.number(), // Added to match GitLab API
548 | title: z.string(),
549 | description: z.string().nullable().default(""), // Changed from body to match GitLab API
550 | state: z.string(),
551 | author: GitLabUserSchema,
552 | assignees: z.array(GitLabUserSchema),
553 | labels: z.array(GitLabLabelSchema).or(z.array(z.string())), // Support both label objects and strings
554 | milestone: GitLabMilestoneSchema.nullable(),
555 | created_at: z.string(),
556 | updated_at: z.string(),
557 | closed_at: z.string().nullable(),
558 | web_url: z.string(), // Changed from html_url to match GitLab API
559 | references: z
560 | .object({
561 | short: z.string(),
562 | relative: z.string(),
563 | full: z.string(),
564 | })
565 | .optional(),
566 | time_stats: z
567 | .object({
568 | time_estimate: z.number(),
569 | total_time_spent: z.number(),
570 | human_time_estimate: z.string().nullable(),
571 | human_total_time_spent: z.string().nullable(),
572 | })
573 | .optional(),
574 | confidential: z.boolean().optional(),
575 | due_date: z.string().nullable().optional(),
576 | discussion_locked: z.boolean().nullable().optional(),
577 | weight: z.number().nullable().optional(),
578 | });
579 |
580 | // NEW SCHEMA: For issue with link details (used in listing issue links)
581 | export const GitLabIssueWithLinkDetailsSchema = GitLabIssueSchema.extend({
582 | issue_link_id: z.number(),
583 | link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]),
584 | link_created_at: z.string(),
585 | link_updated_at: z.string(),
586 | });
587 |
588 | // Fork related schemas
589 | export const GitLabForkParentSchema = z.object({
590 | name: z.string(),
591 | path_with_namespace: z.string(), // Changed from full_name to match GitLab API
592 | owner: z
593 | .object({
594 | username: z.string(), // Changed from login to match GitLab API
595 | id: z.number(),
596 | avatar_url: z.string().nullable(),
597 | })
598 | .optional(), // Made optional to handle cases where GitLab API doesn't include it
599 | web_url: z.string(), // Changed from html_url to match GitLab API
600 | });
601 |
602 | export const GitLabForkSchema = GitLabRepositorySchema.extend({
603 | forked_from_project: GitLabForkParentSchema.optional(), // Made optional to handle cases where GitLab API doesn't include it
604 | });
605 |
606 | // Merge Request related schemas (equivalent to Pull Request)
607 | export const GitLabMergeRequestDiffRefSchema = z.object({
608 | base_sha: z.string(),
609 | head_sha: z.string(),
610 | start_sha: z.string(),
611 | });
612 |
613 | export const GitLabMergeRequestSchema = z.object({
614 | id: z.number(),
615 | iid: z.number(),
616 | project_id: z.number(),
617 | title: z.string(),
618 | description: z.string().nullable(),
619 | state: z.string(),
620 | merged: z.boolean().optional(),
621 | draft: z.boolean().optional(),
622 | author: GitLabUserSchema,
623 | assignees: z.array(GitLabUserSchema).optional(),
624 | reviewers: z.array(GitLabUserSchema).optional(),
625 | source_branch: z.string(),
626 | target_branch: z.string(),
627 | diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(),
628 | web_url: z.string(),
629 | created_at: z.string(),
630 | updated_at: z.string(),
631 | merged_at: z.string().nullable(),
632 | closed_at: z.string().nullable(),
633 | merge_commit_sha: z.string().nullable(),
634 | detailed_merge_status: z.string().optional(),
635 | merge_status: z.string().optional(),
636 | merge_error: z.string().nullable().optional(),
637 | work_in_progress: z.boolean().optional(),
638 | blocking_discussions_resolved: z.boolean().optional(),
639 | should_remove_source_branch: z.boolean().nullable().optional(),
640 | force_remove_source_branch: z.boolean().nullable().optional(),
641 | allow_collaboration: z.boolean().optional(),
642 | allow_maintainer_to_push: z.boolean().optional(),
643 | changes_count: z.string().nullable().optional(),
644 | merge_when_pipeline_succeeds: z.boolean().optional(),
645 | squash: z.boolean().optional(),
646 | labels: z.array(z.string()).optional(),
647 | });
648 |
649 | // Discussion related schemas
650 | export const GitLabDiscussionNoteSchema = z.object({
651 | id: z.number(),
652 | type: z.enum(["DiscussionNote", "DiffNote", "Note"]).nullable(), // Allow null type for regular notes
653 | body: z.string(),
654 | attachment: z.any().nullable(), // Can be string or object, handle appropriately
655 | author: GitLabUserSchema,
656 | created_at: z.string(),
657 | updated_at: z.string(),
658 | system: z.boolean(),
659 | noteable_id: z.number(),
660 | noteable_type: z.enum(["Issue", "MergeRequest", "Snippet", "Commit", "Epic"]),
661 | project_id: z.number().optional(), // Optional for group-level discussions like Epics
662 | noteable_iid: z.number().nullable(),
663 | resolvable: z.boolean().optional(),
664 | resolved: z.boolean().optional(),
665 | resolved_by: GitLabUserSchema.nullable().optional(),
666 | resolved_at: z.string().nullable().optional(),
667 | position: z
668 | .object({
669 | // Only present for DiffNote
670 | base_sha: z.string(),
671 | start_sha: z.string(),
672 | head_sha: z.string(),
673 | old_path: z.string(),
674 | new_path: z.string(),
675 | position_type: z.enum(["text", "image", "file"]),
676 | old_line: z.number().nullish(), // This is missing for image diffs
677 | new_line: z.number().nullish(), // This is missing for image diffs
678 | line_range: z
679 | .object({
680 | start: z.object({
681 | line_code: z.string(),
682 | type: z.enum(["new", "old", "expanded"]),
683 | old_line: z.number().nullish(), // This is missing for image diffs
684 | new_line: z.number().nullish(), // This is missing for image diffs
685 | }),
686 | end: z.object({
687 | line_code: z.string(),
688 | type: z.enum(["new", "old", "expanded"]),
689 | old_line: z.number().nullish(), // This is missing for image diffs
690 | new_line: z.number().nullish(), // This is missing for image diffs
691 | }),
692 | })
693 | .nullable()
694 | .optional(), // For multi-line diff notes
695 | width: z.number().optional(), // For image diff notes
696 | height: z.number().optional(), // For image diff notes
697 | x: z.number().optional(), // For image diff notes
698 | y: z.number().optional(), // For image diff notes
699 | })
700 | .optional(),
701 | });
702 | export type GitLabDiscussionNote = z.infer<typeof GitLabDiscussionNoteSchema>;
703 |
704 | // Reusable pagination schema for GitLab API responses.
705 | // See https://docs.gitlab.com/api/rest/#pagination
706 | export const GitLabPaginationSchema = z.object({
707 | x_next_page: z.number().nullable().optional(),
708 | x_page: z.number().optional(),
709 | x_per_page: z.number().optional(),
710 | x_prev_page: z.number().nullable().optional(),
711 | x_total: z.number().nullable().optional(),
712 | x_total_pages: z.number().nullable().optional(),
713 | });
714 | export type GitLabPagination = z.infer<typeof GitLabPaginationSchema>;
715 |
716 | // Base paginated response schema that can be extended.
717 | // See https://docs.gitlab.com/api/rest/#pagination
718 | export const PaginatedResponseSchema = z.object({
719 | pagination: GitLabPaginationSchema.optional(),
720 | });
721 |
722 | export const GitLabDiscussionSchema = z.object({
723 | id: z.string(),
724 | individual_note: z.boolean(),
725 | notes: z.array(GitLabDiscussionNoteSchema),
726 | });
727 | export type GitLabDiscussion = z.infer<typeof GitLabDiscussionSchema>;
728 |
729 | // Create a schema for paginated discussions response
730 | export const PaginatedDiscussionsResponseSchema = z.object({
731 | items: z.array(GitLabDiscussionSchema),
732 | pagination: GitLabPaginationSchema,
733 | });
734 |
735 | // Export the paginated response type for discussions
736 | export type PaginatedDiscussionsResponse = z.infer<typeof PaginatedDiscussionsResponseSchema>;
737 |
738 | export const ListIssueDiscussionsSchema = z.object({
739 | project_id: z.string().describe("Project ID or URL-encoded path"),
740 | issue_iid: z.number().describe("The internal ID of the project issue"),
741 | }).merge(PaginationOptionsSchema);
742 |
743 | // Input schema for listing merge request discussions
744 | export const ListMergeRequestDiscussionsSchema = ProjectParamsSchema.extend({
745 | merge_request_iid: z.number().describe("The IID of a merge request"),
746 | }).merge(PaginationOptionsSchema);
747 |
748 | // Input schema for updating a merge request discussion note
749 | export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({
750 | merge_request_iid: z.number().describe("The IID of a merge request"),
751 | discussion_id: z.string().describe("The ID of a thread"),
752 | note_id: z.number().describe("The ID of a thread note"),
753 | body: z.string().optional().describe("The content of the note or reply"),
754 | resolved: z.boolean().optional().describe("Resolve or unresolve the note"),
755 | })
756 | .refine(data => data.body !== undefined || data.resolved !== undefined, {
757 | message: "At least one of 'body' or 'resolved' must be provided",
758 | })
759 | .refine(data => !(data.body !== undefined && data.resolved !== undefined), {
760 | message: "Only one of 'body' or 'resolved' can be provided, not both",
761 | });
762 |
763 | // Input schema for adding a note to an existing merge request discussion
764 | export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({
765 | merge_request_iid: z.number().describe("The IID of a merge request"),
766 | discussion_id: z.string().describe("The ID of a thread"),
767 | body: z.string().describe("The content of the note or reply"),
768 | created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
769 | });
770 |
771 | // Input schema for updating an issue discussion note
772 | export const UpdateIssueNoteSchema = ProjectParamsSchema.extend({
773 | issue_iid: z.number().describe("The IID of an issue"),
774 | discussion_id: z.string().describe("The ID of a thread"),
775 | note_id: z.number().describe("The ID of a thread note"),
776 | body: z.string().describe("The content of the note or reply"),
777 | });
778 |
779 | // Input schema for adding a note to an existing issue discussion
780 | export const CreateIssueNoteSchema = ProjectParamsSchema.extend({
781 | issue_iid: z.number().describe("The IID of an issue"),
782 | discussion_id: z.string().describe("The ID of a thread"),
783 | body: z.string().describe("The content of the note or reply"),
784 | created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
785 | });
786 |
787 | // API Operation Parameter Schemas
788 |
789 | export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
790 | file_path: z.string().describe("Path where to create/update the file"),
791 | content: z.string().describe("Content of the file"),
792 | commit_message: z.string().describe("Commit message"),
793 | branch: z.string().describe("Branch to create/update the file in"),
794 | previous_path: z.string().optional().describe("Path of the file to move/rename"),
795 | last_commit_id: z.string().optional().describe("Last known file commit ID"),
796 | commit_id: z.string().optional().describe("Current file commit ID (for update operations)"),
797 | });
798 |
799 | export const SearchRepositoriesSchema = z.object({
800 | search: z.string().describe("Search query"), // Changed from query to match GitLab API
801 | }).merge(PaginationOptionsSchema);
802 |
803 | export const CreateRepositorySchema = z.object({
804 | name: z.string().describe("Repository name"),
805 | description: z.string().optional().describe("Repository description"),
806 | visibility: z
807 | .enum(["private", "internal", "public"])
808 | .optional()
809 | .describe("Repository visibility level"),
810 | initialize_with_readme: z.boolean().optional().describe("Initialize with README.md"),
811 | });
812 |
813 | export const GetFileContentsSchema = ProjectParamsSchema.extend({
814 | file_path: z.string().describe("Path to the file or directory"),
815 | ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
816 | });
817 |
818 | export const PushFilesSchema = ProjectParamsSchema.extend({
819 | branch: z.string().describe("Branch to push to"),
820 | files: z
821 | .array(
822 | z.object({
823 | file_path: z.string().describe("Path where to create the file"),
824 | content: z.string().describe("Content of the file"),
825 | })
826 | )
827 | .describe("Array of files to push"),
828 | commit_message: z.string().describe("Commit message"),
829 | });
830 |
831 | export const CreateIssueSchema = ProjectParamsSchema.extend({
832 | title: z.string().describe("Issue title"),
833 | description: z.string().optional().describe("Issue description"),
834 | assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
835 | labels: z.array(z.string()).optional().describe("Array of label names"),
836 | milestone_id: z.number().optional().describe("Milestone ID to assign"),
837 | });
838 |
839 | export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
840 | title: z.string().describe("Merge request title"),
841 | description: z.string().optional().describe("Merge request description"),
842 | source_branch: z.string().describe("Branch containing changes"),
843 | target_branch: z.string().describe("Branch to merge into"),
844 | assignee_ids: z
845 | .array(z.number())
846 | .optional()
847 | .describe("The ID of the users to assign the MR to"),
848 | reviewer_ids: z
849 | .array(z.number())
850 | .optional()
851 | .describe("The ID of the users to assign as reviewers of the MR"),
852 | labels: z.array(z.string()).optional().describe("Labels for the MR"),
853 | draft: z.boolean().optional().describe("Create as draft merge request"),
854 | allow_collaboration: z
855 | .boolean()
856 | .optional()
857 | .describe("Allow commits from upstream members"),
858 | });
859 |
860 | export const ForkRepositorySchema = ProjectParamsSchema.extend({
861 | namespace: z.string().optional().describe("Namespace to fork to (full path)"),
862 | });
863 |
864 | // Branch related schemas
865 | export const CreateBranchSchema = ProjectParamsSchema.extend({
866 | branch: z.string().describe("Name for the new branch"),
867 | ref: z.string().optional().describe("Source branch/commit for new branch"),
868 | });
869 |
870 | export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
871 | from: z.string().describe("The base branch or commit SHA to compare from"),
872 | to: z.string().describe("The target branch or commit SHA to compare to"),
873 | straight: z.boolean().optional().describe("Comparison method: false for '...' (default), true for '--'"),
874 | excluded_file_patterns: z.array(z.string()).optional().describe(
875 | "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\"]"
876 | ),
877 | });
878 |
879 | export const GetMergeRequestSchema = ProjectParamsSchema.extend({
880 | merge_request_iid: z.number().optional().describe("The IID of a merge request"),
881 | source_branch: z.string().optional().describe("Source branch name"),
882 | });
883 |
884 | export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
885 | title: z.string().optional().describe("The title of the merge request"),
886 | description: z.string().optional().describe("The description of the merge request"),
887 | target_branch: z.string().optional().describe("The target branch"),
888 | assignee_ids: z.array(z.number()).optional().describe("The ID of the users to assign the MR to"),
889 | labels: z.array(z.string()).optional().describe("Labels for the MR"),
890 | state_event: z
891 | .enum(["close", "reopen"])
892 | .optional()
893 | .describe("New state (close/reopen) for the MR"),
894 | remove_source_branch: z
895 | .boolean()
896 | .optional()
897 | .describe("Flag indicating if the source branch should be removed"),
898 | squash: z.boolean().optional().describe("Squash commits into a single commit when merging"),
899 | draft: z.boolean().optional().describe("Work in progress merge request"),
900 | });
901 |
902 | export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
903 | view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
904 | });
905 |
906 | export const CreateNoteSchema = z.object({
907 | project_id: z.string().describe("Project ID or namespace/project_path"),
908 | noteable_type: z
909 | .enum(["issue", "merge_request"])
910 | .describe("Type of noteable (issue or merge_request)"),
911 | noteable_iid: z.number().describe("IID of the issue or merge request"),
912 | body: z.string().describe("Note content"),
913 | });
914 |
915 | // Issues API operation schemas
916 | export const ListIssuesSchema = z.object({
917 | project_id: z.string().describe("Project ID or URL-encoded path"),
918 | assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"),
919 | assignee_username: z.array(z.string()).optional().describe("Return issues assigned to the given username"),
920 | author_id: z.number().optional().describe("Return issues created by the given user ID"),
921 | author_username: z.string().optional().describe("Return issues created by the given username"),
922 | confidential: z.boolean().optional().describe("Filter confidential or public issues"),
923 | created_after: z.string().optional().describe("Return issues created after the given time"),
924 | created_before: z.string().optional().describe("Return issues created before the given time"),
925 | due_date: z.string().optional().describe("Return issues that have the due date"),
926 | labels: z.array(z.string()).optional().describe("Array of label names"),
927 | milestone: z.string().optional().describe("Milestone title"),
928 | scope: z
929 | .enum(["created_by_me", "assigned_to_me", "all"])
930 | .optional()
931 | .describe("Return issues from a specific scope"),
932 | search: z.string().optional().describe("Search for specific terms"),
933 | state: z
934 | .enum(["opened", "closed", "all"])
935 | .optional()
936 | .describe("Return issues with a specific state"),
937 | updated_after: z.string().optional().describe("Return issues updated after the given time"),
938 | updated_before: z.string().optional().describe("Return issues updated before the given time"),
939 | with_labels_details: z.boolean().optional().describe("Return more details for each label"),
940 | }).merge(PaginationOptionsSchema);
941 |
942 | // Merge Requests API operation schemas
943 | export const ListMergeRequestsSchema = z.object({
944 | project_id: z.string().describe("Project ID or URL-encoded path"),
945 | assignee_id: z
946 | .number()
947 | .optional()
948 | .describe("Returns merge requests assigned to the given user ID"),
949 | assignee_username: z
950 | .string()
951 | .optional()
952 | .describe("Returns merge requests assigned to the given username"),
953 | author_id: z.number().optional().describe("Returns merge requests created by the given user ID"),
954 | author_username: z
955 | .string()
956 | .optional()
957 | .describe("Returns merge requests created by the given username"),
958 | reviewer_id: z
959 | .number()
960 | .optional()
961 | .describe("Returns merge requests which have the user as a reviewer"),
962 | reviewer_username: z
963 | .string()
964 | .optional()
965 | .describe("Returns merge requests which have the user as a reviewer"),
966 | created_after: z
967 | .string()
968 | .optional()
969 | .describe("Return merge requests created after the given time"),
970 | created_before: z
971 | .string()
972 | .optional()
973 | .describe("Return merge requests created before the given time"),
974 | updated_after: z
975 | .string()
976 | .optional()
977 | .describe("Return merge requests updated after the given time"),
978 | updated_before: z
979 | .string()
980 | .optional()
981 | .describe("Return merge requests updated before the given time"),
982 | labels: z.array(z.string()).optional().describe("Array of label names"),
983 | milestone: z.string().optional().describe("Milestone title"),
984 | scope: z
985 | .enum(["created_by_me", "assigned_to_me", "all"])
986 | .optional()
987 | .describe("Return merge requests from a specific scope"),
988 | search: z.string().optional().describe("Search for specific terms"),
989 | state: z
990 | .enum(["opened", "closed", "locked", "merged", "all"])
991 | .optional()
992 | .describe("Return merge requests with a specific state"),
993 | order_by: z
994 | .enum(["created_at", "updated_at", "priority", "label_priority", "milestone_due", "popularity"])
995 | .optional()
996 | .describe("Return merge requests ordered by the given field"),
997 | sort: z
998 | .enum(["asc", "desc"])
999 | .optional()
1000 | .describe("Return merge requests sorted in ascending or descending order"),
1001 | target_branch: z
1002 | .string()
1003 | .optional()
1004 | .describe("Return merge requests targeting a specific branch"),
1005 | source_branch: z
1006 | .string()
1007 | .optional()
1008 | .describe("Return merge requests from a specific source branch"),
1009 | wip: z.enum(["yes", "no"]).optional().describe("Filter merge requests against their wip status"),
1010 | with_labels_details: z.boolean().optional().describe("Return more details for each label"),
1011 | }).merge(PaginationOptionsSchema);
1012 |
1013 | export const GetIssueSchema = z.object({
1014 | project_id: z.string().describe("Project ID or URL-encoded path"),
1015 | issue_iid: z.number().describe("The internal ID of the project issue"),
1016 | });
1017 |
1018 | export const UpdateIssueSchema = z.object({
1019 | project_id: z.string().describe("Project ID or URL-encoded path"),
1020 | issue_iid: z.number().describe("The internal ID of the project issue"),
1021 | title: z.string().optional().describe("The title of the issue"),
1022 | description: z.string().optional().describe("The description of the issue"),
1023 | assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"),
1024 | confidential: z.boolean().optional().describe("Set the issue to be confidential"),
1025 | discussion_locked: z.boolean().optional().describe("Flag to lock discussions"),
1026 | due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"),
1027 | labels: z.array(z.string()).optional().describe("Array of label names"),
1028 | milestone_id: z.number().optional().describe("Milestone ID to assign"),
1029 | state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"),
1030 | weight: z.number().optional().describe("Weight of the issue (0-9)"),
1031 | });
1032 |
1033 | export const DeleteIssueSchema = z.object({
1034 | project_id: z.string().describe("Project ID or URL-encoded path"),
1035 | issue_iid: z.number().describe("The internal ID of the project issue"),
1036 | });
1037 |
1038 | // Issue links related schemas
1039 | export const GitLabIssueLinkSchema = z.object({
1040 | source_issue: GitLabIssueSchema,
1041 | target_issue: GitLabIssueSchema,
1042 | link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]),
1043 | });
1044 |
1045 | export const ListIssueLinksSchema = z.object({
1046 | project_id: z.string().describe("Project ID or URL-encoded path"),
1047 | issue_iid: z.number().describe("The internal ID of a project's issue"),
1048 | });
1049 |
1050 | export const GetIssueLinkSchema = z.object({
1051 | project_id: z.string().describe("Project ID or URL-encoded path"),
1052 | issue_iid: z.number().describe("The internal ID of a project's issue"),
1053 | issue_link_id: z.number().describe("ID of an issue relationship"),
1054 | });
1055 |
1056 | export const CreateIssueLinkSchema = z.object({
1057 | project_id: z.string().describe("Project ID or URL-encoded path"),
1058 | issue_iid: z.number().describe("The internal ID of a project's issue"),
1059 | target_project_id: z.string().describe("The ID or URL-encoded path of a target project"),
1060 | target_issue_iid: z.number().describe("The internal ID of a target project's issue"),
1061 | link_type: z
1062 | .enum(["relates_to", "blocks", "is_blocked_by"])
1063 | .optional()
1064 | .describe("The type of the relation, defaults to relates_to"),
1065 | });
1066 |
1067 | export const DeleteIssueLinkSchema = z.object({
1068 | project_id: z.string().describe("Project ID or URL-encoded path"),
1069 | issue_iid: z.number().describe("The internal ID of a project's issue"),
1070 | issue_link_id: z.number().describe("The ID of an issue relationship"),
1071 | });
1072 |
1073 | // Namespace API operation schemas
1074 | export const ListNamespacesSchema = z.object({
1075 | search: z.string().optional().describe("Search term for namespaces"),
1076 | owned: z.boolean().optional().describe("Filter for namespaces owned by current user"),
1077 | }).merge(PaginationOptionsSchema);
1078 |
1079 | export const GetNamespaceSchema = z.object({
1080 | namespace_id: z.string().describe("Namespace ID or full path"),
1081 | });
1082 |
1083 | export const VerifyNamespaceSchema = z.object({
1084 | path: z.string().describe("Namespace path to verify"),
1085 | });
1086 |
1087 | // Project API operation schemas
1088 | export const GetProjectSchema = z.object({
1089 | project_id: z.string().describe("Project ID or URL-encoded path"),
1090 | });
1091 |
1092 | export const ListProjectsSchema = z.object({
1093 | search: z.string().optional().describe("Search term for projects"),
1094 | search_namespaces: z.boolean().optional().describe("Needs to be true if search is full path"),
1095 | owned: z.boolean().optional().describe("Filter for projects owned by current user"),
1096 | membership: z.boolean().optional().describe("Filter for projects where current user is a member"),
1097 | simple: z.boolean().optional().describe("Return only limited fields"),
1098 | archived: z.boolean().optional().describe("Filter for archived projects"),
1099 | visibility: z
1100 | .enum(["public", "internal", "private"])
1101 | .optional()
1102 | .describe("Filter by project visibility"),
1103 | order_by: z
1104 | .enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"])
1105 | .optional()
1106 | .describe("Return projects ordered by field"),
1107 | sort: z
1108 | .enum(["asc", "desc"])
1109 | .optional()
1110 | .describe("Return projects sorted in ascending or descending order"),
1111 | with_issues_enabled: z
1112 | .boolean()
1113 | .optional()
1114 | .describe("Filter projects with issues feature enabled"),
1115 | with_merge_requests_enabled: z
1116 | .boolean()
1117 | .optional()
1118 | .describe("Filter projects with merge requests feature enabled"),
1119 | min_access_level: z.number().optional().describe("Filter by minimum access level"),
1120 | }).merge(PaginationOptionsSchema);
1121 |
1122 | // Label operation schemas
1123 | export const ListLabelsSchema = z.object({
1124 | project_id: z.string().describe("Project ID or URL-encoded path"),
1125 | with_counts: z
1126 | .boolean()
1127 | .optional()
1128 | .describe("Whether or not to include issue and merge request counts"),
1129 | include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"),
1130 | search: z.string().optional().describe("Keyword to filter labels by"),
1131 | });
1132 |
1133 | export const GetLabelSchema = z.object({
1134 | project_id: z.string().describe("Project ID or URL-encoded path"),
1135 | label_id: z.string().describe("The ID or title of a project's label"),
1136 | include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"),
1137 | });
1138 |
1139 | export const CreateLabelSchema = z.object({
1140 | project_id: z.string().describe("Project ID or URL-encoded path"),
1141 | name: z.string().describe("The name of the label"),
1142 | color: z
1143 | .string()
1144 | .describe("The color of the label given in 6-digit hex notation with leading '#' sign"),
1145 | description: z.string().optional().describe("The description of the label"),
1146 | priority: z.number().nullable().optional().describe("The priority of the label"),
1147 | });
1148 |
1149 | export const UpdateLabelSchema = z.object({
1150 | project_id: z.string().describe("Project ID or URL-encoded path"),
1151 | label_id: z.string().describe("The ID or title of a project's label"),
1152 | new_name: z.string().optional().describe("The new name of the label"),
1153 | color: z
1154 | .string()
1155 | .optional()
1156 | .describe("The color of the label given in 6-digit hex notation with leading '#' sign"),
1157 | description: z.string().optional().describe("The new description of the label"),
1158 | priority: z.number().nullable().optional().describe("The new priority of the label"),
1159 | });
1160 |
1161 | export const DeleteLabelSchema = z.object({
1162 | project_id: z.string().describe("Project ID or URL-encoded path"),
1163 | label_id: z.string().describe("The ID or title of a project's label"),
1164 | });
1165 |
1166 | // Group projects schema
1167 | export const ListGroupProjectsSchema = z.object({
1168 | group_id: z.string().describe("Group ID or path"),
1169 | include_subgroups: z.boolean().optional().describe("Include projects from subgroups"),
1170 | search: z.string().optional().describe("Search term to filter projects"),
1171 | order_by: z
1172 | .enum(["name", "path", "created_at", "updated_at", "last_activity_at"])
1173 | .optional()
1174 | .describe("Field to sort by"),
1175 | sort: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
1176 | archived: z.boolean().optional().describe("Filter for archived projects"),
1177 | visibility: z
1178 | .enum(["public", "internal", "private"])
1179 | .optional()
1180 | .describe("Filter by project visibility"),
1181 | with_issues_enabled: z
1182 | .boolean()
1183 | .optional()
1184 | .describe("Filter projects with issues feature enabled"),
1185 | with_merge_requests_enabled: z
1186 | .boolean()
1187 | .optional()
1188 | .describe("Filter projects with merge requests feature enabled"),
1189 | min_access_level: z.number().optional().describe("Filter by minimum access level"),
1190 | with_programming_language: z.string().optional().describe("Filter by programming language"),
1191 | starred: z.boolean().optional().describe("Filter by starred projects"),
1192 | statistics: z.boolean().optional().describe("Include project statistics"),
1193 | with_custom_attributes: z.boolean().optional().describe("Include custom attributes"),
1194 | with_security_reports: z.boolean().optional().describe("Include security reports"),
1195 | }).merge(PaginationOptionsSchema);
1196 |
1197 | // Add wiki operation schemas
1198 | export const ListWikiPagesSchema = z.object({
1199 | project_id: z.string().describe("Project ID or URL-encoded path"),
1200 | with_content: z.boolean().optional().describe("Include content of the wiki pages"),
1201 | }).merge(PaginationOptionsSchema);
1202 |
1203 | export const GetWikiPageSchema = z.object({
1204 | project_id: z.string().describe("Project ID or URL-encoded path"),
1205 | slug: z.string().describe("URL-encoded slug of the wiki page"),
1206 | });
1207 | export const CreateWikiPageSchema = z.object({
1208 | project_id: z.string().describe("Project ID or URL-encoded path"),
1209 | title: z.string().describe("Title of the wiki page"),
1210 | content: z.string().describe("Content of the wiki page"),
1211 | format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
1212 | });
1213 | export const UpdateWikiPageSchema = z.object({
1214 | project_id: z.string().describe("Project ID or URL-encoded path"),
1215 | slug: z.string().describe("URL-encoded slug of the wiki page"),
1216 | title: z.string().optional().describe("New title of the wiki page"),
1217 | content: z.string().optional().describe("New content of the wiki page"),
1218 | format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
1219 | });
1220 |
1221 | export const DeleteWikiPageSchema = z.object({
1222 | project_id: z.string().describe("Project ID or URL-encoded path"),
1223 | slug: z.string().describe("URL-encoded slug of the wiki page"),
1224 | });
1225 |
1226 | // Define wiki response schemas
1227 | export const GitLabWikiPageSchema = z.object({
1228 | title: z.string(),
1229 | slug: z.string(),
1230 | format: z.string(),
1231 | content: z.string().optional(),
1232 | created_at: z.string().optional(),
1233 | updated_at: z.string().optional(),
1234 | });
1235 |
1236 | // Merge Request Thread position schema - used for diff notes
1237 | export const MergeRequestThreadPositionSchema = z.object({
1238 | base_sha: z.string().describe("Base commit SHA in the source branch"),
1239 | head_sha: z.string().describe("SHA referencing HEAD of the source branch"),
1240 | start_sha: z.string().describe("SHA referencing the start commit of the source branch"),
1241 | position_type: z.enum(["text", "image", "file"]).describe("Type of position reference"),
1242 | new_path: z.string().optional().describe("File path after change"),
1243 | old_path: z.string().optional().describe("File path before change"),
1244 | new_line: z.number().nullable().optional().describe("Line number after change"),
1245 | old_line: z.number().nullable().optional().describe("Line number before change"),
1246 | width: z.number().optional().describe("Width of the image (for image diffs)"),
1247 | height: z.number().optional().describe("Height of the image (for image diffs)"),
1248 | x: z.number().optional().describe("X coordinate on the image (for image diffs)"),
1249 | y: z.number().optional().describe("Y coordinate on the image (for image diffs)"),
1250 | });
1251 |
1252 | // Schema for creating a new merge request thread
1253 | export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
1254 | merge_request_iid: z.number().describe("The IID of a merge request"),
1255 | body: z.string().describe("The content of the thread"),
1256 | position: MergeRequestThreadPositionSchema.optional().describe(
1257 | "Position when creating a diff note"
1258 | ),
1259 | created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"),
1260 | });
1261 |
1262 | // Milestone related schemas
1263 | // Schema for listing project milestones
1264 | export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
1265 | iids: z.array(z.number()).optional().describe("Return only the milestones having the given iid"),
1266 | state: z
1267 | .enum(["active", "closed"])
1268 | .optional()
1269 | .describe("Return only active or closed milestones"),
1270 | title: z
1271 | .string()
1272 | .optional()
1273 | .describe("Return only milestones with a title matching the provided string"),
1274 | search: z
1275 | .string()
1276 | .optional()
1277 | .describe("Return only milestones with a title or description matching the provided string"),
1278 | include_ancestors: z.boolean().optional().describe("Include ancestor groups"),
1279 | updated_before: z
1280 | .string()
1281 | .optional()
1282 | .describe("Return milestones updated before the specified date (ISO 8601 format)"),
1283 | updated_after: z
1284 | .string()
1285 | .optional()
1286 | .describe("Return milestones updated after the specified date (ISO 8601 format)"),
1287 | }).merge(PaginationOptionsSchema);
1288 |
1289 | // Schema for getting a single milestone
1290 | export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({
1291 | milestone_id: z.number().describe("The ID of a project milestone"),
1292 | });
1293 |
1294 | // Schema for creating a new milestone
1295 | export const CreateProjectMilestoneSchema = ProjectParamsSchema.extend({
1296 | title: z.string().describe("The title of the milestone"),
1297 | description: z.string().optional().describe("The description of the milestone"),
1298 | due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
1299 | start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
1300 | });
1301 |
1302 | // Schema for editing a milestone
1303 | export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({
1304 | title: z.string().optional().describe("The title of the milestone"),
1305 | description: z.string().optional().describe("The description of the milestone"),
1306 | due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
1307 | start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
1308 | state_event: z
1309 | .enum(["close", "activate"])
1310 | .optional()
1311 | .describe("The state event of the milestone"),
1312 | });
1313 |
1314 | // Schema for deleting a milestone
1315 | export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema;
1316 |
1317 | // Schema for getting issues assigned to a milestone
1318 | export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema;
1319 |
1320 | // Schema for getting merge requests assigned to a milestone
1321 | export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
1322 |
1323 | // Schema for promoting a project milestone to a group milestone
1324 | export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
1325 |
1326 | // Schema for getting burndown chart events for a milestone
1327 | export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
1328 |
1329 | // Export types
1330 | export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
1331 | export type GitLabFork = z.infer<typeof GitLabForkSchema>;
1332 | export type GitLabIssue = z.infer<typeof GitLabIssueSchema>;
1333 | export type GitLabIssueWithLinkDetails = z.infer<typeof GitLabIssueWithLinkDetailsSchema>;
1334 | export type GitLabMergeRequest = z.infer<typeof GitLabMergeRequestSchema>;
1335 | export type GitLabRepository = z.infer<typeof GitLabRepositorySchema>;
1336 | export type GitLabFileContent = z.infer<typeof GitLabFileContentSchema>;
1337 | export type GitLabDirectoryContent = z.infer<typeof GitLabDirectoryContentSchema>;
1338 | export type GitLabContent = z.infer<typeof GitLabContentSchema>;
1339 | export type FileOperation = z.infer<typeof FileOperationSchema>;
1340 | export type GitLabTree = z.infer<typeof GitLabTreeSchema>;
1341 | export type GitLabCompareResult = z.infer<typeof GitLabCompareResultSchema>;
1342 | export type GitLabCommit = z.infer<typeof GitLabCommitSchema>;
1343 | export type GitLabReference = z.infer<typeof GitLabReferenceSchema>;
1344 | export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>;
1345 | export type CreateIssueOptions = z.infer<typeof CreateIssueOptionsSchema>;
1346 | export type CreateMergeRequestOptions = z.infer<typeof CreateMergeRequestOptionsSchema>;
1347 | export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>;
1348 | export type GitLabCreateUpdateFileResponse = z.infer<typeof GitLabCreateUpdateFileResponseSchema>;
1349 | export type GitLabSearchResponse = z.infer<typeof GitLabSearchResponseSchema>;
1350 | export type GitLabMergeRequestDiff = z.infer<
1351 | typeof GitLabDiffSchema
1352 | >;
1353 | export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>;
1354 | export type GitLabIssueLink = z.infer<typeof GitLabIssueLinkSchema>;
1355 | export type ListIssueDiscussionsOptions = z.infer<typeof ListIssueDiscussionsSchema>;
1356 | export type ListMergeRequestDiscussionsOptions = z.infer<typeof ListMergeRequestDiscussionsSchema>;
1357 | export type UpdateIssueNoteOptions = z.infer<typeof UpdateIssueNoteSchema>;
1358 | export type CreateIssueNoteOptions = z.infer<typeof CreateIssueNoteSchema>;
1359 | export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>;
1360 | export type GitLabNamespaceExistsResponse = z.infer<typeof GitLabNamespaceExistsResponseSchema>;
1361 | export type GitLabProject = z.infer<typeof GitLabProjectSchema>;
1362 | export type GitLabLabel = z.infer<typeof GitLabLabelSchema>;
1363 | export type ListWikiPagesOptions = z.infer<typeof ListWikiPagesSchema>;
1364 | export type GetWikiPageOptions = z.infer<typeof GetWikiPageSchema>;
1365 | export type CreateWikiPageOptions = z.infer<typeof CreateWikiPageSchema>;
1366 | export type UpdateWikiPageOptions = z.infer<typeof UpdateWikiPageSchema>;
1367 | export type DeleteWikiPageOptions = z.infer<typeof DeleteWikiPageSchema>;
1368 | export type GitLabWikiPage = z.infer<typeof GitLabWikiPageSchema>;
1369 | export type GitLabTreeItem = z.infer<typeof GitLabTreeItemSchema>;
1370 | export type GetRepositoryTreeOptions = z.infer<typeof GetRepositoryTreeSchema>;
1371 | export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>;
1372 | export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>;
1373 | export type CreateMergeRequestNoteOptions = z.infer<typeof CreateMergeRequestNoteSchema>;
1374 | export type GitLabPipelineJob = z.infer<typeof GitLabPipelineJobSchema>;
1375 | export type GitLabPipeline = z.infer<typeof GitLabPipelineSchema>;
1376 | export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>;
1377 | export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
1378 | export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>;
1379 | export type CreatePipelineOptions = z.infer<typeof CreatePipelineSchema>;
1380 | export type RetryPipelineOptions = z.infer<typeof RetryPipelineSchema>;
1381 | export type CancelPipelineOptions = z.infer<typeof CancelPipelineSchema>;
1382 | export type GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>;
1383 | export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>;
1384 | export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>;
1385 | export type CreateProjectMilestoneOptions = z.infer<typeof CreateProjectMilestoneSchema>;
1386 | export type EditProjectMilestoneOptions = z.infer<typeof EditProjectMilestoneSchema>;
1387 | export type DeleteProjectMilestoneOptions = z.infer<typeof DeleteProjectMilestoneSchema>;
1388 | export type GetMilestoneIssuesOptions = z.infer<typeof GetMilestoneIssuesSchema>;
1389 | export type GetMilestoneMergeRequestsOptions = z.infer<typeof GetMilestoneMergeRequestsSchema>;
1390 | export type PromoteProjectMilestoneOptions = z.infer<typeof PromoteProjectMilestoneSchema>;
1391 | export type GetMilestoneBurndownEventsOptions = z.infer<typeof GetMilestoneBurndownEventsSchema>;
1392 | // Time tracking schemas
1393 | export const TimeLogCreateSchema = z.object({
1394 | project_id: z.string().describe("Project ID or URL-encoded path"),
1395 | issue_iid: z.number().describe("The internal ID of the issue"),
1396 | duration: z.string().describe("The duration in GitLab format (e.g., '3h 30m')"),
1397 | spent_at: z.string().optional().describe("The date the time was spent (YYYY-MM-DD)"),
1398 | summary: z.string().optional().describe("A short description of the time spent")
1399 | });
1400 |
1401 | export const TimeLogDeleteSchema = z.object({
1402 | project_id: z.string().describe("Project ID or URL-encoded path"),
1403 | issue_iid: z.number().describe("The internal ID of the issue"),
1404 | time_log_id: z.number().describe("The ID of the time log to delete")
1405 | });
1406 |
1407 | export type TimeLogCreateOptions = z.infer<typeof TimeLogCreateSchema>;
1408 | export type TimeLogDeleteOptions = z.infer<typeof TimeLogDeleteSchema>;
1409 |
1410 | export type GitLabUser = z.infer<typeof GitLabUserSchema>;
1411 | export type GitLabUsersResponse = z.infer<typeof GitLabUsersResponseSchema>;
1412 | export type PaginationOptions = z.infer<typeof PaginationOptionsSchema>;
1413 |
```