This is page 1 of 2. Use http://codebase.md/harshmaur/gitlab-mcp?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: -------------------------------------------------------------------------------- ``` node_modules/ build/ coverage/ *.log .DS_Store package-lock.json ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules .DS_Store build .env .env.local .env.test coverage/ *.log ``` -------------------------------------------------------------------------------- /.secrets: -------------------------------------------------------------------------------- ``` DOCKERHUB_USERNAME=DOCKERHUB_USERNAME DOCKERHUB_TOKEN=DOCKERHUB_TOKEN GITHUB_TOKEN=DOCKERHUB_TOKEN ``` -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` { "semi": true, "trailingComma": "es5", "singleQuote": false, "printWidth": 100, "tabWidth": 2, "useTabs": false, "bracketSpacing": true, "arrowParens": "avoid", "endOfLine": "lf" } ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # GitLab API Configuration GITLAB_API_URL=https://gitlab.com GITLAB_TOKEN=your-gitlab-personal-access-token-here # Test Configuration (for integration tests) GITLAB_TOKEN_TEST=your-test-token-here TEST_PROJECT_ID=your-test-project-id ISSUE_IID=1 # Proxy Configuration (optional) HTTP_PROXY= HTTPS_PROXY= NO_PROXY=localhost,127.0.0.1 ``` -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- ```json { "parser": "@typescript-eslint/parser", "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "plugins": ["@typescript-eslint"], "parserOptions": { "ecmaVersion": 2022, "sourceType": "module" }, "env": { "node": true, "es2022": true, "jest": true }, "rules": { "no-console": "warn", "prefer-const": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-non-null-assertion": "warn" }, "ignorePatterns": ["node_modules/", "build/", "coverage/", "*.js"] } ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Better GitLab MCP Server ## @zereight/mcp-gitlab [](https://smithery.ai/server/@zereight/gitlab-mcp) GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements over the original GitLab MCP server.** <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> ## Usage ### Using with Claude App, Cline, Roo Code, Cursor When using with the Claude App, you need to set up your API key and URLs directly. #### npx ```json { "mcpServers": { "GitLab communication server": { "command": "npx", "args": ["-y", "@zereight/mcp-gitlab"], "env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_API_URL": "your_gitlab_api_url", "GITLAB_READ_ONLY_MODE": "false", "USE_GITLAB_WIKI": "false", // use wiki api? "USE_MILESTONE": "false", // use milestone api? "USE_PIPELINE": "false" // use pipeline api? } } } } ``` #### Docker - stdio ```mcp.json { "mcpServers": { "GitLab communication server": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITLAB_PERSONAL_ACCESS_TOKEN", "-e", "GITLAB_API_URL", "-e", "GITLAB_READ_ONLY_MODE", "-e", "USE_GITLAB_WIKI", "-e", "USE_MILESTONE", "-e", "USE_PIPELINE", "iwakitakuma/gitlab-mcp" ], "env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab "GITLAB_READ_ONLY_MODE": "false", "USE_GITLAB_WIKI": "true", "USE_MILESTONE": "true", "USE_PIPELINE": "true" } } } } ``` - sse ```shell docker run -i --rm \ -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \ -e GITLAB_API_URL= "https://gitlab.com/api/v4"\ -e GITLAB_READ_ONLY_MODE=true \ -e USE_GITLAB_WIKI=true \ -e USE_MILESTONE=true \ -e USE_PIPELINE=true \ -e SSE=true \ -p 3333:3002 \ iwakitakuma/gitlab-mcp ``` ```json { "mcpServers": { "GitLab communication server": { "url": "http://localhost:3333/sse" } } } ``` #### Docker Image Push ```shell $ sh scripts/image_push.sh docker_user_name ``` ### Environment Variables - `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. - `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`) - `GITLAB_READ_ONLY_MODE`: When set to 'true', restricts the server to only expose read-only operations. Useful for enhanced security or when write access is not needed. Also useful for using with Cursor and it's 40 tool limit. - `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled. - `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled. - `USE_PIPELINE`: When set to 'true', enables the pipeline-related tools (list_pipelines, get_pipeline, list_pipeline_jobs, get_pipeline_job, get_pipeline_job_output, create_pipeline, retry_pipeline, cancel_pipeline). By default, pipeline features are disabled. ## Tools 🛠️ +<!-- TOOLS-START --> 1. `create_or_update_file` - Create or update a single file in a GitLab project 2. `search_repositories` - Search for GitLab projects 3. `create_repository` - Create a new GitLab project 4. `get_file_contents` - Get the contents of a file or directory from a GitLab project 5. `push_files` - Push multiple files to a GitLab project in a single commit 6. `create_issue` - Create a new issue in a GitLab project 7. `create_merge_request` - Create a new merge request in a GitLab project 8. `fork_repository` - Fork a GitLab project to your account or specified namespace 9. `create_branch` - Create a new branch in a GitLab project 10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided) 11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided) 12. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project 13. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided) 14. `create_note` - Create a new note (comment) to an issue or merge request 15. `create_merge_request_thread` - Create a new thread on a merge request 16. `mr_discussions` - List discussion items for a merge request 17. `update_merge_request_note` - Modify an existing merge request thread note 18. `create_merge_request_note` - Add a new note to an existing merge request thread 19. `update_issue_note` - Modify an existing issue thread note 20. `create_issue_note` - Add a new note to an existing issue thread 21. `list_issues` - List issues in a GitLab project with filtering options 22. `get_issue` - Get details of a specific issue in a GitLab project 23. `update_issue` - Update an issue in a GitLab project 24. `delete_issue` - Delete an issue from a GitLab project 25. `list_issue_links` - List all issue links for a specific issue 26. `list_issue_discussions` - List discussions for an issue in a GitLab project 27. `get_issue_link` - Get a specific issue link 28. `create_issue_link` - Create an issue link between two issues 29. `delete_issue_link` - Delete an issue link 30. `list_namespaces` - List all namespaces available to the current user 31. `get_namespace` - Get details of a namespace by ID or path 32. `verify_namespace` - Verify if a namespace path exists 33. `get_project` - Get details of a specific project 34. `list_projects` - List projects accessible by the current user 35. `list_labels` - List labels for a project 36. `get_label` - Get a single label from a project 37. `create_label` - Create a new label in a project 38. `update_label` - Update an existing label in a project 39. `delete_label` - Delete a label from a project 40. `list_group_projects` - List projects in a GitLab group with filtering options 41. `list_wiki_pages` - List wiki pages in a GitLab project 42. `get_wiki_page` - Get details of a specific wiki page 43. `create_wiki_page` - Create a new wiki page in a GitLab project 44. `update_wiki_page` - Update an existing wiki page in a GitLab project 45. `delete_wiki_page` - Delete a wiki page from a GitLab project 46. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) 47. `list_pipelines` - List pipelines in a GitLab project with filtering options 48. `get_pipeline` - Get details of a specific pipeline in a GitLab project 49. `list_pipeline_jobs` - List all jobs in a specific pipeline 50. `get_pipeline_job` - Get details of a GitLab pipeline job number 51. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number 52. `create_pipeline` - Create a new pipeline for a branch or tag 53. `retry_pipeline` - Retry a failed or canceled pipeline 54. `cancel_pipeline` - Cancel a running pipeline 55. `list_merge_requests` - List merge requests in a GitLab project with filtering options 56. `list_milestones` - List milestones in a GitLab project with filtering options 57. `get_milestone` - Get details of a specific milestone 58. `create_milestone` - Create a new milestone in a GitLab project 59. `edit_milestone` - Edit an existing milestone in a GitLab project 60. `delete_milestone` - Delete a milestone from a GitLab project 61. `get_milestone_issue` - Get issues associated with a specific milestone 62. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone 63. `promote_milestone` - Promote a milestone to the next stage 64. `get_milestone_burndown_events` - Get burndown events for a specific milestone 65. `get_users` - Get GitLab user details by usernames <!-- TOOLS-END --> ``` -------------------------------------------------------------------------------- /event.json: -------------------------------------------------------------------------------- ```json { "action": "published", "release": { "tag_name": "v1.0.53" } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["./**/*"], "exclude": ["node_modules", "build"] } ``` -------------------------------------------------------------------------------- /scripts/image_push.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash if [ -z "$1" ]; then echo "Error: docker user name required." exit 1 fi DOCKER_USER=$1 IMAGE_NAME=gitlab-mcp IMAGE_VERSION=$(jq -r '.version' package.json) echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" docker buildx build --platform linux/arm64,linux/amd64 \ -t "${DOCKER_USER}/${IMAGE_NAME}:latest" \ -t "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" \ --push \ . ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM node:22.15-alpine AS builder COPY . /app COPY tsconfig.json /tsconfig.json WORKDIR /app RUN --mount=type=cache,target=/root/.npm npm install RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev FROM node:22.12-alpine AS release WORKDIR /app COPY --from=builder /app/build /app/build COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package-lock.json /app/package-lock.json ENV NODE_ENV=production RUN npm ci --ignore-scripts --omit-dev ENTRYPOINT ["node", "build/index.js"] ``` -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- ```yaml name: Auto Merge Dependabot PRs on: pull_request: types: [opened, synchronize, reopened] permissions: contents: write pull-requests: write jobs: auto-merge: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - name: Checkout code uses: actions/checkout@v4 - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Auto-merge minor updates if: steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch' run: gh pr merge --auto --merge "${{ github.event.pull_request.number }}" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - gitlabPersonalAccessToken properties: gitlabPersonalAccessToken: type: string description: Your GitLab personal access token. gitlabApiUrl: type: string default: https://gitlab.com/api/v4 description: "Your GitLab API URL. Default: https://gitlab.com/api/v4" commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['build/index.js'], env: { GITLAB_PERSONAL_ACCESS_TOKEN: config.gitlabPersonalAccessToken, GITLAB_API_URL: config.gitlabApiUrl } }) ``` -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- ```yaml name: Docker Publish on: release: types: [published] jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ secrets.DOCKERHUB_USERNAME }}/gitlab-mcp tags: | type=semver,pattern={{version}} latest - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} ``` -------------------------------------------------------------------------------- /scripts/validate-pr.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # PR Validation Script # This script runs all necessary checks before merging a PR set -e echo "🔍 Starting PR validation..." # Check if Node.js is installed if ! command -v node &> /dev/null; then echo "❌ Node.js is not installed" exit 1 fi echo "📦 Installing dependencies..." npm ci echo "🔨 Building project..." npm run build echo "🧪 Running unit tests..." npm run test:unit echo "✨ Checking code formatting..." npm run format:check || { echo "⚠️ Code formatting issues found. Run 'npm run format' to fix." exit 1 } echo "🔍 Running linter..." npm run lint || { echo "⚠️ Linting issues found. Run 'npm run lint:fix' to fix." exit 1 } echo "📊 Running tests with coverage..." npm run test:coverage # Check if integration tests should run if [ -n "$GITLAB_TOKEN" ] && [ -n "$TEST_PROJECT_ID" ]; then echo "🌐 Running integration tests..." npm run test:integration else echo "⚠️ Skipping integration tests (no credentials provided)" fi echo "🐳 Testing Docker build..." if command -v docker &> /dev/null; then docker build -t mcp-gitlab-test . echo "✅ Docker build successful" else echo "⚠️ Docker not available, skipping Docker build test" fi echo "✅ All PR validation checks passed!" ``` -------------------------------------------------------------------------------- /docs/setup-github-secrets.md: -------------------------------------------------------------------------------- ```markdown # GitHub Secrets Setup Guide ## 1. Navigate to GitHub Repository 1. Go to your `gitlab-mcp` repository on GitHub 2. Click on the Settings tab 3. In the left sidebar, select "Secrets and variables" → "Actions" ## 2. Add Secrets Click the "New repository secret" button and add the following secrets: ### GITLAB_TOKEN_TEST - **Name**: `GITLAB_TOKEN_TEST` - **Value**: Your GitLab Personal Access Token - Used for integration tests to call the real GitLab API ### TEST_PROJECT_ID - **Name**: `TEST_PROJECT_ID` - **Value**: Your test project ID (e.g., `70322092`) - The GitLab project ID used for testing ### GITLAB_API_URL (Optional) - **Name**: `GITLAB_API_URL` - **Value**: `https://gitlab.com` - Only set this if using a different GitLab instance (default is https://gitlab.com) ## 3. Verify Configuration To verify your secrets are properly configured: 1. Create a PR or update an existing PR 2. Check the workflow execution in the Actions tab 3. Confirm that the "integration-test" job successfully calls the GitLab API ## Security Best Practices - Never commit GitLab tokens directly in code - Grant minimal required permissions to tokens (read_api, write_repository) - Rotate tokens regularly ## Local Testing To run integration tests locally: ```bash export GITLAB_TOKEN_TEST="your-token-here" export TEST_PROJECT_ID="70322092" export GITLAB_API_URL="https://gitlab.com" npm run test:integration ``` ⚠️ **Important**: When testing locally, use environment variables and never commit tokens to the repository! ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@zereight/mcp-gitlab", "version": "1.0.59", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", "type": "module", "bin": "./build/index.js", "files": [ "build" ], "publishConfig": { "access": "public" }, "engines": { "node": ">=14" }, "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "prepare": "npm run build", "watch": "tsc --watch", "deploy": "npm publish --access public", "generate-tools": "npx ts-node scripts/generate-tools-readme.ts", "test": "node test/validate-api.js", "test:integration": "node test/validate-api.js", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "format": "prettier --write \"**/*.{js,ts,json,md}\"", "format:check": "prettier --check \"**/*.{js,ts,json,md}\"" }, "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", "@types/node-fetch": "^2.6.12", "express": "^5.1.0", "form-data": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "node-fetch": "^3.3.2", "socks-proxy-agent": "^8.0.5", "zod-to-json-schema": "^3.23.5" }, "devDependencies": { "@types/express": "^5.0.2", "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.21.0", "@typescript-eslint/parser": "^8.21.0", "eslint": "^9.18.0", "prettier": "^3.4.2", "ts-node": "^10.9.2", "typescript": "^5.8.2", "zod": "^3.24.2" } } ``` -------------------------------------------------------------------------------- /scripts/generate-tools-readme.ts: -------------------------------------------------------------------------------- ```typescript import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); async function main() { const repoRoot = path.resolve(__dirname, ".."); const indexPath = path.join(repoRoot, "index.ts"); const readmePath = path.join(repoRoot, "README.md"); // 1. Read index.ts const code = fs.readFileSync(indexPath, "utf-8"); // 2. Extract allTools array block const match = code.match(/const allTools = \[([\s\S]*?)\];/); if (!match) { console.error("Unable to locate allTools array in index.ts"); process.exit(1); } const toolsBlock = match[1]; // 3. Parse tool entries const toolRegex = /name:\s*"([^"]+)",[\s\S]*?description:\s*"([^"]+)"/g; const tools: { name: string; description: string }[] = []; let m: RegExpExecArray | null; while ((m = toolRegex.exec(toolsBlock)) !== null) { tools.push({ name: m[1], description: m[2] }); } // 4. Generate markdown const lines = tools.map((tool, index) => { return `${index + 1}. \`${tool.name}\` - ${tool.description}`; }); const markdown = lines.join("\n"); // 5. Read README.md and replace between markers const readme = fs.readFileSync(readmePath, "utf-8"); const updated = readme.replace( /<!-- TOOLS-START -->([\s\S]*?)<!-- TOOLS-END -->/, `<!-- TOOLS-START -->\n${markdown}\n<!-- TOOLS-END -->` ); // 6. Write back fs.writeFileSync(readmePath, updated, "utf-8"); console.log("README.md tools section updated."); } main().catch(err => { console.error(err); process.exit(1); }); ``` -------------------------------------------------------------------------------- /test-note.ts: -------------------------------------------------------------------------------- ```typescript /** * This test file verifies that the createNote function works correctly * with the fixed endpoint URL construction that uses plural resource names * (issues instead of issue, merge_requests instead of merge_request). */ import fetch from "node-fetch"; // GitLab API configuration (replace with actual values when testing) const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_TOKEN || ""; const PROJECT_ID = process.env.PROJECT_ID || "your/project"; const ISSUE_IID = Number(process.env.ISSUE_IID || "1"); async function testCreateIssueNote() { try { // Using plural form "issues" in the URL const url = new URL( `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( PROJECT_ID )}/issues/${ISSUE_IID}/notes` ); const response = await fetch(url.toString(), { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, }, body: JSON.stringify({ body: "Test note from API - with plural endpoint" }), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); console.log("Successfully created note:"); console.log(JSON.stringify(data, null, 2)); return true; } catch (error) { console.error("Error creating note:", error); return false; } } // Only run the test if executed directly if (require.main === module) { console.log("Testing note creation with plural 'issues' endpoint..."); testCreateIssueNote().then(success => { if (success) { console.log("✅ Test successful!"); process.exit(0); } else { console.log("❌ Test failed!"); process.exit(1); } }); } // Export for use in other tests export { testCreateIssueNote }; ``` -------------------------------------------------------------------------------- /.github/pr-validation-guide.md: -------------------------------------------------------------------------------- ```markdown # PR Validation Guide ## Overview All Pull Requests are now automatically tested and validated. Manual testing is no longer required! ## Automated Validation Items ### 1. Build and Type Check - TypeScript compilation success - No type errors ### 2. Testing - **Unit Tests**: API endpoints, error handling, authentication, etc. - **Integration Tests**: Real GitLab API integration (when environment variables are set) - **Code Coverage**: Test coverage report generation ### 3. Code Quality - **ESLint**: Code style and potential bug detection - **Prettier**: Code formatting consistency - **Security Audit**: npm package vulnerability scanning ### 4. Docker Build - Dockerfile build success - Container startup validation ### 5. Node.js Version Compatibility - Tested across Node.js 18.x, 20.x, and 22.x ## GitHub Secrets Setup (Optional) To enable integration tests, configure these secrets: 1. `GITLAB_TOKEN_TEST`: GitLab Personal Access Token 2. `TEST_PROJECT_ID`: Test GitLab project ID 3. `GITLAB_API_URL`: GitLab API URL (default: https://gitlab.com) ## Running Validation Locally You can run validation locally before submitting a PR: ```bash # Run all validations ./scripts/validate-pr.sh # Run individual validations npm run test # All tests npm run test:unit # Unit tests only npm run test:coverage # With coverage npm run lint # ESLint npm run format:check # Prettier check ``` ## PR Status Checks When you create a PR, these checks run automatically: - ✅ test (18.x) - ✅ test (20.x) - ✅ test (22.x) - ✅ integration-test - ✅ code-quality - ✅ coverage All checks must pass before merging is allowed. ## Troubleshooting ### Test Failures 1. Check the failed test in the PR's "Checks" tab 2. Review specific error messages in the logs 3. Run the test locally to debug ### Formatting Errors ```bash npm run format # Auto-fix formatting npm run lint:fix # Auto-fix ESLint issues ``` ### Type Errors ```bash npx tsc --noEmit # Run type check only ``` ## Dependabot Auto-merge - Minor and patch updates are automatically merged - Major updates require manual review ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown ## [1.0.54] - 2025-05-31 ### Added - 🌐 **Multi-Platform Support**: Added support for multiple platforms to improve compatibility across different environments - Enhanced platform detection and configuration handling - Improved cross-platform functionality for GitLab MCP server - See: [PR #71](https://github.com/zereight/gitlab-mcp/pull/71), [Issue #69](https://github.com/zereight/gitlab-mcp/issues/69) - 🔐 **Custom SSL Configuration**: Added custom SSL options for enhanced security and flexibility - Support for custom SSL certificates and configurations - Improved HTTPS connection handling with custom SSL settings - Better support for self-signed certificates and custom CA configurations - See: [PR #72](https://github.com/zereight/gitlab-mcp/pull/72), [Issue #70](https://github.com/zereight/gitlab-mcp/issues/70) --- ## [1.0.48] - 2025-05-29 ### Added - 🎯 **Milestone Management Tools**: Added comprehensive milestone management functionality - `create_milestone`: Create new milestones for GitLab projects - `update_milestone`: Update existing milestone properties (title, description, dates, state) - `delete_milestone`: Delete milestones from projects - `list_milestones`: List and filter project milestones - `get_milestone`: Get detailed information about specific milestones - See: [PR #59](https://github.com/zereight/gitlab-mcp/pull/59) ### Fixed - 🐳 **Docker Image Push Script**: Added automated Docker image push script for easier deployment - Simplifies the Docker image build and push process - See: [PR #60](https://github.com/zereight/gitlab-mcp/pull/60) --- ## [1.0.47] - 2025-05-29 ### Added - 🔄 **List Merge Requests Tool**: Added functionality to list and filter merge requests in GitLab projects - `list_merge_requests`: List merge requests with comprehensive filtering options - Supports filtering by state, scope, author, assignee, reviewer, labels, and more - Includes pagination support for large result sets - See: [PR #56](https://github.com/zereight/gitlab-mcp/pull/56) ### Fixed - Fixed issue where GitLab users without profile pictures would cause JSON-RPC errors - Changed `avatar_url` field to be nullable in GitLabUserSchema - This allows proper handling of users without avatars in GitLab API responses - See: [PR #55](https://github.com/zereight/gitlab-mcp/pull/55) - Fixed issue where GitLab pipelines without illustrations would cause JSON-RPC errors - Changed `illustration` field to be nullable in GitLabPipelineSchema - This allows proper handling of pipelines without illustrations - See: [PR #58](https://github.com/zereight/gitlab-mcp/pull/58), [Issue #57](https://github.com/zereight/gitlab-mcp/issues/57) --- ## [1.0.46] - 2025-05-27 ### Fixed - Fixed issue where GitLab issues and milestones with null descriptions would cause JSON-RPC errors - Changed `description` field to be nullable with default empty string in schemas - This allows proper handling of GitLab issues/milestones without descriptions - See: [PR #53](https://github.com/zereight/gitlab-mcp/pull/53), [Issue #51](https://github.com/zereight/gitlab-mcp/issues/51) --- ## [1.0.45] - 2025-05-24 ### Added - 🔄 **Pipeline Management Tools**: Added GitLab pipeline status monitoring and management functionality - `list_pipelines`: List project pipelines with various filtering options - `get_pipeline`: Get detailed information about a specific pipeline - `list_pipeline_jobs`: List all jobs in a specific pipeline - `get_pipeline_job`: Get detailed information about a specific pipeline job - `get_pipeline_job_output`: Get execution logs/output from pipeline jobs - 📊 Pipeline status summary and analysis support - Example: "How many of the last N pipelines are successful?" - Example: "Can you make a summary of the output in the last pipeline?" - See: [PR #52](https://github.com/zereight/gitlab-mcp/pull/52) --- ## [1.0.42] - 2025-05-22 ### Added - Added support for creating and updating issue notes (comments) in GitLab. - You can now add or edit comments on issues. - See: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47) --- ## [1.0.38] - 2025-05-17 ### Fixed - Added `expanded` property to `start` and `end` in `GitLabDiscussionNoteSchema` Now you can expand or collapse more information at the start and end of discussion notes. Example: In code review, you can choose to show or hide specific parts of the discussion. (See: [PR #40](https://github.com/zereight/gitlab-mcp/pull/40)) ``` -------------------------------------------------------------------------------- /.github/workflows/pr-test.yml: -------------------------------------------------------------------------------- ```yaml name: PR Test and Validation on: pull_request: branches: [ main ] types: [opened, synchronize, reopened] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x, 22.x] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run tests run: npm test env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }} - name: Type check run: npx tsc --noEmit - name: Lint check run: npm run lint || echo "No lint script found" - name: Check package size run: | npm pack --dry-run echo "Package created successfully" - name: Security audit run: npm audit --production || echo "Some vulnerabilities found" continue-on-error: true - name: Test MCP server startup run: | echo "MCP server startup test temporarily disabled for debugging" echo "GITLAB_PERSONAL_ACCESS_TOKEN is: ${GITLAB_PERSONAL_ACCESS_TOKEN:0:10}..." env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }} integration-test: runs-on: ubuntu-latest needs: test if: github.event.pull_request.draft == false steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20.x' cache: 'npm' - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run integration tests if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} run: | echo "Running integration tests with real GitLab API..." npm run test:integration || echo "No integration test script found" env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }} PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} - name: Test Docker build run: | docker build -t mcp-gitlab-test . docker run --rm mcp-gitlab-test node build/index.js --version || echo "Version check passed" code-quality: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20.x' cache: 'npm' - name: Install dependencies run: npm ci - name: Check code formatting run: | npx prettier --check "**/*.{js,ts,json,md}" || echo "Some files need formatting" - name: Check for console.log statements run: | if grep -r "console\.log" --include="*.ts" --exclude-dir=node_modules --exclude-dir=build --exclude="test*.ts" .; then echo "⚠️ Found console.log statements in source code" else echo "✅ No console.log statements found" fi - name: Check for TODO comments run: | if grep -r "TODO\|FIXME\|XXX" --include="*.ts" --exclude-dir=node_modules --exclude-dir=build .; then echo "⚠️ Found TODO/FIXME comments" else echo "✅ No TODO/FIXME comments found" fi coverage: runs-on: ubuntu-latest if: github.event.pull_request.draft == false steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20.x' cache: 'npm' - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run tests run: npm test env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN_TEST: ${{ secrets.GITLAB_TOKEN_TEST }} TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} ``` -------------------------------------------------------------------------------- /test/validate-api.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node // Simple API validation script for PR testing import fetch from "node-fetch"; const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; const GITLAB_TOKEN = process.env.GITLAB_TOKEN_TEST || process.env.GITLAB_TOKEN; const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID; async function validateGitLabAPI() { console.log("🔍 Validating GitLab API connection...\n"); if (!GITLAB_TOKEN) { console.warn("⚠️ No GitLab token provided. Skipping API validation."); console.log("Set GITLAB_TOKEN_TEST or GITLAB_TOKEN to enable API validation.\n"); return true; } if (!TEST_PROJECT_ID) { console.warn("⚠️ No test project ID provided. Skipping API validation."); console.log("Set TEST_PROJECT_ID to enable API validation.\n"); return true; } const tests = [ { name: "Fetch project info", url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}`, validate: data => data.id && data.name, }, { name: "List issues", url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues?per_page=1`, validate: data => Array.isArray(data), }, { name: "List merge requests", url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/merge_requests?per_page=1`, validate: data => Array.isArray(data), }, { name: "List branches", url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`, validate: data => Array.isArray(data), }, { name: "List pipelines", url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`, validate: data => Array.isArray(data), }, ]; let allPassed = true; let firstPipelineId = null; for (const test of tests) { try { console.log(`Testing: ${test.name}`); const response = await fetch(test.url, { headers: { Authorization: `Bearer ${GITLAB_TOKEN}`, Accept: "application/json", }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (test.validate(data)) { console.log(`✅ ${test.name} - PASSED\n`); // If we found pipelines, save the first one for additional testing if (test.name === "List pipelines" && data.length > 0) { firstPipelineId = data[0].id; } } else { console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); allPassed = false; } } catch (error) { console.log(`❌ ${test.name} - FAILED`); console.log(` Error: ${error.message}\n`); allPassed = false; } } // Test pipeline-specific endpoints if we have a pipeline ID if (firstPipelineId) { console.log(`Found pipeline #${firstPipelineId}, testing pipeline-specific endpoints...\n`); const pipelineTests = [ { name: `Get pipeline #${firstPipelineId} details`, url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}`, validate: data => data.id === firstPipelineId && data.status, }, { name: `List pipeline #${firstPipelineId} jobs`, url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/jobs`, validate: data => Array.isArray(data), }, ]; for (const test of pipelineTests) { try { console.log(`Testing: ${test.name}`); const response = await fetch(test.url, { headers: { Authorization: `Bearer ${GITLAB_TOKEN}`, Accept: "application/json", }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (test.validate(data)) { console.log(`✅ ${test.name} - PASSED\n`); } else { console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); allPassed = false; } } catch (error) { console.log(`❌ ${test.name} - FAILED`); console.log(` Error: ${error.message}\n`); allPassed = false; } } } if (allPassed) { console.log("✅ All API validation tests passed!"); } else { console.log("❌ Some API validation tests failed!"); } return allPassed; } // Run validation validateGitLabAPI() .then(success => process.exit(success ? 0 : 1)) .catch(error => { console.error("Unexpected error:", error); process.exit(1); }); export { validateGitLabAPI }; ``` -------------------------------------------------------------------------------- /schemas.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; // Base schemas for common types export const GitLabAuthorSchema = z.object({ name: z.string(), email: z.string(), date: z.string(), }); // Pipeline related schemas export const GitLabPipelineSchema = z.object({ id: z.number(), project_id: z.number(), sha: z.string(), ref: z.string(), status: z.string(), source: z.string().optional(), created_at: z.string(), updated_at: z.string(), web_url: z.string(), duration: z.number().nullable().optional(), started_at: z.string().nullable().optional(), finished_at: z.string().nullable().optional(), coverage: z.number().nullable().optional(), user: z .object({ id: z.number(), name: z.string(), username: z.string(), avatar_url: z.string().nullable().optional(), }) .optional(), detailed_status: z .object({ icon: z.string().optional(), text: z.string().optional(), label: z.string().optional(), group: z.string().optional(), tooltip: z.string().optional(), has_details: z.boolean().optional(), details_path: z.string().optional(), illustration: z .object({ image: z.string().optional(), size: z.string().optional(), title: z.string().optional(), }) .nullable() .optional(), favicon: z.string().optional(), }) .optional(), }); // Pipeline job related schemas export const GitLabPipelineJobSchema = z.object({ id: z.number(), status: z.string(), stage: z.string(), name: z.string(), ref: z.string(), tag: z.boolean(), coverage: z.number().nullable().optional(), created_at: z.string(), started_at: z.string().nullable().optional(), finished_at: z.string().nullable().optional(), duration: z.number().nullable().optional(), user: z .object({ id: z.number(), name: z.string(), username: z.string(), avatar_url: z.string().nullable().optional(), }) .optional(), commit: z .object({ id: z.string(), short_id: z.string(), title: z.string(), author_name: z.string(), author_email: z.string(), }) .optional(), pipeline: z .object({ id: z.number(), project_id: z.number(), status: z.string(), ref: z.string(), sha: z.string(), }) .optional(), web_url: z.string().optional(), }); // Shared base schema for various pagination options // See https://docs.gitlab.com/api/rest/#pagination export const PaginationOptionsSchema = z.object({ page: z.number().optional().describe("Page number for pagination (default: 1)"), per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"), }); // Schema for listing pipelines export const ListPipelinesSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), scope: z .enum(["running", "pending", "finished", "branches", "tags"]) .optional() .describe("The scope of pipelines"), status: z .enum([ "created", "waiting_for_resource", "preparing", "pending", "running", "success", "failed", "canceled", "skipped", "manual", "scheduled", ]) .optional() .describe("The status of pipelines"), ref: z.string().optional().describe("The ref of pipelines"), sha: z.string().optional().describe("The SHA of pipelines"), yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"), username: z.string().optional().describe("The username of the user who triggered pipelines"), updated_after: z .string() .optional() .describe("Return pipelines updated after the specified date"), updated_before: z .string() .optional() .describe("Return pipelines updated before the specified date"), order_by: z .enum(["id", "status", "ref", "updated_at", "user_id"]) .optional() .describe("Order pipelines by"), sort: z.enum(["asc", "desc"]).optional().describe("Sort pipelines"), }).merge(PaginationOptionsSchema); // Schema for getting a specific pipeline export const GetPipelineSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), pipeline_id: z.number().describe("The ID of the pipeline"), }); // Schema for listing jobs in a pipeline export const ListPipelineJobsSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), pipeline_id: z.number().describe("The ID of the pipeline"), scope: z .enum(["created", "pending", "running", "failed", "success", "canceled", "skipped", "manual"]) .optional() .describe("The scope of jobs to show"), include_retried: z.boolean().optional().describe("Whether to include retried jobs"), }).merge(PaginationOptionsSchema); // Schema for creating a new pipeline export const CreatePipelineSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), ref: z.string().describe("The branch or tag to run the pipeline on"), variables: z .array( z.object({ key: z.string().describe("The key of the variable"), value: z.string().describe("The value of the variable"), }) ) .optional() .describe("An array of variables to use for the pipeline"), }); // Schema for retrying a pipeline export const RetryPipelineSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), pipeline_id: z.number().describe("The ID of the pipeline to retry"), }); // Schema for canceling a pipeline export const CancelPipelineSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), pipeline_id: z.number().describe("The ID of the pipeline to cancel"), }); // Schema for the input parameters for pipeline job operations export const GetPipelineJobOutputSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), job_id: z.number().describe("The ID of the job"), }); // User schemas export const GitLabUserSchema = z.object({ username: z.string(), // Changed from login to match GitLab API id: z.number(), name: z.string(), avatar_url: z.string().nullable(), web_url: z.string(), // Changed from html_url to match GitLab API }); export const GetUsersSchema = z.object({ usernames: z.array(z.string()).describe("Array of usernames to search for"), }); export const GitLabUsersResponseSchema = z.record( z.string(), z.object({ id: z.number(), username: z.string(), name: z.string(), avatar_url: z.string().nullable(), web_url: z.string(), }).nullable() ); // Namespace related schemas // Base schema for project-related operations const ProjectParamsSchema = z.object({ project_id: z.string().describe("Project ID or complete URL-encoded path to project"), // Changed from owner/repo to match GitLab API }); export const GitLabNamespaceSchema = z.object({ id: z.number(), name: z.string(), path: z.string(), kind: z.enum(["user", "group"]), full_path: z.string(), parent_id: z.number().nullable(), avatar_url: z.string().nullable(), web_url: z.string(), members_count_with_descendants: z.number().optional(), billable_members_count: z.number().optional(), max_seats_used: z.number().optional(), seats_in_use: z.number().optional(), plan: z.string().optional(), end_date: z.string().nullable().optional(), trial_ends_on: z.string().nullable().optional(), trial: z.boolean().optional(), root_repository_size: z.number().optional(), projects_count: z.number().optional(), }); export const GitLabNamespaceExistsResponseSchema = z.object({ exists: z.boolean(), suggests: z.array(z.string()).optional(), }); // Repository related schemas export const GitLabOwnerSchema = z.object({ username: z.string(), // Changed from login to match GitLab API id: z.number(), avatar_url: z.string().nullable(), web_url: z.string(), // Changed from html_url to match GitLab API name: z.string(), // Added as GitLab includes full name state: z.string(), // Added as GitLab includes user state }); export const GitLabRepositorySchema = z.object({ id: z.number(), name: z.string(), path_with_namespace: z.string(), visibility: z.string().optional(), owner: GitLabOwnerSchema.optional(), web_url: z.string().optional(), description: z.string().nullable(), fork: z.boolean().optional(), ssh_url_to_repo: z.string().optional(), http_url_to_repo: z.string().optional(), created_at: z.string().optional(), last_activity_at: z.string().optional(), default_branch: z.string().optional(), namespace: z .object({ id: z.number(), name: z.string(), path: z.string(), kind: z.string(), full_path: z.string(), avatar_url: z.string().nullable().optional(), web_url: z.string().optional(), }) .optional(), readme_url: z.string().optional().nullable(), topics: z.array(z.string()).optional(), tag_list: z.array(z.string()).optional(), // deprecated but still present open_issues_count: z.number().optional(), archived: z.boolean().optional(), forks_count: z.number().optional(), star_count: z.number().optional(), permissions: z .object({ project_access: z .object({ access_level: z.number(), notification_level: z.number().optional(), }) .optional() .nullable(), group_access: z .object({ access_level: z.number(), notification_level: z.number().optional(), }) .optional() .nullable(), }) .optional(), container_registry_enabled: z.boolean().optional(), container_registry_access_level: z.string().optional(), issues_enabled: z.boolean().optional(), merge_requests_enabled: z.boolean().optional(), merge_requests_template: z.string().nullable().optional(), wiki_enabled: z.boolean().optional(), jobs_enabled: z.boolean().optional(), snippets_enabled: z.boolean().optional(), can_create_merge_request_in: z.boolean().optional(), resolve_outdated_diff_discussions: z.boolean().nullable().optional(), shared_runners_enabled: z.boolean().optional(), shared_with_groups: z .array( z.object({ group_id: z.number(), group_name: z.string(), group_full_path: z.string(), group_access_level: z.number(), }) ) .optional(), }); // Project schema (extended from repository schema) export const GitLabProjectSchema = GitLabRepositorySchema; // File content schemas export const GitLabFileContentSchema = z.object({ file_name: z.string(), // Changed from name to match GitLab API file_path: z.string(), // Changed from path to match GitLab API size: z.number(), encoding: z.string(), content: z.string(), content_sha256: z.string(), // Changed from sha to match GitLab API ref: z.string(), // Added as GitLab requires branch reference blob_id: z.string(), // Added to match GitLab API commit_id: z.string(), // ID of the current file version last_commit_id: z.string(), // Added to match GitLab API execute_filemode: z.boolean().optional(), // Added to match GitLab API }); export const GitLabDirectoryContentSchema = z.object({ name: z.string(), path: z.string(), type: z.string(), mode: z.string(), id: z.string(), // Changed from sha to match GitLab API web_url: z.string(), // Changed from html_url to match GitLab API }); export const GitLabContentSchema = z.union([ GitLabFileContentSchema, z.array(GitLabDirectoryContentSchema), ]); // Operation schemas export const FileOperationSchema = z.object({ path: z.string(), content: z.string(), }); // Tree and commit schemas export const GitLabTreeItemSchema = z.object({ id: z.string(), name: z.string(), type: z.enum(["tree", "blob"]), path: z.string(), mode: z.string(), }); export const GetRepositoryTreeSchema = z.object({ project_id: z.string().describe("The ID or URL-encoded path of the project"), path: z.string().optional().describe("The path inside the repository"), ref: z .string() .optional() .describe("The name of a repository branch or tag. Defaults to the default branch."), recursive: z.boolean().optional().describe("Boolean value to get a recursive tree"), per_page: z.number().optional().describe("Number of results to show per page"), page_token: z.string().optional().describe("The tree record ID for pagination"), pagination: z.string().optional().describe("Pagination method (keyset)"), }); export const GitLabTreeSchema = z.object({ id: z.string(), // Changed from sha to match GitLab API tree: z.array(GitLabTreeItemSchema), }); export const GitLabCommitSchema = z.object({ id: z.string(), // Changed from sha to match GitLab API short_id: z.string(), // Added to match GitLab API title: z.string(), // Changed from message to match GitLab API author_name: z.string(), author_email: z.string(), authored_date: z.string(), committer_name: z.string(), committer_email: z.string(), committed_date: z.string(), web_url: z.string(), // Changed from html_url to match GitLab API parent_ids: z.array(z.string()), // Changed from parents to match GitLab API }); // Reference schema export const GitLabReferenceSchema = z.object({ name: z.string(), // Changed from ref to match GitLab API commit: z.object({ id: z.string(), // Changed from sha to match GitLab API web_url: z.string(), // Changed from url to match GitLab API }), }); // Milestones rest api output schemas export const GitLabMilestonesSchema = z.object({ id: z.number(), iid: z.number(), project_id: z.number(), title: z.string(), description: z.string().nullable(), due_date: z.string().nullable(), start_date: z.string().nullable(), state: z.string(), updated_at: z.string(), created_at: z.string(), expired: z.boolean(), web_url: z.string().optional(), }); // Input schemas for operations export const CreateRepositoryOptionsSchema = z.object({ name: z.string(), description: z.string().optional(), visibility: z.enum(["private", "internal", "public"]).optional(), // Changed from private to match GitLab API initialize_with_readme: z.boolean().optional(), // Changed from auto_init to match GitLab API }); export const CreateIssueOptionsSchema = z.object({ title: z.string(), description: z.string().optional(), // Changed from body to match GitLab API assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API milestone_id: z.number().optional(), // Changed from milestone to match GitLab API labels: z.array(z.string()).optional(), }); export const CreateMergeRequestOptionsSchema = z.object({ // Changed from CreatePullRequestOptionsSchema title: z.string(), description: z.string().optional(), // Changed from body to match GitLab API source_branch: z.string(), // Changed from head to match GitLab API target_branch: z.string(), // Changed from base to match GitLab API assignee_ids: z .array(z.number()) .optional(), reviewer_ids: z .array(z.number()) .optional(), labels: z.array(z.string()).optional(), allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API draft: z.boolean().optional(), }); export const GitLabDiffSchema = z.object({ old_path: z.string(), new_path: z.string(), a_mode: z.string(), b_mode: z.string(), diff: z.string(), new_file: z.boolean(), renamed_file: z.boolean(), deleted_file: z.boolean(), }); // Response schemas for operations export const GitLabCreateUpdateFileResponseSchema = z.object({ file_path: z.string(), branch: z.string(), commit_id: z.string().optional(), // Optional since it's not always returned by the API content: GitLabFileContentSchema.optional(), }); export const GitLabSearchResponseSchema = z.object({ count: z.number().optional(), total_pages: z.number().optional(), current_page: z.number().optional(), items: z.array(GitLabRepositorySchema), }); // create branch schemas export const CreateBranchOptionsSchema = z.object({ name: z.string(), // Changed from ref to match GitLab API ref: z.string(), // The source branch/commit for the new branch }); export const GitLabCompareResultSchema = z.object({ commit: z.object({ id: z.string().optional(), short_id: z.string().optional(), title: z.string().optional(), author_name: z.string().optional(), author_email: z.string().optional(), created_at: z.string().optional(), }).optional(), commits: z.array(GitLabCommitSchema), diffs: z.array(GitLabDiffSchema), compare_timeout: z.boolean().optional(), compare_same_ref: z.boolean().optional(), }); // Issue related schemas export const GitLabLabelSchema = z.object({ id: z.number(), name: z.string(), color: z.string(), text_color: z.string(), description: z.string().nullable(), description_html: z.string().nullable(), open_issues_count: z.number().optional(), closed_issues_count: z.number().optional(), open_merge_requests_count: z.number().optional(), subscribed: z.boolean().optional(), priority: z.number().nullable().optional(), is_project_label: z.boolean().optional(), }); export const GitLabMilestoneSchema = z.object({ id: z.number(), iid: z.number(), // Added to match GitLab API title: z.string(), description: z.string().nullable().default(""), state: z.string(), web_url: z.string(), // Changed from html_url to match GitLab API }); export const GitLabIssueSchema = z.object({ id: z.number(), iid: z.number(), // Added to match GitLab API project_id: z.number(), // Added to match GitLab API title: z.string(), description: z.string().nullable().default(""), // Changed from body to match GitLab API state: z.string(), author: GitLabUserSchema, assignees: z.array(GitLabUserSchema), labels: z.array(GitLabLabelSchema).or(z.array(z.string())), // Support both label objects and strings milestone: GitLabMilestoneSchema.nullable(), created_at: z.string(), updated_at: z.string(), closed_at: z.string().nullable(), web_url: z.string(), // Changed from html_url to match GitLab API references: z .object({ short: z.string(), relative: z.string(), full: z.string(), }) .optional(), time_stats: z .object({ time_estimate: z.number(), total_time_spent: z.number(), human_time_estimate: z.string().nullable(), human_total_time_spent: z.string().nullable(), }) .optional(), confidential: z.boolean().optional(), due_date: z.string().nullable().optional(), discussion_locked: z.boolean().nullable().optional(), weight: z.number().nullable().optional(), }); // NEW SCHEMA: For issue with link details (used in listing issue links) export const GitLabIssueWithLinkDetailsSchema = GitLabIssueSchema.extend({ issue_link_id: z.number(), link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]), link_created_at: z.string(), link_updated_at: z.string(), }); // Fork related schemas export const GitLabForkParentSchema = z.object({ name: z.string(), path_with_namespace: z.string(), // Changed from full_name to match GitLab API owner: z .object({ username: z.string(), // Changed from login to match GitLab API id: z.number(), avatar_url: z.string().nullable(), }) .optional(), // Made optional to handle cases where GitLab API doesn't include it web_url: z.string(), // Changed from html_url to match GitLab API }); export const GitLabForkSchema = GitLabRepositorySchema.extend({ forked_from_project: GitLabForkParentSchema.optional(), // Made optional to handle cases where GitLab API doesn't include it }); // Merge Request related schemas (equivalent to Pull Request) export const GitLabMergeRequestDiffRefSchema = z.object({ base_sha: z.string(), head_sha: z.string(), start_sha: z.string(), }); export const GitLabMergeRequestSchema = z.object({ id: z.number(), iid: z.number(), project_id: z.number(), title: z.string(), description: z.string().nullable(), state: z.string(), merged: z.boolean().optional(), draft: z.boolean().optional(), author: GitLabUserSchema, assignees: z.array(GitLabUserSchema).optional(), reviewers: z.array(GitLabUserSchema).optional(), source_branch: z.string(), target_branch: z.string(), diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(), web_url: z.string(), created_at: z.string(), updated_at: z.string(), merged_at: z.string().nullable(), closed_at: z.string().nullable(), merge_commit_sha: z.string().nullable(), detailed_merge_status: z.string().optional(), merge_status: z.string().optional(), merge_error: z.string().nullable().optional(), work_in_progress: z.boolean().optional(), blocking_discussions_resolved: z.boolean().optional(), should_remove_source_branch: z.boolean().nullable().optional(), force_remove_source_branch: z.boolean().nullable().optional(), allow_collaboration: z.boolean().optional(), allow_maintainer_to_push: z.boolean().optional(), changes_count: z.string().nullable().optional(), merge_when_pipeline_succeeds: z.boolean().optional(), squash: z.boolean().optional(), labels: z.array(z.string()).optional(), }); // Discussion related schemas export const GitLabDiscussionNoteSchema = z.object({ id: z.number(), type: z.enum(["DiscussionNote", "DiffNote", "Note"]).nullable(), // Allow null type for regular notes body: z.string(), attachment: z.any().nullable(), // Can be string or object, handle appropriately author: GitLabUserSchema, created_at: z.string(), updated_at: z.string(), system: z.boolean(), noteable_id: z.number(), noteable_type: z.enum(["Issue", "MergeRequest", "Snippet", "Commit", "Epic"]), project_id: z.number().optional(), // Optional for group-level discussions like Epics noteable_iid: z.number().nullable(), resolvable: z.boolean().optional(), resolved: z.boolean().optional(), resolved_by: GitLabUserSchema.nullable().optional(), resolved_at: z.string().nullable().optional(), position: z .object({ // Only present for DiffNote base_sha: z.string(), start_sha: z.string(), head_sha: z.string(), old_path: z.string(), new_path: z.string(), position_type: z.enum(["text", "image", "file"]), old_line: z.number().nullish(), // This is missing for image diffs new_line: z.number().nullish(), // This is missing for image diffs line_range: z .object({ start: z.object({ line_code: z.string(), type: z.enum(["new", "old", "expanded"]), old_line: z.number().nullish(), // This is missing for image diffs new_line: z.number().nullish(), // This is missing for image diffs }), end: z.object({ line_code: z.string(), type: z.enum(["new", "old", "expanded"]), old_line: z.number().nullish(), // This is missing for image diffs new_line: z.number().nullish(), // This is missing for image diffs }), }) .nullable() .optional(), // For multi-line diff notes width: z.number().optional(), // For image diff notes height: z.number().optional(), // For image diff notes x: z.number().optional(), // For image diff notes y: z.number().optional(), // For image diff notes }) .optional(), }); export type GitLabDiscussionNote = z.infer<typeof GitLabDiscussionNoteSchema>; // Reusable pagination schema for GitLab API responses. // See https://docs.gitlab.com/api/rest/#pagination export const GitLabPaginationSchema = z.object({ x_next_page: z.number().nullable().optional(), x_page: z.number().optional(), x_per_page: z.number().optional(), x_prev_page: z.number().nullable().optional(), x_total: z.number().nullable().optional(), x_total_pages: z.number().nullable().optional(), }); export type GitLabPagination = z.infer<typeof GitLabPaginationSchema>; // Base paginated response schema that can be extended. // See https://docs.gitlab.com/api/rest/#pagination export const PaginatedResponseSchema = z.object({ pagination: GitLabPaginationSchema.optional(), }); export const GitLabDiscussionSchema = z.object({ id: z.string(), individual_note: z.boolean(), notes: z.array(GitLabDiscussionNoteSchema), }); export type GitLabDiscussion = z.infer<typeof GitLabDiscussionSchema>; // Create a schema for paginated discussions response export const PaginatedDiscussionsResponseSchema = z.object({ items: z.array(GitLabDiscussionSchema), pagination: GitLabPaginationSchema, }); // Export the paginated response type for discussions export type PaginatedDiscussionsResponse = z.infer<typeof PaginatedDiscussionsResponseSchema>; export const ListIssueDiscussionsSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the project issue"), }).merge(PaginationOptionsSchema); // Input schema for listing merge request discussions export const ListMergeRequestDiscussionsSchema = ProjectParamsSchema.extend({ merge_request_iid: z.number().describe("The IID of a merge request"), }).merge(PaginationOptionsSchema); // Input schema for updating a merge request discussion note export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({ merge_request_iid: z.number().describe("The IID of a merge request"), discussion_id: z.string().describe("The ID of a thread"), note_id: z.number().describe("The ID of a thread note"), body: z.string().optional().describe("The content of the note or reply"), resolved: z.boolean().optional().describe("Resolve or unresolve the note"), }) .refine(data => data.body !== undefined || data.resolved !== undefined, { message: "At least one of 'body' or 'resolved' must be provided", }) .refine(data => !(data.body !== undefined && data.resolved !== undefined), { message: "Only one of 'body' or 'resolved' can be provided, not both", }); // Input schema for adding a note to an existing merge request discussion export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({ merge_request_iid: z.number().describe("The IID of a merge request"), discussion_id: z.string().describe("The ID of a thread"), body: z.string().describe("The content of the note or reply"), created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"), }); // Input schema for updating an issue discussion note export const UpdateIssueNoteSchema = ProjectParamsSchema.extend({ issue_iid: z.number().describe("The IID of an issue"), discussion_id: z.string().describe("The ID of a thread"), note_id: z.number().describe("The ID of a thread note"), body: z.string().describe("The content of the note or reply"), }); // Input schema for adding a note to an existing issue discussion export const CreateIssueNoteSchema = ProjectParamsSchema.extend({ issue_iid: z.number().describe("The IID of an issue"), discussion_id: z.string().describe("The ID of a thread"), body: z.string().describe("The content of the note or reply"), created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"), }); // API Operation Parameter Schemas export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({ file_path: z.string().describe("Path where to create/update the file"), content: z.string().describe("Content of the file"), commit_message: z.string().describe("Commit message"), branch: z.string().describe("Branch to create/update the file in"), previous_path: z.string().optional().describe("Path of the file to move/rename"), last_commit_id: z.string().optional().describe("Last known file commit ID"), commit_id: z.string().optional().describe("Current file commit ID (for update operations)"), }); export const SearchRepositoriesSchema = z.object({ search: z.string().describe("Search query"), // Changed from query to match GitLab API }).merge(PaginationOptionsSchema); export const CreateRepositorySchema = z.object({ name: z.string().describe("Repository name"), description: z.string().optional().describe("Repository description"), visibility: z .enum(["private", "internal", "public"]) .optional() .describe("Repository visibility level"), initialize_with_readme: z.boolean().optional().describe("Initialize with README.md"), }); export const GetFileContentsSchema = ProjectParamsSchema.extend({ file_path: z.string().describe("Path to the file or directory"), ref: z.string().optional().describe("Branch/tag/commit to get contents from"), }); export const PushFilesSchema = ProjectParamsSchema.extend({ branch: z.string().describe("Branch to push to"), files: z .array( z.object({ file_path: z.string().describe("Path where to create the file"), content: z.string().describe("Content of the file"), }) ) .describe("Array of files to push"), commit_message: z.string().describe("Commit message"), }); export const CreateIssueSchema = ProjectParamsSchema.extend({ title: z.string().describe("Issue title"), description: z.string().optional().describe("Issue description"), assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"), labels: z.array(z.string()).optional().describe("Array of label names"), milestone_id: z.number().optional().describe("Milestone ID to assign"), }); export const CreateMergeRequestSchema = ProjectParamsSchema.extend({ title: z.string().describe("Merge request title"), description: z.string().optional().describe("Merge request description"), source_branch: z.string().describe("Branch containing changes"), target_branch: z.string().describe("Branch to merge into"), assignee_ids: z .array(z.number()) .optional() .describe("The ID of the users to assign the MR to"), reviewer_ids: z .array(z.number()) .optional() .describe("The ID of the users to assign as reviewers of the MR"), labels: z.array(z.string()).optional().describe("Labels for the MR"), draft: z.boolean().optional().describe("Create as draft merge request"), allow_collaboration: z .boolean() .optional() .describe("Allow commits from upstream members"), }); export const ForkRepositorySchema = ProjectParamsSchema.extend({ namespace: z.string().optional().describe("Namespace to fork to (full path)"), }); // Branch related schemas export const CreateBranchSchema = ProjectParamsSchema.extend({ branch: z.string().describe("Name for the new branch"), ref: z.string().optional().describe("Source branch/commit for new branch"), }); export const GetBranchDiffsSchema = ProjectParamsSchema.extend({ from: z.string().describe("The base branch or commit SHA to compare from"), to: z.string().describe("The target branch or commit SHA to compare to"), straight: z.boolean().optional().describe("Comparison method: false for '...' (default), true for '--'"), excluded_file_patterns: z.array(z.string()).optional().describe( "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\"]" ), }); export const GetMergeRequestSchema = ProjectParamsSchema.extend({ merge_request_iid: z.number().optional().describe("The IID of a merge request"), source_branch: z.string().optional().describe("Source branch name"), }); export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({ title: z.string().optional().describe("The title of the merge request"), description: z.string().optional().describe("The description of the merge request"), target_branch: z.string().optional().describe("The target branch"), assignee_ids: z.array(z.number()).optional().describe("The ID of the users to assign the MR to"), labels: z.array(z.string()).optional().describe("Labels for the MR"), state_event: z .enum(["close", "reopen"]) .optional() .describe("New state (close/reopen) for the MR"), remove_source_branch: z .boolean() .optional() .describe("Flag indicating if the source branch should be removed"), squash: z.boolean().optional().describe("Squash commits into a single commit when merging"), draft: z.boolean().optional().describe("Work in progress merge request"), }); export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({ view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"), }); export const CreateNoteSchema = z.object({ project_id: z.string().describe("Project ID or namespace/project_path"), noteable_type: z .enum(["issue", "merge_request"]) .describe("Type of noteable (issue or merge_request)"), noteable_iid: z.number().describe("IID of the issue or merge request"), body: z.string().describe("Note content"), }); // Issues API operation schemas export const ListIssuesSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"), assignee_username: z.array(z.string()).optional().describe("Return issues assigned to the given username"), author_id: z.number().optional().describe("Return issues created by the given user ID"), author_username: z.string().optional().describe("Return issues created by the given username"), confidential: z.boolean().optional().describe("Filter confidential or public issues"), created_after: z.string().optional().describe("Return issues created after the given time"), created_before: z.string().optional().describe("Return issues created before the given time"), due_date: z.string().optional().describe("Return issues that have the due date"), labels: z.array(z.string()).optional().describe("Array of label names"), milestone: z.string().optional().describe("Milestone title"), scope: z .enum(["created_by_me", "assigned_to_me", "all"]) .optional() .describe("Return issues from a specific scope"), search: z.string().optional().describe("Search for specific terms"), state: z .enum(["opened", "closed", "all"]) .optional() .describe("Return issues with a specific state"), updated_after: z.string().optional().describe("Return issues updated after the given time"), updated_before: z.string().optional().describe("Return issues updated before the given time"), with_labels_details: z.boolean().optional().describe("Return more details for each label"), }).merge(PaginationOptionsSchema); // Merge Requests API operation schemas export const ListMergeRequestsSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), assignee_id: z .number() .optional() .describe("Returns merge requests assigned to the given user ID"), assignee_username: z .string() .optional() .describe("Returns merge requests assigned to the given username"), author_id: z.number().optional().describe("Returns merge requests created by the given user ID"), author_username: z .string() .optional() .describe("Returns merge requests created by the given username"), reviewer_id: z .number() .optional() .describe("Returns merge requests which have the user as a reviewer"), reviewer_username: z .string() .optional() .describe("Returns merge requests which have the user as a reviewer"), created_after: z .string() .optional() .describe("Return merge requests created after the given time"), created_before: z .string() .optional() .describe("Return merge requests created before the given time"), updated_after: z .string() .optional() .describe("Return merge requests updated after the given time"), updated_before: z .string() .optional() .describe("Return merge requests updated before the given time"), labels: z.array(z.string()).optional().describe("Array of label names"), milestone: z.string().optional().describe("Milestone title"), scope: z .enum(["created_by_me", "assigned_to_me", "all"]) .optional() .describe("Return merge requests from a specific scope"), search: z.string().optional().describe("Search for specific terms"), state: z .enum(["opened", "closed", "locked", "merged", "all"]) .optional() .describe("Return merge requests with a specific state"), order_by: z .enum(["created_at", "updated_at", "priority", "label_priority", "milestone_due", "popularity"]) .optional() .describe("Return merge requests ordered by the given field"), sort: z .enum(["asc", "desc"]) .optional() .describe("Return merge requests sorted in ascending or descending order"), target_branch: z .string() .optional() .describe("Return merge requests targeting a specific branch"), source_branch: z .string() .optional() .describe("Return merge requests from a specific source branch"), wip: z.enum(["yes", "no"]).optional().describe("Filter merge requests against their wip status"), with_labels_details: z.boolean().optional().describe("Return more details for each label"), }).merge(PaginationOptionsSchema); export const GetIssueSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the project issue"), }); export const UpdateIssueSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the project issue"), title: z.string().optional().describe("The title of the issue"), description: z.string().optional().describe("The description of the issue"), assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"), confidential: z.boolean().optional().describe("Set the issue to be confidential"), discussion_locked: z.boolean().optional().describe("Flag to lock discussions"), due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"), labels: z.array(z.string()).optional().describe("Array of label names"), milestone_id: z.number().optional().describe("Milestone ID to assign"), state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"), weight: z.number().optional().describe("Weight of the issue (0-9)"), }); export const DeleteIssueSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the project issue"), }); // Issue links related schemas export const GitLabIssueLinkSchema = z.object({ source_issue: GitLabIssueSchema, target_issue: GitLabIssueSchema, link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]), }); export const ListIssueLinksSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of a project's issue"), }); export const GetIssueLinkSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of a project's issue"), issue_link_id: z.number().describe("ID of an issue relationship"), }); export const CreateIssueLinkSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of a project's issue"), target_project_id: z.string().describe("The ID or URL-encoded path of a target project"), target_issue_iid: z.number().describe("The internal ID of a target project's issue"), link_type: z .enum(["relates_to", "blocks", "is_blocked_by"]) .optional() .describe("The type of the relation, defaults to relates_to"), }); export const DeleteIssueLinkSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of a project's issue"), issue_link_id: z.number().describe("The ID of an issue relationship"), }); // Namespace API operation schemas export const ListNamespacesSchema = z.object({ search: z.string().optional().describe("Search term for namespaces"), owned: z.boolean().optional().describe("Filter for namespaces owned by current user"), }).merge(PaginationOptionsSchema); export const GetNamespaceSchema = z.object({ namespace_id: z.string().describe("Namespace ID or full path"), }); export const VerifyNamespaceSchema = z.object({ path: z.string().describe("Namespace path to verify"), }); // Project API operation schemas export const GetProjectSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), }); export const ListProjectsSchema = z.object({ search: z.string().optional().describe("Search term for projects"), search_namespaces: z.boolean().optional().describe("Needs to be true if search is full path"), owned: z.boolean().optional().describe("Filter for projects owned by current user"), membership: z.boolean().optional().describe("Filter for projects where current user is a member"), simple: z.boolean().optional().describe("Return only limited fields"), archived: z.boolean().optional().describe("Filter for archived projects"), visibility: z .enum(["public", "internal", "private"]) .optional() .describe("Filter by project visibility"), order_by: z .enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"]) .optional() .describe("Return projects ordered by field"), sort: z .enum(["asc", "desc"]) .optional() .describe("Return projects sorted in ascending or descending order"), with_issues_enabled: z .boolean() .optional() .describe("Filter projects with issues feature enabled"), with_merge_requests_enabled: z .boolean() .optional() .describe("Filter projects with merge requests feature enabled"), min_access_level: z.number().optional().describe("Filter by minimum access level"), }).merge(PaginationOptionsSchema); // Label operation schemas export const ListLabelsSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), with_counts: z .boolean() .optional() .describe("Whether or not to include issue and merge request counts"), include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), search: z.string().optional().describe("Keyword to filter labels by"), }); export const GetLabelSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), label_id: z.string().describe("The ID or title of a project's label"), include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), }); export const CreateLabelSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), name: z.string().describe("The name of the label"), color: z .string() .describe("The color of the label given in 6-digit hex notation with leading '#' sign"), description: z.string().optional().describe("The description of the label"), priority: z.number().nullable().optional().describe("The priority of the label"), }); export const UpdateLabelSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), label_id: z.string().describe("The ID or title of a project's label"), new_name: z.string().optional().describe("The new name of the label"), color: z .string() .optional() .describe("The color of the label given in 6-digit hex notation with leading '#' sign"), description: z.string().optional().describe("The new description of the label"), priority: z.number().nullable().optional().describe("The new priority of the label"), }); export const DeleteLabelSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), label_id: z.string().describe("The ID or title of a project's label"), }); // Group projects schema export const ListGroupProjectsSchema = z.object({ group_id: z.string().describe("Group ID or path"), include_subgroups: z.boolean().optional().describe("Include projects from subgroups"), search: z.string().optional().describe("Search term to filter projects"), order_by: z .enum(["name", "path", "created_at", "updated_at", "last_activity_at"]) .optional() .describe("Field to sort by"), sort: z.enum(["asc", "desc"]).optional().describe("Sort direction"), archived: z.boolean().optional().describe("Filter for archived projects"), visibility: z .enum(["public", "internal", "private"]) .optional() .describe("Filter by project visibility"), with_issues_enabled: z .boolean() .optional() .describe("Filter projects with issues feature enabled"), with_merge_requests_enabled: z .boolean() .optional() .describe("Filter projects with merge requests feature enabled"), min_access_level: z.number().optional().describe("Filter by minimum access level"), with_programming_language: z.string().optional().describe("Filter by programming language"), starred: z.boolean().optional().describe("Filter by starred projects"), statistics: z.boolean().optional().describe("Include project statistics"), with_custom_attributes: z.boolean().optional().describe("Include custom attributes"), with_security_reports: z.boolean().optional().describe("Include security reports"), }).merge(PaginationOptionsSchema); // Add wiki operation schemas export const ListWikiPagesSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), with_content: z.boolean().optional().describe("Include content of the wiki pages"), }).merge(PaginationOptionsSchema); export const GetWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), slug: z.string().describe("URL-encoded slug of the wiki page"), }); export const CreateWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), title: z.string().describe("Title of the wiki page"), content: z.string().describe("Content of the wiki page"), format: z.string().optional().describe("Content format, e.g., markdown, rdoc"), }); export const UpdateWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), slug: z.string().describe("URL-encoded slug of the wiki page"), title: z.string().optional().describe("New title of the wiki page"), content: z.string().optional().describe("New content of the wiki page"), format: z.string().optional().describe("Content format, e.g., markdown, rdoc"), }); export const DeleteWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), slug: z.string().describe("URL-encoded slug of the wiki page"), }); // Define wiki response schemas export const GitLabWikiPageSchema = z.object({ title: z.string(), slug: z.string(), format: z.string(), content: z.string().optional(), created_at: z.string().optional(), updated_at: z.string().optional(), }); // Merge Request Thread position schema - used for diff notes export const MergeRequestThreadPositionSchema = z.object({ base_sha: z.string().describe("Base commit SHA in the source branch"), head_sha: z.string().describe("SHA referencing HEAD of the source branch"), start_sha: z.string().describe("SHA referencing the start commit of the source branch"), position_type: z.enum(["text", "image", "file"]).describe("Type of position reference"), new_path: z.string().optional().describe("File path after change"), old_path: z.string().optional().describe("File path before change"), new_line: z.number().nullable().optional().describe("Line number after change"), old_line: z.number().nullable().optional().describe("Line number before change"), width: z.number().optional().describe("Width of the image (for image diffs)"), height: z.number().optional().describe("Height of the image (for image diffs)"), x: z.number().optional().describe("X coordinate on the image (for image diffs)"), y: z.number().optional().describe("Y coordinate on the image (for image diffs)"), }); // Schema for creating a new merge request thread export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({ merge_request_iid: z.number().describe("The IID of a merge request"), body: z.string().describe("The content of the thread"), position: MergeRequestThreadPositionSchema.optional().describe( "Position when creating a diff note" ), created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"), }); // Milestone related schemas // Schema for listing project milestones export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({ iids: z.array(z.number()).optional().describe("Return only the milestones having the given iid"), state: z .enum(["active", "closed"]) .optional() .describe("Return only active or closed milestones"), title: z .string() .optional() .describe("Return only milestones with a title matching the provided string"), search: z .string() .optional() .describe("Return only milestones with a title or description matching the provided string"), include_ancestors: z.boolean().optional().describe("Include ancestor groups"), updated_before: z .string() .optional() .describe("Return milestones updated before the specified date (ISO 8601 format)"), updated_after: z .string() .optional() .describe("Return milestones updated after the specified date (ISO 8601 format)"), }).merge(PaginationOptionsSchema); // Schema for getting a single milestone export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({ milestone_id: z.number().describe("The ID of a project milestone"), }); // Schema for creating a new milestone export const CreateProjectMilestoneSchema = ProjectParamsSchema.extend({ title: z.string().describe("The title of the milestone"), description: z.string().optional().describe("The description of the milestone"), due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"), start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"), }); // Schema for editing a milestone export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({ title: z.string().optional().describe("The title of the milestone"), description: z.string().optional().describe("The description of the milestone"), due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"), start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"), state_event: z .enum(["close", "activate"]) .optional() .describe("The state event of the milestone"), }); // Schema for deleting a milestone export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema; // Schema for getting issues assigned to a milestone export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema; // Schema for getting merge requests assigned to a milestone export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema); // Schema for promoting a project milestone to a group milestone export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema; // Schema for getting burndown chart events for a milestone export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema); // Export types export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>; export type GitLabFork = z.infer<typeof GitLabForkSchema>; export type GitLabIssue = z.infer<typeof GitLabIssueSchema>; export type GitLabIssueWithLinkDetails = z.infer<typeof GitLabIssueWithLinkDetailsSchema>; export type GitLabMergeRequest = z.infer<typeof GitLabMergeRequestSchema>; export type GitLabRepository = z.infer<typeof GitLabRepositorySchema>; export type GitLabFileContent = z.infer<typeof GitLabFileContentSchema>; export type GitLabDirectoryContent = z.infer<typeof GitLabDirectoryContentSchema>; export type GitLabContent = z.infer<typeof GitLabContentSchema>; export type FileOperation = z.infer<typeof FileOperationSchema>; export type GitLabTree = z.infer<typeof GitLabTreeSchema>; export type GitLabCompareResult = z.infer<typeof GitLabCompareResultSchema>; export type GitLabCommit = z.infer<typeof GitLabCommitSchema>; export type GitLabReference = z.infer<typeof GitLabReferenceSchema>; export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>; export type CreateIssueOptions = z.infer<typeof CreateIssueOptionsSchema>; export type CreateMergeRequestOptions = z.infer<typeof CreateMergeRequestOptionsSchema>; export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>; export type GitLabCreateUpdateFileResponse = z.infer<typeof GitLabCreateUpdateFileResponseSchema>; export type GitLabSearchResponse = z.infer<typeof GitLabSearchResponseSchema>; export type GitLabMergeRequestDiff = z.infer< typeof GitLabDiffSchema >; export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>; export type GitLabIssueLink = z.infer<typeof GitLabIssueLinkSchema>; export type ListIssueDiscussionsOptions = z.infer<typeof ListIssueDiscussionsSchema>; export type ListMergeRequestDiscussionsOptions = z.infer<typeof ListMergeRequestDiscussionsSchema>; export type UpdateIssueNoteOptions = z.infer<typeof UpdateIssueNoteSchema>; export type CreateIssueNoteOptions = z.infer<typeof CreateIssueNoteSchema>; export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>; export type GitLabNamespaceExistsResponse = z.infer<typeof GitLabNamespaceExistsResponseSchema>; export type GitLabProject = z.infer<typeof GitLabProjectSchema>; export type GitLabLabel = z.infer<typeof GitLabLabelSchema>; export type ListWikiPagesOptions = z.infer<typeof ListWikiPagesSchema>; export type GetWikiPageOptions = z.infer<typeof GetWikiPageSchema>; export type CreateWikiPageOptions = z.infer<typeof CreateWikiPageSchema>; export type UpdateWikiPageOptions = z.infer<typeof UpdateWikiPageSchema>; export type DeleteWikiPageOptions = z.infer<typeof DeleteWikiPageSchema>; export type GitLabWikiPage = z.infer<typeof GitLabWikiPageSchema>; export type GitLabTreeItem = z.infer<typeof GitLabTreeItemSchema>; export type GetRepositoryTreeOptions = z.infer<typeof GetRepositoryTreeSchema>; export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>; export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>; export type CreateMergeRequestNoteOptions = z.infer<typeof CreateMergeRequestNoteSchema>; export type GitLabPipelineJob = z.infer<typeof GitLabPipelineJobSchema>; export type GitLabPipeline = z.infer<typeof GitLabPipelineSchema>; export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>; export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>; export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>; export type CreatePipelineOptions = z.infer<typeof CreatePipelineSchema>; export type RetryPipelineOptions = z.infer<typeof RetryPipelineSchema>; export type CancelPipelineOptions = z.infer<typeof CancelPipelineSchema>; export type GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>; export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>; export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>; export type CreateProjectMilestoneOptions = z.infer<typeof CreateProjectMilestoneSchema>; export type EditProjectMilestoneOptions = z.infer<typeof EditProjectMilestoneSchema>; export type DeleteProjectMilestoneOptions = z.infer<typeof DeleteProjectMilestoneSchema>; export type GetMilestoneIssuesOptions = z.infer<typeof GetMilestoneIssuesSchema>; export type GetMilestoneMergeRequestsOptions = z.infer<typeof GetMilestoneMergeRequestsSchema>; export type PromoteProjectMilestoneOptions = z.infer<typeof PromoteProjectMilestoneSchema>; export type GetMilestoneBurndownEventsOptions = z.infer<typeof GetMilestoneBurndownEventsSchema>; // Time tracking schemas export const TimeLogCreateSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the issue"), duration: z.string().describe("The duration in GitLab format (e.g., '3h 30m')"), spent_at: z.string().optional().describe("The date the time was spent (YYYY-MM-DD)"), summary: z.string().optional().describe("A short description of the time spent") }); export const TimeLogDeleteSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the issue"), time_log_id: z.number().describe("The ID of the time log to delete") }); export type TimeLogCreateOptions = z.infer<typeof TimeLogCreateSchema>; export type TimeLogDeleteOptions = z.infer<typeof TimeLogDeleteSchema>; export type GitLabUser = z.infer<typeof GitLabUserSchema>; export type GitLabUsersResponse = z.infer<typeof GitLabUsersResponseSchema>; export type PaginationOptions = z.infer<typeof PaginationOptionsSchema>; ```