#
tokens: 31489/50000 24/25 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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 | [![smithery badge](https://smithery.ai/badge/@zereight/gitlab-mcp)](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 | 
```
Page 1/2FirstPrevNextLast