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