#
tokens: 23726/50000 24/25 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
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

[![smithery badge](https://smithery.ai/badge/@zereight/gitlab-mcp)](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>;

```
Page 1/2FirstPrevNextLast