diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..46dd13b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,26 @@
+# 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
+
+
+# MCP Transport Mode (Optional)
+# Description:
+# When multiple transport modes are enabled, the server will use the following priority:
+# 1. **Streamable HTTP** (if `STREAMABLE_HTTP=true`) - Highest priority
+# 2. **SSE** (if `SSE=true` and `STREAMABLE_HTTP!=true`) - Medium priority
+# 3. **Stdio** (if `SSE!=true` and `STREAMABLE_HTTP!=true`)
+SSE=true
+STREAMABLE_HTTP=false
+
+# MCP Server Host With SSE Transport and Streamable Http Transport
+HOST=127.0.0.1
+
+# Proxy Configuration (optional)
+HTTP_PROXY=
+HTTPS_PROXY=
+NO_PROXY=localhost,127.0.0.1
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..14d4edd
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,24 @@
+{
+ "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"]
+}
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
new file mode 100644
index 0000000..9d16b5f
--- /dev/null
+++ b/.github/workflows/auto-merge.yml
@@ -0,0 +1,30 @@
+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 }}
\ No newline at end of file
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..06ac5b2
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,39 @@
+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: iwakitakuma/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 }}
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
new file mode 100644
index 0000000..d28f272
--- /dev/null
+++ b/.github/workflows/npm-publish.yml
@@ -0,0 +1,23 @@
+name: NPM Publish
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ npm:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ registry-url: 'https://registry.npmjs.org/'
+ - name: Install dependencies
+ run: npm ci
+ - name: Publish to npm
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: npm publish --access public
diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml
new file mode 100644
index 0000000..17ffd3d
--- /dev/null
+++ b/.github/workflows/pr-test.yml
@@ -0,0 +1,166 @@
+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 }}
+ TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }}
+
+ - 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 }}
+ TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }}
+
+ 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: ${{ secrets.GITLAB_TOKEN_TEST }}
+ GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }}
+ TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 28f1ba7..639f24e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,12 @@
node_modules
-.DS_Store
\ No newline at end of file
+.DS_Store
+build
+.env
+.env.local
+.env.test
+coverage/
+*.log
+
+# ai
+.opencode*
+.aider*
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..f70ef08
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,6 @@
+node_modules/
+build/
+coverage/
+*.log
+.DS_Store
+package-lock.json
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..3ed9654
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,11 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": false,
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "bracketSpacing": true,
+ "arrowParens": "avoid",
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/.secrets b/.secrets
new file mode 100644
index 0000000..dc5f5ec
--- /dev/null
+++ b/.secrets
@@ -0,0 +1,3 @@
+DOCKERHUB_USERNAME=DOCKERHUB_USERNAME
+DOCKERHUB_TOKEN=DOCKERHUB_TOKEN
+GITHUB_TOKEN=DOCKERHUB_TOKEN
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d851fcd
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,325 @@
+#### [v1.0.77](https://github.com/zereight/gitlab-mcp/compare/v1.0.76...v1.0.77)
+
+- v1.0.77 [`#209`](https://github.com/zereight/gitlab-mcp/pull/209)
+- FIX: docker hub repo user name [`#210`](https://github.com/zereight/gitlab-mcp/pull/210)
+- feat: Add NPM publish workflow for automated package publishing [`#208`](https://github.com/zereight/gitlab-mcp/pull/208)
+- Fix list of tools in `README.md` [`#205`](https://github.com/zereight/gitlab-mcp/pull/205)
+- FIX: flexible boolean [`#201`](https://github.com/zereight/gitlab-mcp/pull/201)
+- feat(attachement):download attachement, e.g. images [`#200`](https://github.com/zereight/gitlab-mcp/pull/200)
+- FEAT: merge MR [`#193`](https://github.com/zereight/gitlab-mcp/pull/193)
+- FEAT: get draft note [`#197`](https://github.com/zereight/gitlab-mcp/pull/197)
+- feat: Add createDraftNote api support, useful for bulk code review [`#183`](https://github.com/zereight/gitlab-mcp/pull/183)
+- feat: add my_issues and list_project_members tools [`#133`](https://github.com/zereight/gitlab-mcp/pull/133)
+- feat(pipeline): Add list_pipeline_trigger_jobs tools [`#194`](https://github.com/zereight/gitlab-mcp/pull/194)
+- (feat): add tool to upload file for markdown content [`#196`](https://github.com/zereight/gitlab-mcp/pull/196)
+- Fix list of tools [`4ab6eb1`](https://github.com/zereight/gitlab-mcp/commit/4ab6eb186c16cc8534f649fe76b71f5e4dc82b9d)
+- FIX [`19b7254`](https://github.com/zereight/gitlab-mcp/commit/19b725447acfbc35d4c6ae3d6f07de7df6d5d0c8)
+- Merge pull request #192 from zereight/feat/188-1 [`449de4d`](https://github.com/zereight/gitlab-mcp/commit/449de4dab8f9578e212636e414aea471bcae6125)
+
+#### [v1.0.76](https://github.com/zereight/gitlab-mcp/compare/v1.0.75...v1.0.76)
+
+> 25 July 2025
+
+- Bump version to 1.0.76 [`#182`](https://github.com/zereight/gitlab-mcp/pull/182)
+- FEAT: iteration [`#179`](https://github.com/zereight/gitlab-mcp/pull/179)
+- Bump version to 1.0.75 [`#178`](https://github.com/zereight/gitlab-mcp/pull/178)
+- Merge pull request #181 from zereight/feat/166-1 [`6adbeea`](https://github.com/zereight/gitlab-mcp/commit/6adbeea560dc6a06d90a1b0984253bb2f3df2867)
+- FIX [`46f8405`](https://github.com/zereight/gitlab-mcp/commit/46f8405a31f957c4c60113e3473e9e084562eff0)
+- FIX: default null [`6e82e7e`](https://github.com/zereight/gitlab-mcp/commit/6e82e7e5c9e603af18fa4639956d159a861edce6)
+
+#### [v1.0.75](https://github.com/zereight/gitlab-mcp/compare/v1.0.74...v1.0.75)
+
+> 18 July 2025
+
+- Allow accessing issues without project id [`#168`](https://github.com/zereight/gitlab-mcp/pull/168)
+- Feat/deploy script [`#176`](https://github.com/zereight/gitlab-mcp/pull/176)
+- FEAT: target project on create MR [`#174`](https://github.com/zereight/gitlab-mcp/pull/174)
+- Bump version to 1.0.74 [`#175`](https://github.com/zereight/gitlab-mcp/pull/175)
+- FIX [`2872aed`](https://github.com/zereight/gitlab-mcp/commit/2872aed35fee55b29d44ec01e0417a0a087e9ef3)
+- fix(list_issues): make project_id optional [`b242522`](https://github.com/zereight/gitlab-mcp/commit/b2425221a6077c7fdba343e8681d1938a24d3a39)
+- Bump version to 1.0.75 [`b0411ba`](https://github.com/zereight/gitlab-mcp/commit/b0411ba2b9e949fefe37620b27425310d38a5cd7)
+
+#### [v1.0.74](https://github.com/zereight/gitlab-mcp/compare/v1.0.73...v1.0.74)
+
+> 17 July 2025
+
+- Bump version to 1.0.74 [`8effa28`](https://github.com/zereight/gitlab-mcp/commit/8effa283ba7fe71e7b9c6548e37a7866bd730421)
+
+#### [v1.0.73](https://github.com/zereight/gitlab-mcp/compare/v1.0.72...v1.0.73)
+
+> 13 July 2025
+
+- FEAT: add logging [`#162`](https://github.com/zereight/gitlab-mcp/pull/162)
+- FEAT: id is string or number [`#161`](https://github.com/zereight/gitlab-mcp/pull/161)
+- FIX string or number [`#160`](https://github.com/zereight/gitlab-mcp/pull/160)
+- FIX: id string or number [`#158`](https://github.com/zereight/gitlab-mcp/pull/158)
+- FIX: deploy script [`#156`](https://github.com/zereight/gitlab-mcp/pull/156)
+- FIX [`1131897`](https://github.com/zereight/gitlab-mcp/commit/1131897817ca5462320ba1a9a532ec3f2b196286)
+- FIX [`0affdf9`](https://github.com/zereight/gitlab-mcp/commit/0affdf9df0c2ca1ec9deab2e4977b93f8acd9e0b)
+- FIX: string or number [`429f397`](https://github.com/zereight/gitlab-mcp/commit/429f39700ee9f48f65af8bbc8d8756abe3da37bd)
+
+#### [v1.0.72](https://github.com/zereight/gitlab-mcp/compare/v1.0.65...v1.0.72)
+
+> 9 July 2025
+
+- chore: Bump version 1.0.72 [`#154`](https://github.com/zereight/gitlab-mcp/pull/154)
+- FEAT: enable string for boolean for claude [`#150`](https://github.com/zereight/gitlab-mcp/pull/150)
+- Bump version to 1.0.71 [`#152`](https://github.com/zereight/gitlab-mcp/pull/152)
+- tag with image push [`#151`](https://github.com/zereight/gitlab-mcp/pull/151)
+- FIX: string | number for gemini [`#149`](https://github.com/zereight/gitlab-mcp/pull/149)
+- FIX: format boolean for claude [`#148`](https://github.com/zereight/gitlab-mcp/pull/148)
+- Print logs to stderr [`#147`](https://github.com/zereight/gitlab-mcp/pull/147)
+- FIX: CreateMergeRequestSchema [`#146`](https://github.com/zereight/gitlab-mcp/pull/146)
+- chore: Bump version to 1.0.70 [`#145`](https://github.com/zereight/gitlab-mcp/pull/145)
+- FIX: sse [`#144`](https://github.com/zereight/gitlab-mcp/pull/144)
+- FIX: default project id [`#141`](https://github.com/zereight/gitlab-mcp/pull/141)
+- FEAT: format boolean [`#143`](https://github.com/zereight/gitlab-mcp/pull/143)
+- FIX: console to stderr [`#135`](https://github.com/zereight/gitlab-mcp/pull/135)
+- FIX: create pipeline [`#138`](https://github.com/zereight/gitlab-mcp/pull/138)
+- FEAT: Add support for Streamable HTTP transport [`#128`](https://github.com/zereight/gitlab-mcp/pull/128)
+- FIX: notable iid [`#126`](https://github.com/zereight/gitlab-mcp/pull/126)
+- FEAT: reviewer on update MR [`#118`](https://github.com/zereight/gitlab-mcp/pull/118)
+- FEAT: not call create fork repo on set default project id [`#116`](https://github.com/zereight/gitlab-mcp/pull/116)
+- FEAT: project id , vscode doc [`#113`](https://github.com/zereight/gitlab-mcp/pull/113)
+- feat(simple healthcheck): [`#112`](https://github.com/zereight/gitlab-mcp/pull/112)
+- FIX: new,old path nullable [`#108`](https://github.com/zereight/gitlab-mcp/pull/108)
+- fix: avoid error caused by line_range type: null in discussion [`#107`](https://github.com/zereight/gitlab-mcp/pull/107)
+- FIX: sse readme [`#103`](https://github.com/zereight/gitlab-mcp/pull/103)
+- REVIEW FIX [`3ad2954`](https://github.com/zereight/gitlab-mcp/commit/3ad29547b4f53aacb07b54d4f88329f8cb23c1cf)
+- fix: avoid error caused by line_range: null in discussion [`d50b7fd`](https://github.com/zereight/gitlab-mcp/commit/d50b7fd1ac01802889bd383e39d767378204aa66)
+- FIX [`b109392`](https://github.com/zereight/gitlab-mcp/commit/b109392f1c89d891d6a9706249c2fcd541ab0165)
+
+#### [v1.0.65](https://github.com/zereight/gitlab-mcp/compare/v1.0.64...v1.0.65)
+
+> 16 June 2025
+
+- FEAT: mr discussion with code diff [`#93`](https://github.com/zereight/gitlab-mcp/pull/93)
+- docs: update README and version to 1.0.64 [`cced1c1`](https://github.com/zereight/gitlab-mcp/commit/cced1c16f9c2c7cc0ba2e7e2c28884bb966f0bd5)
+
+#### [v1.0.64](https://github.com/zereight/gitlab-mcp/compare/1.0.63...v1.0.64)
+
+> 16 June 2025
+
+- feat: add cookie-based authentication support for enterprise GitLab [`#101`](https://github.com/zereight/gitlab-mcp/pull/101)
+- Fix notification_level null handling for GitLab group owners [`#99`](https://github.com/zereight/gitlab-mcp/pull/99)
+
+#### [1.0.63](https://github.com/zereight/gitlab-mcp/compare/v1.0.63...1.0.63)
+
+> 12 June 2025
+
+- docs: add CHANGELOG entry for v1.0.63 [`8d70627`](https://github.com/zereight/gitlab-mcp/commit/8d706275e657be0509941b43c47f892643a24a5b)
+
+#### [v1.0.63](https://github.com/zereight/gitlab-mcp/compare/1.0.62...v1.0.63)
+
+> 12 June 2025
+
+- feat: add pagination support for CI job logs to prevent context window flooding [`#97`](https://github.com/zereight/gitlab-mcp/pull/97)
+- [version-update] fix: correct Private-Token header authentication for GitLab API 🔐 [`3c23675`](https://github.com/zereight/gitlab-mcp/commit/3c23675eece9b1d8ce90f65cc9692100f5cb2c8a)
+- chore: bump version to 1.0.63 [`62f0fff`](https://github.com/zereight/gitlab-mcp/commit/62f0ffff69e8b52acc078410e5578231ef883cc1)
+
+#### [1.0.62](https://github.com/zereight/gitlab-mcp/compare/1.0.60...1.0.62)
+
+> 10 June 2025
+
+- FIX: private token auth [`#91`](https://github.com/zereight/gitlab-mcp/pull/91)
+- FEAT: private token auth [`#89`](https://github.com/zereight/gitlab-mcp/pull/89)
+- style: format code for consistency and readability ✨ [`1ba5434`](https://github.com/zereight/gitlab-mcp/commit/1ba54342bc4a2769b95cf27fc6cc54c84e55aa94)
+- [version-update] feat: bump version to 1.0.60 🎉 [`29659db`](https://github.com/zereight/gitlab-mcp/commit/29659db0b74471b6042106fcae1b2ca273f2ae4c)
+- [version-update] feat: bump version to 1.0.62 🎉 [`8df87c6`](https://github.com/zereight/gitlab-mcp/commit/8df87c67d2701f5bd6bdbef4e0880457507b135d)
+
+#### [1.0.60](https://github.com/zereight/gitlab-mcp/compare/v1.0.59...1.0.60)
+
+> 7 June 2025
+
+- FIX: list issues assginee username [`#87`](https://github.com/zereight/gitlab-mcp/pull/87)
+- FEAT: add support for `remove_source_branch` and `squash` options for merge requests [`#86`](https://github.com/zereight/gitlab-mcp/pull/86)
+
+#### [v1.0.59](https://github.com/zereight/gitlab-mcp/compare/v1.0.57...v1.0.59)
+
+> 4 June 2025
+
+- Fix for null error [`#85`](https://github.com/zereight/gitlab-mcp/pull/85)
+- FIX: bug get issues [`#83`](https://github.com/zereight/gitlab-mcp/pull/83)
+- Add support for retrieving wiki page content in list_wiki_pages [`#82`](https://github.com/zereight/gitlab-mcp/pull/82)
+- DOC: readme docker image [`#81`](https://github.com/zereight/gitlab-mcp/pull/81)
+- [version-update] feat: bump version to 1.0.59 🎉 [`0930ce3`](https://github.com/zereight/gitlab-mcp/commit/0930ce3636e8b155d7ac5892226cc1c780135de3)
+- [feat] update: bump version to 1.0.58 [`8cb7703`](https://github.com/zereight/gitlab-mcp/commit/8cb7703aa1a2284143bc4e84f16bf2af59a2792a)
+
+#### [v1.0.57](https://github.com/zereight/gitlab-mcp/compare/v1.0.56...v1.0.57)
+
+> 3 June 2025
+
+- Add pagination to merge request discussions, similar to issue discussions [`#80`](https://github.com/zereight/gitlab-mcp/pull/80)
+- fix: merge_requests_template can be null [`#79`](https://github.com/zereight/gitlab-mcp/pull/79)
+- [feat] update: bump version to 1.0.57 [`c07356b`](https://github.com/zereight/gitlab-mcp/commit/c07356bd465dc565ce323683a8b96a7e76241c8b)
+
+#### [v1.0.56](https://github.com/zereight/gitlab-mcp/compare/v1.0.54...v1.0.56)
+
+> 2 June 2025
+
+- FIX: issue param [`#78`](https://github.com/zereight/gitlab-mcp/pull/78)
+- FIX: get issues labels [`#77`](https://github.com/zereight/gitlab-mcp/pull/77)
+- FEAT: MCP SSE [`#76`](https://github.com/zereight/gitlab-mcp/pull/76)
+- Feat: Enrich Merge Request Creation [`#68`](https://github.com/zereight/gitlab-mcp/pull/68)
+- feat: add branch comparison functionality and update related schemas [`c834ebc`](https://github.com/zereight/gitlab-mcp/commit/c834ebc135bf5896ab4f7982ae417f0c32d8ea42)
+- fix: remove duplicate entry for get_branch_diffs in tools list [`6bc1379`](https://github.com/zereight/gitlab-mcp/commit/6bc13794c8cfe09dafa2fddeae2d05589700cac6)
+- feat: add user retrieval functions and schemas for GitLab API integration [`005b46a`](https://github.com/zereight/gitlab-mcp/commit/005b46a1a66d2d72bc922f9f98f2df2f58c5f084)
+
+#### [v1.0.54](https://github.com/zereight/gitlab-mcp/compare/v1.0.53...v1.0.54)
+
+> 31 May 2025
+
+- Feat/custom ssl [`#72`](https://github.com/zereight/gitlab-mcp/pull/72)
+- FEAT: multi platform [`#71`](https://github.com/zereight/gitlab-mcp/pull/71)
+- Release v1.0.54: Add multi-platform support and custom SSL configuration [`459161e`](https://github.com/zereight/gitlab-mcp/commit/459161e23514e9a4d70fd6f902e5f84ba049eec1)
+- chore: remove outdated release notes for version 1.0.40 [`e9493b2`](https://github.com/zereight/gitlab-mcp/commit/e9493b2ff90554d21bd8056350a554e8325c22ba)
+- [main] chore: bump version to v1.0.54 🚀 [`4a8088c`](https://github.com/zereight/gitlab-mcp/commit/4a8088c25cea0c747c9df71501ff0a6fe46bef40)
+
+#### [v1.0.53](https://github.com/zereight/gitlab-mcp/compare/v1.0.52...v1.0.53)
+
+> 30 May 2025
+
+- FEAT: ci push docker hub [`#65`](https://github.com/zereight/gitlab-mcp/pull/65)
+- [main] fix: make old_line and new_line optional for image diff discussions [`cb36c00`](https://github.com/zereight/gitlab-mcp/commit/cb36c007cb215127c16e621ef5a0255c76a6cdbe)
+- [main] chore: bump version to v1.0.53 [`fcb71e2`](https://github.com/zereight/gitlab-mcp/commit/fcb71e293e8a0f7f803397582d2e5ff867febd2d)
+
+#### [v1.0.52](https://github.com/zereight/gitlab-mcp/compare/v1.0.50...v1.0.52)
+
+> 30 May 2025
+
+- feat: add pipeline management commands [`#64`](https://github.com/zereight/gitlab-mcp/pull/64)
+- [main] docs: update README with comments on GITLAB configuration options [`#63`](https://github.com/zereight/gitlab-mcp/pull/63)
+- test [`#61`](https://github.com/zereight/gitlab-mcp/pull/61)
+- Fix GitHub Actions workflow syntax errors [`#62`](https://github.com/zereight/gitlab-mcp/pull/62)
+- [feat/pipeline-support] feat: add pipeline management commands [`#46`](https://github.com/zereight/gitlab-mcp/issues/46)
+- [feat/pipeline-support] feat: add USE_PIPELINE environment variable for conditional pipeline feature activation [`de0b138`](https://github.com/zereight/gitlab-mcp/commit/de0b138d8002daf15d845c6360957c50d95a6288)
+- [main] docs: update README to remove automated testing section 📝 [`37203ba`](https://github.com/zereight/gitlab-mcp/commit/37203bae5a87d902380ecb7ead454ec9b19af1ef)
+- [main] debug: temporarily disable MCP server startup test [`8e2b6e6`](https://github.com/zereight/gitlab-mcp/commit/8e2b6e67349aa575dd9c3217b58bfe76772932ae)
+
+#### [v1.0.50](https://github.com/zereight/gitlab-mcp/compare/v1.0.48...v1.0.50)
+
+> 29 May 2025
+
+- [main] feat: update milestone management tools and improve code formatting ✨ [`181f1e9`](https://github.com/zereight/gitlab-mcp/commit/181f1e943cbfcee8486717e73a63fd62e3ded280)
+
+#### [v1.0.48](https://github.com/zereight/gitlab-mcp/compare/v1.0.47...v1.0.48)
+
+> 29 May 2025
+
+- feat: add tools for milestones [`#59`](https://github.com/zereight/gitlab-mcp/pull/59)
+- FEAT: docker image push script [`#60`](https://github.com/zereight/gitlab-mcp/pull/60)
+- [main] chore: v1.0.48 버전 업데이트 [`2a80988`](https://github.com/zereight/gitlab-mcp/commit/2a80988a0231320f80a1d4bd75e51f50e195b29a)
+- feat: add milestone management commands to README [`5762b32`](https://github.com/zereight/gitlab-mcp/commit/5762b32a69c3aa13ae819335ba7549be6f36722e)
+
+#### [v1.0.47](https://github.com/zereight/gitlab-mcp/compare/v1.0.46...v1.0.47)
+
+> 29 May 2025
+
+- fix(schemas): make illustration nullable in GitLabPipelineSchema [`#58`](https://github.com/zereight/gitlab-mcp/pull/58)
+- feat: implement list_merge_requests functionality [`#56`](https://github.com/zereight/gitlab-mcp/pull/56)
+- fix(schemas): make avatar_url nullable in GitLabUserSchema [`#55`](https://github.com/zereight/gitlab-mcp/pull/55)
+- feat: implement list_merge_requests functionality [`cc84777`](https://github.com/zereight/gitlab-mcp/commit/cc847772f1f8560d9ce9cba25acbb232cbbf618d)
+- [main] release: v1.0.47 [`a2c2ac1`](https://github.com/zereight/gitlab-mcp/commit/a2c2ac185ad2891e11e27a534ef089701effb526)
+
+#### [v1.0.46](https://github.com/zereight/gitlab-mcp/compare/v1.0.45...v1.0.46)
+
+> 27 May 2025
+
+- FIX: description null error [`#53`](https://github.com/zereight/gitlab-mcp/pull/53)
+- [main] fix: description null error handling [`f8b1444`](https://github.com/zereight/gitlab-mcp/commit/f8b1444afd5932307ae743ec11380189e59daafa)
+
+#### [v1.0.45](https://github.com/zereight/gitlab-mcp/compare/v1.0.42...v1.0.45)
+
+> 24 May 2025
+
+- feat(release): 1.0.44 adds pipeline jobs tool [`#52`](https://github.com/zereight/gitlab-mcp/pull/52)
+- chore(release): 1.0.43 - get_repository_tree is added read_only_mode [`1406203`](https://github.com/zereight/gitlab-mcp/commit/140620397ba88ee6abbd6da01147a466905e1f22)
+- [main] docs: update changelog for v1.0.45 pipeline tools [`8ba3398`](https://github.com/zereight/gitlab-mcp/commit/8ba33986f3da8eae4079b179aa3580a1712586a1)
+- docs: translate issue notes changelog from Korean to English [`3d7aa80`](https://github.com/zereight/gitlab-mcp/commit/3d7aa8035d996a312559e15f7dd1457e1f32a826)
+
+#### [v1.0.42](https://github.com/zereight/gitlab-mcp/compare/v1.0.40...v1.0.42)
+
+> 22 May 2025
+
+- feat: add support for creating and updating issue notes [`#47`](https://github.com/zereight/gitlab-mcp/pull/47)
+- fix: fix README [`#45`](https://github.com/zereight/gitlab-mcp/pull/45)
+- chore(release): 1.0.42 - issue note 기능 추가 (#47) [`25be194`](https://github.com/zereight/gitlab-mcp/commit/25be1947b98ffe1e5cffbfce9e04928f4180d2f8)
+- docs: update release notes for v1.0.40 (2025-05-21) [`b326f4c`](https://github.com/zereight/gitlab-mcp/commit/b326f4c3c3c43ec6b669a36bbc016377ebfc1a0c)
+
+#### [v1.0.40](https://github.com/zereight/gitlab-mcp/compare/v1.0.39...v1.0.40)
+
+> 21 May 2025
+
+- feat: add issue discussions support [`#44`](https://github.com/zereight/gitlab-mcp/pull/44)
+
+#### [v1.0.39](https://github.com/zereight/gitlab-mcp/compare/v1.0.38...v1.0.39)
+
+> 20 May 2025
+
+- feat: add docker image and push to dockerhub [`#42`](https://github.com/zereight/gitlab-mcp/pull/42)
+- fixed resolve_outdated_diff_discussions nullable [`#41`](https://github.com/zereight/gitlab-mcp/pull/41)
+- docs: add release-notes.md [`676bbcd`](https://github.com/zereight/gitlab-mcp/commit/676bbcd4ddb9fa3b566a67fffdd2f25de258b933)
+- 버전 1.0.39로 업데이트 [`e4a28a9`](https://github.com/zereight/gitlab-mcp/commit/e4a28a9a47540214587169b7d3f3a98fe057c7d8)
+
+#### [v1.0.38](https://github.com/zereight/gitlab-mcp/compare/v1.0.37...v1.0.38)
+
+> 17 May 2025
+
+- fix: add `expanded` to `start` and `end` for GitLabDiscussionNoteSchema [`#40`](https://github.com/zereight/gitlab-mcp/pull/40)
+- Bump version [`0bb59a3`](https://github.com/zereight/gitlab-mcp/commit/0bb59a3217f4c3dd98b51503bf2de51d8578bb0d)
+
+#### [v1.0.37](https://github.com/zereight/gitlab-mcp/compare/v1.0.36...v1.0.37)
+
+> 15 May 2025
+
+- Adds threaded comment support for merge requests [`#38`](https://github.com/zereight/gitlab-mcp/pull/38)
+- Support resolving merge request discussion notes [`#37`](https://github.com/zereight/gitlab-mcp/pull/37)
+- feat: Add create_merge_request_thread tool for diff notes [`026dd58`](https://github.com/zereight/gitlab-mcp/commit/026dd58887079bb60187d6acacaafc6fa28d0c3d)
+- feat: Implement add_merge_request_thread_note function for adding notes to existing MR threads [`3f2b355`](https://github.com/zereight/gitlab-mcp/commit/3f2b35535ee93b14a6649074608842d1ff8de208)
+- feat: support resolving merge request notes [`bde83c0`](https://github.com/zereight/gitlab-mcp/commit/bde83c0a912ba60026abd1954e764bb09d5a013d)
+
+#### [v1.0.36](https://github.com/zereight/gitlab-mcp/compare/v1.0.34...v1.0.36)
+
+> 13 May 2025
+
+- feat: Decode project_id for GitLab API calls [`08ab135`](https://github.com/zereight/gitlab-mcp/commit/08ab1357a0bfdef0bf6360f0c61759f25405652b)
+- [main] refactor: update label_id schema to use string type [`bf250b0`](https://github.com/zereight/gitlab-mcp/commit/bf250b0d88fad864a93ae2d95c0f99b7eb827498)
+- [main] chore: update version to 1.0.35 🚀 [`651072d`](https://github.com/zereight/gitlab-mcp/commit/651072dfd7926101b77f095d5ce2ab9d0fe6af58)
+
+#### [v1.0.34](https://github.com/zereight/gitlab-mcp/compare/1.0.32...v1.0.34)
+
+> 7 May 2025
+
+- feat: Gitlab list repository tree tool [`#35`](https://github.com/zereight/gitlab-mcp/pull/35)
+- feat: support search by branch for get_merge_request [`#34`](https://github.com/zereight/gitlab-mcp/pull/34)
+- fix: rename to source branch [`7b8cbc0`](https://github.com/zereight/gitlab-mcp/commit/7b8cbc0806ed9123e033d98f4965fd6fbc532c07)
+- [main] docs: update README with detailed descriptions for merge request functions [`3a25e7c`](https://github.com/zereight/gitlab-mcp/commit/3a25e7c5e8b9e21585068db15e61818ca542f0f9)
+- [main] chore: update version to 1.0.34 [`23a9bbc`](https://github.com/zereight/gitlab-mcp/commit/23a9bbc728a4171eb362d6458ef165d3f9246564)
+
+#### 1.0.32
+
+> 25 April 2025
+
+- feat: Implement proxy configuration for HTTP/HTTPS/SOCKS [`#33`](https://github.com/zereight/gitlab-mcp/pull/33)
+- feat: Add read-only mode support [`#29`](https://github.com/zereight/gitlab-mcp/pull/29)
+- Add schemas for GitLab discussion notes and merge request discussions [`#26`](https://github.com/zereight/gitlab-mcp/pull/26)
+- :sparkles: Add `list_group_projects tool` [`#25`](https://github.com/zereight/gitlab-mcp/pull/25)
+- Update README.md [`#24`](https://github.com/zereight/gitlab-mcp/pull/24)
+- Fixed types for create_merge_request and get_merge_request tools. [`#23`](https://github.com/zereight/gitlab-mcp/pull/23)
+- Authentication Header Consistency Fix [`#22`](https://github.com/zereight/gitlab-mcp/pull/22)
+- Labels API Support [`#21`](https://github.com/zereight/gitlab-mcp/pull/21)
+- Improve README documentation with detailed tool descriptions [`#18`](https://github.com/zereight/gitlab-mcp/pull/18)
+- Implement GitLab Issues and Issue Links API [`#17`](https://github.com/zereight/gitlab-mcp/pull/17)
+- Add GitLab Projects API support [`#16`](https://github.com/zereight/gitlab-mcp/pull/16)
+- Add GitLab Namespaces API support [`#15`](https://github.com/zereight/gitlab-mcp/pull/15)
+- Fix GitLab API fork repository parameter handling [`#14`](https://github.com/zereight/gitlab-mcp/pull/14)
+- Fix GitLab API parameter handling in create_or_update_file [`#13`](https://github.com/zereight/gitlab-mcp/pull/13)
+- Improve code documentation with bilingual JSDoc comments [`#11`](https://github.com/zereight/gitlab-mcp/pull/11)
+- Fix URL construction with smart API URL normalization [`#10`](https://github.com/zereight/gitlab-mcp/pull/10)
+- Fix createNote function URL construction: use plural resource names and avoid duplicate /api/v4 [`#7`](https://github.com/zereight/gitlab-mcp/pull/7)
+- Added missing api url part to create_note [`#3`](https://github.com/zereight/gitlab-mcp/pull/3)
+- Deployment: Dockerfile and Smithery config [`#2`](https://github.com/zereight/gitlab-mcp/pull/2)
+- add MCP server badge [`#1`](https://github.com/zereight/gitlab-mcp/pull/1)
+- build: test-note.js 파일 삭제 [`61ee124`](https://github.com/zereight/gitlab-mcp/commit/61ee1244f431c591f199d93d683f2f9b573e48b6)
+- Add compiled JavaScript files for Issue Links API schema fix [`a4d7795`](https://github.com/zereight/gitlab-mcp/commit/a4d7795a7ab28a28a3863e8cc77322d6829ec713)
+- Build upd. [`5d10401`](https://github.com/zereight/gitlab-mcp/commit/5d1040141d20169420e63e67c438a9a942d157d6)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..7caacda
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+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"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a81c6db
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Roo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index cd4a3be..3b77756 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,270 @@
-# @zereight/mcp-gitlab
+# GitLab MCP Server
-GitLab MCP(Model Context Protocol) Server.
+[](https://www.star-history.com/#zereight/gitlab-mcp&Date)
-## Installation and Execution
+## @zereight/mcp-gitlab
-```bash
-npx @zereight/mcp-gitlab
-```
+[](https://smithery.ai/server/@zereight/gitlab-mcp)
-## Environment Variable Configuration
+GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements over the original GitLab MCP server.**
-Before running the server, you need to set the following environment variables:
+
-```bash
-GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token
-GITLAB_API_URL=your_gitlab_api_url # Default: https://gitlab.com/api/v4
-```
+## Usage
-## License
+### Using with Claude App, Cline, Roo Code, Cursor, Kilo Code
-MIT License
+When using with the Claude App, you need to set up your API key and URLs directly.
-## How to use
+#### npx
-## Using with Claude App, Cline, Roo Code
+```json
+{
+ "mcpServers": {
+ "gitlab": {
+ "command": "npx",
+ "args": ["-y", "@zereight/mcp-gitlab"],
+ "env": {
+ "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
+ "GITLAB_API_URL": "your_gitlab_api_url",
+ "GITLAB_PROJECT_ID": "your_project_id", // Optional: default project
+ "GITLAB_ALLOWED_PROJECT_IDS": "", // Optional: comma-separated list of allowed project IDs
+ "GITLAB_READ_ONLY_MODE": "false",
+ "USE_GITLAB_WIKI": "false", // use wiki api?
+ "USE_MILESTONE": "false", // use milestone api?
+ "USE_PIPELINE": "false" // use pipeline api?
+ }
+ }
+ }
+}
+```
-When using with the Claude App, you need to set up your API key and URLs directly.
+#### vscode .vscode/mcp.json
```json
{
- "mcpServers": {
- "GitLab communication server": {
+ "inputs": [
+ {
+ "type": "promptString",
+ "id": "gitlab-token",
+ "description": "Gitlab Token to read API",
+ "password": true
+ }
+ ],
+ "servers": {
+ "GitLab-MCP": {
+ "type": "stdio",
"command": "npx",
"args": ["-y", "@zereight/mcp-gitlab"],
+ "env": {
+ "GITLAB_PERSONAL_ACCESS_TOKEN": "${input:gitlab-token}",
+ "GITLAB_API_URL": "your-fancy-gitlab-url",
+ "GITLAB_READ_ONLY_MODE": "true",
+ ...
+ }
+ }
+ }
+}
+```
+
+#### Docker
+
+- stdio mcp.json
+
+```json
+{
+ "mcpServers": {
+ "gitlab": {
+ "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": "your_gitlab_api_url"
+ "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"
}
}
}
}
```
-## Using with Cursor
+- 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": {
+ "type": "sse",
+ "url": "http://localhost:3333/sse"
+ }
+ }
+}
+```
-When using with Cursor, you can set up environment variables and run the server as follows:
+- streamable-http
-```bash
-env GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token GITLAB_API_URL=your_gitlab_api_url npx @zereight/mcp-gitlab
+```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 STREAMABLE_HTTP=true \
+ -p 3333:3002 \
+ iwakitakuma/gitlab-mcp
```
+```json
+{
+ "mcpServers": {
+ "gitlab": {
+ "type": "streamable-http",
+ "url": "http://localhost:3333/mcp"
+ }
+ }
+}
+```
+
+### 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_PROJECT_ID`: Default project ID. If set, Overwrite this value when making an API request.
+- `GITLAB_ALLOWED_PROJECT_IDS`: Optional comma-separated list of allowed project IDs. When set with a single value, acts as a default project (like the old "lock" mode). When set with multiple values, restricts access to only those projects. Examples:
+ - Single value `123`: MCP server can only access project 123 and uses it as default
+ - Multiple values `123,456,789`: MCP server can access projects 123, 456, and 789 but requires explicit project ID in requests
+- `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.
+- `GITLAB_AUTH_COOKIE_PATH`: Path to an authentication cookie file for GitLab instances that require cookie-based authentication. When provided, the cookie will be included in all GitLab API requests.
+- `SSE`: When set to 'true', enables the Server-Sent Events transport.
+- `STREAMABLE_HTTP`: When set to 'true', enables the Streamable HTTP transport. If both **SSE** and **STREAMABLE_HTTP** are set to 'true', the server will prioritize Streamable HTTP over SSE transport.
+
+## Tools 🛠️
+
+
+Click to expand
+
+
+
+1. `merge_merge_request` - Merge a merge request in a GitLab project
+2. `create_or_update_file` - Create or update a single file in a GitLab project
+3. `search_repositories` - Search for GitLab projects
+4. `create_repository` - Create a new GitLab project
+5. `get_file_contents` - Get the contents of a file or directory from a GitLab project
+6. `push_files` - Push multiple files to a GitLab project in a single commit
+7. `create_issue` - Create a new issue in a GitLab project
+8. `create_merge_request` - Create a new merge request in a GitLab project
+9. `fork_repository` - Fork a GitLab project to your account or specified namespace
+10. `create_branch` - Create a new branch in a GitLab project
+11. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided)
+12. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
+13. `list_merge_request_diffs` - List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)
+14. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
+15. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
+16. `create_note` - Create a new note (comment) to an issue or merge request
+17. `create_merge_request_thread` - Create a new thread on a merge request
+18. `mr_discussions` - List discussion items for a merge request
+19. `update_merge_request_note` - Modify an existing merge request thread note
+20. `create_merge_request_note` - Add a new note to an existing merge request thread
+21. `get_draft_note` - Get a single draft note from a merge request
+22. `list_draft_notes` - List draft notes for a merge request
+23. `create_draft_note` - Create a draft note for a merge request
+24. `update_draft_note` - Update an existing draft note
+25. `delete_draft_note` - Delete a draft note
+26. `publish_draft_note` - Publish a single draft note
+27. `bulk_publish_draft_notes` - Publish all draft notes for a merge request
+28. `update_issue_note` - Modify an existing issue thread note
+29. `create_issue_note` - Add a new note to an existing issue thread
+30. `list_issues` - List issues (default: created by current user only; use scope='all' for all accessible issues)
+31. `my_issues` - List issues assigned to the authenticated user (defaults to open issues)
+32. `get_issue` - Get details of a specific issue in a GitLab project
+33. `update_issue` - Update an issue in a GitLab project
+34. `delete_issue` - Delete an issue from a GitLab project
+35. `list_issue_links` - List all issue links for a specific issue
+36. `list_issue_discussions` - List discussions for an issue in a GitLab project
+37. `get_issue_link` - Get a specific issue link
+38. `create_issue_link` - Create an issue link between two issues
+39. `delete_issue_link` - Delete an issue link
+40. `list_namespaces` - List all namespaces available to the current user
+41. `get_namespace` - Get details of a namespace by ID or path
+42. `verify_namespace` - Verify if a namespace path exists
+43. `get_project` - Get details of a specific project
+44. `list_projects` - List projects accessible by the current user
+45. `list_project_members` - List members of a GitLab project
+46. `list_labels` - List labels for a project
+47. `get_label` - Get a single label from a project
+48. `create_label` - Create a new label in a project
+49. `update_label` - Update an existing label in a project
+50. `delete_label` - Delete a label from a project
+51. `list_group_projects` - List projects in a GitLab group with filtering options
+52. `list_wiki_pages` - List wiki pages in a GitLab project
+53. `get_wiki_page` - Get details of a specific wiki page
+54. `create_wiki_page` - Create a new wiki page in a GitLab project
+55. `update_wiki_page` - Update an existing wiki page in a GitLab project
+56. `delete_wiki_page` - Delete a wiki page from a GitLab project
+57. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
+58. `list_pipelines` - List pipelines in a GitLab project with filtering options
+59. `get_pipeline` - Get details of a specific pipeline in a GitLab project
+60. `list_pipeline_jobs` - List all jobs in a specific pipeline
+61. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
+62. `get_pipeline_job` - Get details of a GitLab pipeline job number
+63. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
+64. `create_pipeline` - Create a new pipeline for a branch or tag
+65. `retry_pipeline` - Retry a failed or canceled pipeline
+66. `cancel_pipeline` - Cancel a running pipeline
+67. `list_merge_requests` - List merge requests in a GitLab project with filtering options
+68. `list_milestones` - List milestones in a GitLab project with filtering options
+69. `get_milestone` - Get details of a specific milestone
+70. `create_milestone` - Create a new milestone in a GitLab project
+71. `edit_milestone` - Edit an existing milestone in a GitLab project
+72. `delete_milestone` - Delete a milestone from a GitLab project
+73. `get_milestone_issue` - Get issues associated with a specific milestone
+74. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
+75. `promote_milestone` - Promote a milestone to the next stage
+76. `get_milestone_burndown_events` - Get burndown events for a specific milestone
+77. `get_users` - Get GitLab user details by usernames
+78. `list_commits` - List repository commits with filtering options
+79. `get_commit` - Get details of a specific commit
+80. `get_commit_diff` - Get changes/diffs of a specific commit
+81. `list_group_iterations` - List group iterations with filtering options
+82. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
+83. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
+
+
+
diff --git a/build/index.js b/build/index.js
deleted file mode 100755
index e7e57dd..0000000
--- a/build/index.js
+++ /dev/null
@@ -1,539 +0,0 @@
-#!/usr/bin/env node
-import { Server } from "@modelcontextprotocol/sdk/server/index.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
-import fetch from "node-fetch";
-import { z } from "zod";
-import { zodToJsonSchema } from "zod-to-json-schema";
-import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, } from "./schemas.js";
-const server = new Server({
- name: "gitlab-mcp-server",
- version: "0.0.1",
-}, {
- capabilities: {
- tools: {},
- },
-});
-const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
-const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com/api/v4";
-if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
- console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
- process.exit(1);
-}
-// GitLab API 공통 헤더
-const DEFAULT_HEADERS = {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
-};
-// API 에러 처리를 위한 유틸리티 함수
-async function handleGitLabError(response) {
- if (!response.ok) {
- const errorBody = await response.text();
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
- }
-}
-// 프로젝트 포크 생성
-async function forkProject(projectId, namespace) {
- // API 엔드포인트 URL 생성
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork`);
- if (namespace) {
- url.searchParams.append("namespace", namespace);
- }
- const response = await fetch(url.toString(), {
- method: "POST",
- headers: DEFAULT_HEADERS,
- });
- // 이미 존재하는 프로젝트인 경우 처리
- if (response.status === 409) {
- throw new Error("Project already exists in the target namespace");
- }
- await handleGitLabError(response);
- const data = await response.json();
- return GitLabForkSchema.parse(data);
-}
-// 새로운 브랜치 생성
-async function createBranch(projectId, options) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/branches`);
- const response = await fetch(url.toString(), {
- method: "POST",
- headers: DEFAULT_HEADERS,
- body: JSON.stringify({
- branch: options.name,
- ref: options.ref,
- }),
- });
- await handleGitLabError(response);
- return GitLabReferenceSchema.parse(await response.json());
-}
-// 프로젝트의 기본 브랜치 조회
-async function getDefaultBranchRef(projectId) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}`);
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
- await handleGitLabError(response);
- const project = GitLabRepositorySchema.parse(await response.json());
- return project.default_branch ?? "main";
-}
-// 파일 내용 조회
-async function getFileContents(projectId, filePath, ref) {
- const encodedPath = encodeURIComponent(filePath);
- // ref가 없는 경우 default branch를 가져옴
- if (!ref) {
- ref = await getDefaultBranchRef(projectId);
- }
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
- url.searchParams.append("ref", ref);
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
- // 파일을 찾을 수 없는 경우 처리
- if (response.status === 404) {
- throw new Error(`File not found: ${filePath}`);
- }
- await handleGitLabError(response);
- const data = await response.json();
- const parsedData = GitLabContentSchema.parse(data);
- // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
- if (!Array.isArray(parsedData) && parsedData.content) {
- parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
- parsedData.encoding = "utf8";
- }
- return parsedData;
-}
-// 이슈 생성
-async function createIssue(projectId, options) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues`);
- const response = await fetch(url.toString(), {
- method: "POST",
- headers: DEFAULT_HEADERS,
- body: JSON.stringify({
- title: options.title,
- description: options.description,
- assignee_ids: options.assignee_ids,
- milestone_id: options.milestone_id,
- labels: options.labels?.join(","),
- }),
- });
- // 잘못된 요청 처리
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
- await handleGitLabError(response);
- const data = await response.json();
- return GitLabIssueSchema.parse(data);
-}
-async function createMergeRequest(projectId, options) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests`);
- 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({
- title: options.title,
- description: options.description,
- source_branch: options.source_branch,
- target_branch: options.target_branch,
- allow_collaboration: options.allow_collaboration,
- draft: options.draft,
- }),
- });
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
- 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();
- return GitLabMergeRequestSchema.parse(data);
-}
-async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath) {
- const encodedPath = encodeURIComponent(filePath);
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
- const body = {
- branch,
- content,
- commit_message: commitMessage,
- encoding: "text",
- ...(previousPath ? { previous_path: previousPath } : {}),
- };
- // Check if file exists
- let method = "POST";
- try {
- await getFileContents(projectId, filePath, branch);
- method = "PUT";
- }
- catch (error) {
- if (!(error instanceof Error && error.message.includes("File not found"))) {
- throw error;
- }
- // File doesn't exist, use POST
- }
- const response = await fetch(url.toString(), {
- method,
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
- body: JSON.stringify(body),
- });
- 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();
- return GitLabCreateUpdateFileResponseSchema.parse(data);
-}
-async function createTree(projectId, files, ref) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/tree`);
- if (ref) {
- url.searchParams.append("ref", ref);
- }
- 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({
- files: files.map((file) => ({
- file_path: file.path,
- content: file.content,
- encoding: "text",
- })),
- }),
- });
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
- 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();
- return GitLabTreeSchema.parse(data);
-}
-async function createCommit(projectId, message, branch, actions) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/commits`);
- 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({
- branch,
- commit_message: message,
- actions: actions.map((action) => ({
- action: "create",
- file_path: action.path,
- content: action.content,
- encoding: "text",
- })),
- }),
- });
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
- 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();
- return GitLabCommitSchema.parse(data);
-}
-async function searchProjects(query, page = 1, perPage = 20) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects`);
- url.searchParams.append("search", query);
- url.searchParams.append("page", page.toString());
- url.searchParams.append("per_page", perPage.toString());
- url.searchParams.append("order_by", "id");
- url.searchParams.append("sort", "desc");
- const response = await fetch(url.toString(), {
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
- });
- if (!response.ok) {
- const errorBody = await response.text();
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
- }
- const projects = (await response.json());
- const totalCount = response.headers.get("x-total");
- const totalPages = response.headers.get("x-total-pages");
- // GitLab API doesn't return these headers for results > 10,000
- const count = totalCount ? parseInt(totalCount) : projects.length;
- return GitLabSearchResponseSchema.parse({
- count,
- total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
- current_page: page,
- items: projects,
- });
-}
-async function createRepository(options) {
- const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
- method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
- body: JSON.stringify({
- name: options.name,
- description: options.description,
- visibility: options.visibility,
- initialize_with_readme: options.initialize_with_readme,
- default_branch: "main",
- path: options.name.toLowerCase().replace(/\s+/g, "-"),
- }),
- });
- 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();
- return GitLabRepositorySchema.parse(data);
-}
-// MR 조회 함수
-async function getMergeRequest(projectId, mergeRequestIid) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
- await handleGitLabError(response);
- return GitLabMergeRequestSchema.parse(await response.json());
-}
-// MR 변경사항 조회 함수
-async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
- if (view) {
- url.searchParams.append("view", view);
- }
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
- await handleGitLabError(response);
- const data = (await response.json());
- return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
-}
-// MR 업데이트 함수
-async function updateMergeRequest(projectId, mergeRequestIid, options) {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
- const response = await fetch(url.toString(), {
- method: "PUT",
- headers: DEFAULT_HEADERS,
- body: JSON.stringify(options),
- });
- await handleGitLabError(response);
- return GitLabMergeRequestSchema.parse(await response.json());
-}
-server.setRequestHandler(ListToolsRequestSchema, async () => {
- return {
- tools: [
- {
- name: "create_or_update_file",
- description: "Create or update a single file in a GitLab project",
- inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
- },
- {
- name: "search_repositories",
- description: "Search for GitLab projects",
- inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
- },
- {
- name: "create_repository",
- description: "Create a new GitLab project",
- inputSchema: zodToJsonSchema(CreateRepositorySchema),
- },
- {
- name: "get_file_contents",
- description: "Get the contents of a file or directory from a GitLab project",
- inputSchema: zodToJsonSchema(GetFileContentsSchema),
- },
- {
- name: "push_files",
- description: "Push multiple files to a GitLab project in a single commit",
- inputSchema: zodToJsonSchema(PushFilesSchema),
- },
- {
- name: "create_issue",
- description: "Create a new issue in a GitLab project",
- inputSchema: zodToJsonSchema(CreateIssueSchema),
- },
- {
- name: "create_merge_request",
- description: "Create a new merge request in a GitLab project",
- inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
- },
- {
- name: "fork_repository",
- description: "Fork a GitLab project to your account or specified namespace",
- inputSchema: zodToJsonSchema(ForkRepositorySchema),
- },
- {
- name: "create_branch",
- description: "Create a new branch in a GitLab project",
- inputSchema: zodToJsonSchema(CreateBranchSchema),
- },
- {
- name: "get_merge_request",
- description: "Get details of a merge request",
- inputSchema: zodToJsonSchema(GetMergeRequestSchema),
- },
- {
- name: "get_merge_request_diffs",
- description: "Get the changes/diffs of a merge request",
- inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
- },
- {
- name: "update_merge_request",
- description: "Update a merge request",
- inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
- },
- ],
- };
-});
-server.setRequestHandler(CallToolRequestSchema, async (request) => {
- try {
- if (!request.params.arguments) {
- throw new Error("Arguments are required");
- }
- switch (request.params.name) {
- case "fork_repository": {
- const args = ForkRepositorySchema.parse(request.params.arguments);
- const fork = await forkProject(args.project_id, args.namespace);
- return {
- content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
- };
- }
- case "create_branch": {
- const args = CreateBranchSchema.parse(request.params.arguments);
- let ref = args.ref;
- if (!ref) {
- ref = await getDefaultBranchRef(args.project_id);
- }
- const branch = await createBranch(args.project_id, {
- name: args.branch,
- ref,
- });
- return {
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
- };
- }
- case "search_repositories": {
- const args = SearchRepositoriesSchema.parse(request.params.arguments);
- const results = await searchProjects(args.search, args.page, args.per_page);
- return {
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
- };
- }
- case "create_repository": {
- const args = CreateRepositorySchema.parse(request.params.arguments);
- const repository = await createRepository(args);
- return {
- content: [
- { type: "text", text: JSON.stringify(repository, null, 2) },
- ],
- };
- }
- case "get_file_contents": {
- const args = GetFileContentsSchema.parse(request.params.arguments);
- const contents = await getFileContents(args.project_id, args.file_path, args.ref);
- return {
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
- };
- }
- case "create_or_update_file": {
- const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
- const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path);
- return {
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
- };
- }
- case "push_files": {
- const args = PushFilesSchema.parse(request.params.arguments);
- const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map((f) => ({ path: f.file_path, content: f.content })));
- return {
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
- };
- }
- case "create_issue": {
- const args = CreateIssueSchema.parse(request.params.arguments);
- const { project_id, ...options } = args;
- const issue = await createIssue(project_id, options);
- return {
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
- };
- }
- case "create_merge_request": {
- const args = CreateMergeRequestSchema.parse(request.params.arguments);
- const { project_id, ...options } = args;
- const mergeRequest = await createMergeRequest(project_id, options);
- return {
- content: [
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
- ],
- };
- }
- case "get_merge_request": {
- const args = GetMergeRequestSchema.parse(request.params.arguments);
- const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid);
- return {
- content: [
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
- ],
- };
- }
- case "get_merge_request_diffs": {
- const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
- const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.view);
- return {
- content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
- };
- }
- case "update_merge_request": {
- const args = UpdateMergeRequestSchema.parse(request.params.arguments);
- const { project_id, merge_request_iid, ...options } = args;
- const mergeRequest = await updateMergeRequest(project_id, merge_request_iid, options);
- return {
- content: [
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
- ],
- };
- }
- default:
- throw new Error(`Unknown tool: ${request.params.name}`);
- }
- }
- catch (error) {
- if (error instanceof z.ZodError) {
- throw new Error(`Invalid arguments: ${error.errors
- .map((e) => `${e.path.join(".")}: ${e.message}`)
- .join(", ")}`);
- }
- throw error;
- }
-});
-async function runServer() {
- const transport = new StdioServerTransport();
- await server.connect(transport);
- console.error("GitLab MCP Server running on stdio");
-}
-runServer().catch((error) => {
- console.error("Fatal error in main():", error);
- process.exit(1);
-});
diff --git a/build/schemas.js b/build/schemas.js
deleted file mode 100644
index d1eb081..0000000
--- a/build/schemas.js
+++ /dev/null
@@ -1,348 +0,0 @@
-import { z } from "zod";
-// Base schemas for common types
-export const GitLabAuthorSchema = z.object({
- name: z.string(),
- email: z.string(),
- date: z.string(),
-});
-// 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(),
- 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(),
-});
-// 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
- last_commit_id: z.string(), // 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 GitLabTreeEntrySchema = z.object({
- id: z.string(), // Changed from sha to match GitLab API
- name: z.string(),
- type: z.enum(["blob", "tree"]),
- path: z.string(),
- mode: z.string(),
-});
-export const GitLabTreeSchema = z.object({
- id: z.string(), // Changed from sha to match GitLab API
- tree: z.array(GitLabTreeEntrySchema),
-});
-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
- }),
-});
-// 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
- allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
- draft: z.boolean().optional(),
-});
-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
-});
-// Response schemas for operations
-export const GitLabCreateUpdateFileResponseSchema = z.object({
- file_path: z.string(),
- branch: z.string(),
- commit_id: z.string(), // Changed from sha to match GitLab 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),
-});
-// 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(),
- }),
- web_url: z.string(), // Changed from html_url to match GitLab API
-});
-export const GitLabForkSchema = GitLabRepositorySchema.extend({
- forked_from_project: GitLabForkParentSchema, // Changed from parent to match GitLab API
-});
-// Issue related schemas
-export const GitLabLabelSchema = z.object({
- id: z.number(),
- name: z.string(),
- color: z.string(),
- description: z.string().optional(),
-});
-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(),
- web_url: z.string(), // Changed from html_url to match GitLab API
-});
-export const GitLabMilestoneSchema = z.object({
- id: z.number(),
- iid: z.number(), // Added to match GitLab API
- title: z.string(),
- description: z.string(),
- 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(), // Changed from body to match GitLab API
- state: z.string(),
- author: GitLabUserSchema,
- assignees: z.array(GitLabUserSchema),
- labels: z.array(GitLabLabelSchema),
- 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
-});
-// 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(),
- source_branch: z.string(),
- target_branch: z.string(),
- diff_refs: GitLabMergeRequestDiffRefSchema.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().optional(),
- allow_collaboration: z.boolean().optional(),
- allow_maintainer_to_push: z.boolean().optional(),
- changes_count: z.string().optional(),
- merge_when_pipeline_succeeds: z.boolean().optional(),
- squash: z.boolean().optional(),
- labels: z.array(z.string()).optional(),
-});
-// API Operation Parameter Schemas
-const ProjectParamsSchema = z.object({
- project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API
-});
-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"),
-});
-export const SearchRepositoriesSchema = z.object({
- search: z.string().describe("Search query"), // Changed from query to match GitLab API
- page: z
- .number()
- .optional()
- .describe("Page number for pagination (default: 1)"),
- per_page: z
- .number()
- .optional()
- .describe("Number of results per page (default: 20)"),
-});
-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"),
- 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)"),
-});
-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 GitLabMergeRequestDiffSchema = 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(),
-});
-export const GetMergeRequestSchema = ProjectParamsSchema.extend({
- merge_request_iid: z
- .number()
- .describe("The internal ID of the merge request"),
-});
-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"),
-});
diff --git a/customSchemas.ts b/customSchemas.ts
new file mode 100644
index 0000000..e8546f1
--- /dev/null
+++ b/customSchemas.ts
@@ -0,0 +1,31 @@
+import { z } from "zod";
+import { pino } from 'pino';
+const DEFAULT_NULL = process.env.DEFAULT_NULL === "true";
+
+
+const logger = pino({
+ level: process.env.LOG_LEVEL || 'info',
+ transport: {
+ target: 'pino-pretty',
+ options: {
+ colorize: true,
+ levelFirst: true,
+ destination: 2,
+ },
+ },
+});
+
+export const flexibleBoolean = z.preprocess(val => {
+ if (typeof val === "boolean") {
+ return val;
+ }
+ let result = "false";
+ try {
+ result = String(val).toLowerCase();
+ } catch {
+ return false;
+ }
+ return ["true", "t", "1"].includes(result);
+}, z.boolean());
+
+export const flexibleBooleanNullable = DEFAULT_NULL ? flexibleBoolean.nullable().default(null) : flexibleBoolean.nullable();
diff --git a/docs/setup-github-secrets.md b/docs/setup-github-secrets.md
new file mode 100644
index 0000000..e6b465e
--- /dev/null
+++ b/docs/setup-github-secrets.md
@@ -0,0 +1,57 @@
+# 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!
diff --git a/event.json b/event.json
new file mode 100644
index 0000000..1010827
--- /dev/null
+++ b/event.json
@@ -0,0 +1,6 @@
+{
+ "action": "published",
+ "release": {
+ "tag_name": "v1.0.53"
+ }
+}
diff --git a/index.ts b/index.ts
index 7d4eecf..370e29d 100644
--- a/index.ts
+++ b/index.ts
@@ -1,62 +1,240 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import {
- CallToolRequestSchema,
- ListToolsRequestSchema,
-} from "@modelcontextprotocol/sdk/types.js";
-import fetch from "node-fetch";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
+import express, { Request, Response } from "express";
+import fetchCookie from "fetch-cookie";
+import fs from "fs";
+import { HttpProxyAgent } from "http-proxy-agent";
+import { HttpsProxyAgent } from "https-proxy-agent";
+import nodeFetch from "node-fetch";
+import path, { dirname } from "path";
+import { SocksProxyAgent } from "socks-proxy-agent";
+import { CookieJar, parse as parseCookie } from "tough-cookie";
+import { fileURLToPath } from "url";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
-import { fileURLToPath } from "url";
-import { dirname, resolve } from "path";
+// Add type imports for proxy agents
+import { Agent } from "http";
+import { Agent as HttpsAgent } from "https";
+import { URL } from "url";
import {
- GitLabForkSchema,
- GitLabReferenceSchema,
- GitLabRepositorySchema,
- GitLabIssueSchema,
- GitLabMergeRequestSchema,
- GitLabContentSchema,
- GitLabCreateUpdateFileResponseSchema,
- GitLabSearchResponseSchema,
- GitLabTreeSchema,
- GitLabCommitSchema,
- CreateRepositoryOptionsSchema,
+ BulkPublishDraftNotesSchema,
+ CancelPipelineSchema,
+ CreateBranchOptionsSchema,
+ CreateBranchSchema,
+ CreateDraftNoteSchema,
+ CreateIssueLinkSchema,
+ CreateIssueNoteSchema,
CreateIssueOptionsSchema,
+ CreateIssueSchema,
+ CreateLabelSchema, // Added
+ CreateMergeRequestNoteSchema,
CreateMergeRequestOptionsSchema,
- CreateBranchOptionsSchema,
+ CreateMergeRequestSchema,
+ CreateMergeRequestThreadSchema,
+ CreateNoteSchema,
CreateOrUpdateFileSchema,
- SearchRepositoriesSchema,
+ CreatePipelineSchema,
+ CreateProjectMilestoneSchema,
+ CreateRepositoryOptionsSchema,
CreateRepositorySchema,
- GetFileContentsSchema,
- PushFilesSchema,
- CreateIssueSchema,
- CreateMergeRequestSchema,
+ CreateWikiPageSchema,
+ DeleteDraftNoteSchema,
+ DeleteIssueLinkSchema,
+ DeleteIssueSchema,
+ DeleteLabelSchema,
+ DeleteProjectMilestoneSchema,
+ DeleteWikiPageSchema,
+ EditProjectMilestoneSchema,
+ type FileOperation,
ForkRepositorySchema,
- CreateBranchSchema,
- GitLabMergeRequestDiffSchema,
- GetMergeRequestSchema,
+ GetBranchDiffsSchema,
+ GetCommitDiffSchema,
+ GetCommitSchema,
+ GetDraftNoteSchema,
+ GetFileContentsSchema,
+ GetIssueLinkSchema,
+ GetIssueSchema,
+ GetLabelSchema,
GetMergeRequestDiffsSchema,
- UpdateMergeRequestSchema,
+ GetMergeRequestSchema,
+ GetMilestoneBurndownEventsSchema,
+ GetMilestoneIssuesSchema,
+ GetMilestoneMergeRequestsSchema,
+ GetNamespaceSchema,
+ // pipeline job schemas
+ GetPipelineJobOutputSchema,
+ GetPipelineSchema,
+ GetProjectMilestoneSchema,
+ GetProjectSchema,
+ type GetRepositoryTreeOptions,
+ GetRepositoryTreeSchema,
+ GetUsersSchema,
+ GetWikiPageSchema,
+ type GitLabCommit,
+ GitLabCommitSchema,
+ GitLabCompareResult,
+ GitLabCompareResultSchema,
+ type GitLabContent,
+ GitLabContentSchema,
+ type GitLabCreateUpdateFileResponse,
+ GitLabCreateUpdateFileResponseSchema,
+ GitLabDiffSchema,
+ type GitLabDiscussion,
+ // Discussion Types
+ type GitLabDiscussionNote,
+ // Discussion Schemas
+ GitLabDiscussionNoteSchema, // Added
+ GitLabDiscussionSchema,
+ // Draft Notes Types
+ type GitLabDraftNote,
+ // Draft Notes Schemas
+ GitLabDraftNoteSchema,
type GitLabFork,
- type GitLabReference,
- type GitLabRepository,
+ GitLabForkSchema,
type GitLabIssue,
+ type GitLabIssueLink,
+ GitLabIssueLinkSchema,
+ GitLabIssueSchema,
+ type GitLabIssueWithLinkDetails,
+ GitLabIssueWithLinkDetailsSchema,
+ type GitLabLabel,
+ GitLabMarkdownUpload,
+ GitLabMarkdownUploadSchema,
type GitLabMergeRequest,
- type GitLabContent,
- type GitLabCreateUpdateFileResponse,
+ type GitLabMergeRequestDiff,
+ GitLabMergeRequestSchema,
+ type GitLabMilestones,
+ GitLabMilestonesSchema,
+ type GitLabNamespace,
+ type GitLabNamespaceExistsResponse,
+ GitLabNamespaceExistsResponseSchema,
+ GitLabNamespaceSchema,
+ type GitLabPipeline,
+ type GitLabPipelineJob,
+ GitLabPipelineJobSchema,
+ GitLabPipelineSchema,
+ type GitLabPipelineTriggerJob,
+ GitLabPipelineTriggerJobSchema,
+ type GitLabProject,
+ type GitLabProjectMember,
+ GitLabProjectMemberSchema,
+ GitLabProjectSchema,
+ type GitLabReference,
+ GitLabReferenceSchema,
+ type GitLabRepository,
+ GitLabRepositorySchema,
type GitLabSearchResponse,
+ GitLabSearchResponseSchema,
type GitLabTree,
- type GitLabCommit,
- type FileOperation,
- type GitLabMergeRequestDiff,
+ type GitLabTreeItem,
+ GitLabTreeItemSchema,
+ GitLabTreeSchema,
+ type GitLabUser,
+ GitLabUserSchema,
+ type GitLabUsersResponse,
+ GitLabUsersResponseSchema,
+ type GitLabWikiPage,
+ GitLabWikiPageSchema,
+ GroupIteration,
+ type ListCommitsOptions,
+ ListCommitsSchema,
+ ListDraftNotesSchema,
+ ListGroupIterationsSchema,
+ ListGroupProjectsSchema,
+ ListIssueDiscussionsSchema,
+ ListIssueLinksSchema,
+ ListIssuesSchema,
+ ListLabelsSchema,
+ ListMergeRequestDiffsSchema, // Added
+ ListMergeRequestDiscussionsSchema,
+ ListMergeRequestsSchema,
+ ListNamespacesSchema,
+ type ListPipelineJobsOptions,
+ ListPipelineJobsSchema,
+ type ListPipelinesOptions,
+ ListPipelinesSchema,
+ type ListPipelineTriggerJobsOptions,
+ ListPipelineTriggerJobsSchema,
+ type ListProjectMembersOptions,
+ ListProjectMembersSchema,
+ ListProjectMilestonesSchema,
+ ListProjectsSchema,
+ ListWikiPagesOptions,
+ ListWikiPagesSchema,
+ MarkdownUploadSchema,
+ DownloadAttachmentSchema,
+ MergeMergeRequestSchema,
+ type MergeRequestThreadPosition,
+ type MergeRequestThreadPositionCreate,
+ type MyIssuesOptions,
+ MyIssuesSchema,
+ type PaginatedDiscussionsResponse,
+ PaginatedDiscussionsResponseSchema,
+ type PaginationOptions,
+ PromoteProjectMilestoneSchema,
+ PublishDraftNoteSchema,
+ PushFilesSchema,
+ RetryPipelineSchema,
+ SearchRepositoriesSchema,
+ UpdateDraftNoteSchema,
+ UpdateIssueNoteSchema,
+ UpdateIssueSchema,
+ UpdateLabelSchema,
+ UpdateMergeRequestNoteSchema,
+ UpdateMergeRequestSchema,
+ UpdateWikiPageSchema,
+ VerifyNamespaceSchema
} from "./schemas.js";
+import { randomUUID } from "crypto";
+import { pino } from "pino";
+
+const logger = pino({
+ level: process.env.LOG_LEVEL || "info",
+ transport: {
+ target: "pino-pretty",
+ options: {
+ colorize: true,
+ levelFirst: true,
+ destination: 2,
+ },
+ },
+});
+
+/**
+ * Available transport modes for MCP server
+ */
+enum TransportMode {
+ STDIO = "stdio",
+ SSE = "sse",
+ STREAMABLE_HTTP = "streamable-http",
+}
+
+/**
+ * Read version from package.json
+ */
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const packageJsonPath = path.resolve(__dirname, "../package.json");
+let SERVER_VERSION = "unknown";
+try {
+ if (fs.existsSync(packageJsonPath)) {
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
+ SERVER_VERSION = packageJson.version || SERVER_VERSION;
+ }
+} catch (error) {
+ // Warning: Could not read version from package.json - silently continue
+}
+
const server = new Server(
{
- name: "gitlab-mcp-server",
- version: "0.0.1",
+ name: "better-gitlab-mcp-server",
+ version: SERVER_VERSION,
},
{
capabilities: {
@@ -66,745 +244,4947 @@ const server = new Server(
);
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
-const GITLAB_API_URL =
- process.env.GITLAB_API_URL || "https://gitlab.com/api/v4";
+const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH;
+const IS_OLD = process.env.GITLAB_IS_OLD === "true";
+const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
+const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
+const USE_MILESTONE = process.env.USE_MILESTONE === "true";
+const USE_PIPELINE = process.env.USE_PIPELINE === "true";
+const SSE = process.env.SSE === "true";
+const STREAMABLE_HTTP = process.env.STREAMABLE_HTTP === "true";
+const HOST = process.env.HOST || "0.0.0.0";
+const PORT = process.env.PORT || 3002;
+// Add proxy configuration
+const HTTP_PROXY = process.env.HTTP_PROXY;
+const HTTPS_PROXY = process.env.HTTPS_PROXY;
+const NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
+const GITLAB_CA_CERT_PATH = process.env.GITLAB_CA_CERT_PATH;
-if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
- console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
- process.exit(1);
+let sslOptions = undefined;
+if (NODE_TLS_REJECT_UNAUTHORIZED === "0") {
+ sslOptions = { rejectUnauthorized: false };
+} else if (GITLAB_CA_CERT_PATH) {
+ const ca = fs.readFileSync(GITLAB_CA_CERT_PATH);
+ sslOptions = { ca };
}
-// GitLab API 공통 헤더
-const DEFAULT_HEADERS = {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
-};
+// Configure proxy agents if proxies are set
+let httpAgent: Agent | undefined = undefined;
+let httpsAgent: Agent | undefined = undefined;
-// API 에러 처리를 위한 유틸리티 함수
-async function handleGitLabError(
- response: import("node-fetch").Response
-): Promise {
- if (!response.ok) {
- const errorBody = await response.text();
- throw new Error(
- `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
- );
+if (HTTP_PROXY) {
+ if (HTTP_PROXY.startsWith("socks")) {
+ httpAgent = new SocksProxyAgent(HTTP_PROXY);
+ } else {
+ httpAgent = new HttpProxyAgent(HTTP_PROXY);
}
}
-
-// 프로젝트 포크 생성
-async function forkProject(
- projectId: string,
- namespace?: string
-): Promise {
- // API 엔드포인트 URL 생성
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork`
- );
-
- if (namespace) {
- url.searchParams.append("namespace", namespace);
- }
-
- const response = await fetch(url.toString(), {
- method: "POST",
- headers: DEFAULT_HEADERS,
- });
-
- // 이미 존재하는 프로젝트인 경우 처리
- if (response.status === 409) {
- throw new Error("Project already exists in the target namespace");
+if (HTTPS_PROXY) {
+ if (HTTPS_PROXY.startsWith("socks")) {
+ httpsAgent = new SocksProxyAgent(HTTPS_PROXY);
+ } else {
+ httpsAgent = new HttpsProxyAgent(HTTPS_PROXY, sslOptions);
}
-
- await handleGitLabError(response);
- const data = await response.json();
- return GitLabForkSchema.parse(data);
-}
-
-// 새로운 브랜치 생성
-async function createBranch(
- projectId: string,
- options: z.infer
-): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/repository/branches`
- );
-
- const response = await fetch(url.toString(), {
- method: "POST",
- headers: DEFAULT_HEADERS,
- body: JSON.stringify({
- branch: options.name,
- ref: options.ref,
- }),
- });
-
- await handleGitLabError(response);
- return GitLabReferenceSchema.parse(await response.json());
}
+httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
+httpAgent = httpAgent || new Agent();
-// 프로젝트의 기본 브랜치 조회
-async function getDefaultBranchRef(projectId: string): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}`
- );
-
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
+// Create cookie jar with clean Netscape file parsing
+const createCookieJar = (): CookieJar | null => {
+ if (!GITLAB_AUTH_COOKIE_PATH) return null;
- await handleGitLabError(response);
- const project = GitLabRepositorySchema.parse(await response.json());
- return project.default_branch ?? "main";
-}
+ try {
+ const cookiePath = GITLAB_AUTH_COOKIE_PATH.startsWith("~/")
+ ? path.join(process.env.HOME || "", GITLAB_AUTH_COOKIE_PATH.slice(2))
+ : GITLAB_AUTH_COOKIE_PATH;
-// 파일 내용 조회
-async function getFileContents(
- projectId: string,
- filePath: string,
- ref?: string
-): Promise {
- const encodedPath = encodeURIComponent(filePath);
+ const jar = new CookieJar();
+ const cookieContent = fs.readFileSync(cookiePath, "utf8");
- // ref가 없는 경우 default branch를 가져옴
- if (!ref) {
- ref = await getDefaultBranchRef(projectId);
- }
+ cookieContent.split("\n").forEach(line => {
+ // Handle #HttpOnly_ prefix
+ if (line.startsWith("#HttpOnly_")) {
+ line = line.slice(10);
+ }
+ // Skip comments and empty lines
+ if (line.startsWith("#") || !line.trim()) {
+ return;
+ }
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/repository/files/${encodedPath}`
- );
+ // Parse Netscape format: domain, flag, path, secure, expires, name, value
+ const parts = line.split("\t");
+ if (parts.length >= 7) {
+ const [domain, , path, secure, expires, name, value] = parts;
- url.searchParams.append("ref", ref);
+ // Build cookie string in standard format
+ const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`;
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
+ // Use tough-cookie's parse function for robust parsing
+ const cookie = parseCookie(cookieStr);
+ if (cookie) {
+ const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`;
+ jar.setCookieSync(cookie, url);
+ }
+ }
+ });
- // 파일을 찾을 수 없는 경우 처리
- if (response.status === 404) {
- throw new Error(`File not found: ${filePath}`);
+ return jar;
+ } catch (error) {
+ logger.error("Error loading cookie file:", error);
+ return null;
}
+};
- await handleGitLabError(response);
- const data = await response.json();
- const parsedData = GitLabContentSchema.parse(data);
+// Initialize cookie jar and fetch
+const cookieJar = createCookieJar();
+const fetch = cookieJar ? fetchCookie(nodeFetch, cookieJar) : nodeFetch;
- // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
- if (!Array.isArray(parsedData) && parsedData.content) {
- parsedData.content = Buffer.from(parsedData.content, "base64").toString(
- "utf8"
- );
- parsedData.encoding = "utf8";
- }
+// Ensure session is established for the current request
+async function ensureSessionForRequest(): Promise {
+ if (!cookieJar || !GITLAB_AUTH_COOKIE_PATH) return;
- return parsedData;
-}
+ // Extract the base URL from GITLAB_API_URL
+ const apiUrl = new URL(GITLAB_API_URL);
+ const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`;
-// 이슈 생성
-async function createIssue(
- projectId: string,
- options: z.infer
-): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues`
+ // Check if we already have GitLab session cookies
+ const gitlabCookies = cookieJar.getCookiesSync(baseUrl);
+ const hasSessionCookie = gitlabCookies.some(
+ cookie => cookie.key === "_gitlab_session" || cookie.key === "remember_user_token"
);
- const response = await fetch(url.toString(), {
- method: "POST",
- headers: DEFAULT_HEADERS,
- body: JSON.stringify({
- title: options.title,
- description: options.description,
- assignee_ids: options.assignee_ids,
- milestone_id: options.milestone_id,
- labels: options.labels?.join(","),
- }),
- });
+ if (!hasSessionCookie) {
+ try {
+ // Establish session with a lightweight request
+ await fetch(`${GITLAB_API_URL}/user`, {
+ ...DEFAULT_FETCH_CONFIG,
+ redirect: "follow",
+ }).catch(() => {
+ // Ignore errors - the important thing is that cookies get set during redirects
+ });
- // 잘못된 요청 처리
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
+ // Small delay to ensure cookies are fully processed
+ await new Promise(resolve => setTimeout(resolve, 100));
+ } catch (error) {
+ // Ignore session establishment errors
+ }
}
-
- await handleGitLabError(response);
- const data = await response.json();
- return GitLabIssueSchema.parse(data);
}
-async function createMergeRequest(
- projectId: string,
- options: z.infer
-): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/merge_requests`
- );
-
- 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({
- title: options.title,
- description: options.description,
- source_branch: options.source_branch,
- target_branch: options.target_branch,
- allow_collaboration: options.allow_collaboration,
- draft: options.draft,
- }),
- });
-
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
-
- 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();
- return GitLabMergeRequestSchema.parse(data);
+// Modify DEFAULT_HEADERS to include agent configuration
+const DEFAULT_HEADERS: Record = {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+};
+if (IS_OLD) {
+ DEFAULT_HEADERS["Private-Token"] = `${GITLAB_PERSONAL_ACCESS_TOKEN}`;
+} else {
+ DEFAULT_HEADERS["Authorization"] = `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`;
}
-async function createOrUpdateFile(
- projectId: string,
- filePath: string,
- content: string,
- commitMessage: string,
- branch: string,
- previousPath?: string
-): Promise {
- const encodedPath = encodeURIComponent(filePath);
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/repository/files/${encodedPath}`
- );
-
- const body = {
- branch,
- content,
- commit_message: commitMessage,
- encoding: "text",
- ...(previousPath ? { previous_path: previousPath } : {}),
- };
-
- // Check if file exists
- let method = "POST";
- try {
- await getFileContents(projectId, filePath, branch);
- method = "PUT";
- } catch (error) {
- if (!(error instanceof Error && error.message.includes("File not found"))) {
- throw error;
+// Create a default fetch configuration object that includes proxy agents if set
+const DEFAULT_FETCH_CONFIG = {
+ headers: DEFAULT_HEADERS,
+ agent: (parsedUrl: URL) => {
+ if (parsedUrl.protocol === "https:") {
+ return httpsAgent;
}
- // File doesn't exist, use POST
- }
+ return httpAgent;
+ },
+};
- const response = await fetch(url.toString(), {
- method,
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
- body: JSON.stringify(body),
- });
+// Define all available tools
+const allTools = [
+ {
+ name: "merge_merge_request",
+ description: "Merge a merge request in a GitLab project",
+ inputSchema: zodToJsonSchema(MergeMergeRequestSchema),
+ },
+ {
+ name: "create_or_update_file",
+ description: "Create or update a single file in a GitLab project",
+ inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
+ },
+ {
+ name: "search_repositories",
+ description: "Search for GitLab projects",
+ inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
+ },
+ {
+ name: "create_repository",
+ description: "Create a new GitLab project",
+ inputSchema: zodToJsonSchema(CreateRepositorySchema),
+ },
+ {
+ name: "get_file_contents",
+ description: "Get the contents of a file or directory from a GitLab project",
+ inputSchema: zodToJsonSchema(GetFileContentsSchema),
+ },
+ {
+ name: "push_files",
+ description: "Push multiple files to a GitLab project in a single commit",
+ inputSchema: zodToJsonSchema(PushFilesSchema),
+ },
+ {
+ name: "create_issue",
+ description: "Create a new issue in a GitLab project",
+ inputSchema: zodToJsonSchema(CreateIssueSchema),
+ },
+ {
+ name: "create_merge_request",
+ description: "Create a new merge request in a GitLab project",
+ inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
+ },
+ {
+ name: "fork_repository",
+ description: "Fork a GitLab project to your account or specified namespace",
+ inputSchema: zodToJsonSchema(ForkRepositorySchema),
+ },
+ {
+ name: "create_branch",
+ description: "Create a new branch in a GitLab project",
+ inputSchema: zodToJsonSchema(CreateBranchSchema),
+ },
+ {
+ name: "get_merge_request",
+ description:
+ "Get details of a merge request (Either mergeRequestIid or branchName must be provided)",
+ inputSchema: zodToJsonSchema(GetMergeRequestSchema),
+ },
+ {
+ name: "get_merge_request_diffs",
+ description:
+ "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
+ inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
+ },
+ {
+ name: "list_merge_request_diffs",
+ description:
+ "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
+ inputSchema: zodToJsonSchema(ListMergeRequestDiffsSchema),
+ },
+ {
+ name: "get_branch_diffs",
+ description: "Get the changes/diffs between two branches or commits in a GitLab project",
+ inputSchema: zodToJsonSchema(GetBranchDiffsSchema),
+ },
+ {
+ name: "update_merge_request",
+ description: "Update a merge request (Either mergeRequestIid or branchName must be provided)",
+ inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
+ },
+ {
+ name: "create_note",
+ description: "Create a new note (comment) to an issue or merge request",
+ inputSchema: zodToJsonSchema(CreateNoteSchema),
+ },
+ {
+ name: "create_merge_request_thread",
+ description: "Create a new thread on a merge request",
+ inputSchema: zodToJsonSchema(CreateMergeRequestThreadSchema),
+ },
+ {
+ name: "mr_discussions",
+ description: "List discussion items for a merge request",
+ inputSchema: zodToJsonSchema(ListMergeRequestDiscussionsSchema),
+ },
+ {
+ name: "update_merge_request_note",
+ description: "Modify an existing merge request thread note",
+ inputSchema: zodToJsonSchema(UpdateMergeRequestNoteSchema),
+ },
+ {
+ name: "create_merge_request_note",
+ description: "Add a new note to an existing merge request thread",
+ inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema),
+ },
+ {
+ name: "get_draft_note",
+ description: "Get a single draft note from a merge request",
+ inputSchema: zodToJsonSchema(GetDraftNoteSchema),
+ },
+ {
+ name: "list_draft_notes",
+ description: "List draft notes for a merge request",
+ inputSchema: zodToJsonSchema(ListDraftNotesSchema),
+ },
+ {
+ name: "create_draft_note",
+ description: "Create a draft note for a merge request",
+ inputSchema: zodToJsonSchema(CreateDraftNoteSchema),
+ },
+ {
+ name: "update_draft_note",
+ description: "Update an existing draft note",
+ inputSchema: zodToJsonSchema(UpdateDraftNoteSchema),
+ },
+ {
+ name: "delete_draft_note",
+ description: "Delete a draft note",
+ inputSchema: zodToJsonSchema(DeleteDraftNoteSchema),
+ },
+ {
+ name: "publish_draft_note",
+ description: "Publish a single draft note",
+ inputSchema: zodToJsonSchema(PublishDraftNoteSchema),
+ },
+ {
+ name: "bulk_publish_draft_notes",
+ description: "Publish all draft notes for a merge request",
+ inputSchema: zodToJsonSchema(BulkPublishDraftNotesSchema),
+ },
+ {
+ name: "update_issue_note",
+ description: "Modify an existing issue thread note",
+ inputSchema: zodToJsonSchema(UpdateIssueNoteSchema),
+ },
+ {
+ name: "create_issue_note",
+ description: "Add a new note to an existing issue thread",
+ inputSchema: zodToJsonSchema(CreateIssueNoteSchema),
+ },
+ {
+ name: "list_issues",
+ description:
+ "List issues (default: created by current user only; use scope='all' for all accessible issues)",
+ inputSchema: zodToJsonSchema(ListIssuesSchema),
+ },
+ {
+ name: "my_issues",
+ description: "List issues assigned to the authenticated user (defaults to open issues)",
+ inputSchema: zodToJsonSchema(MyIssuesSchema),
+ },
+ {
+ name: "get_issue",
+ description: "Get details of a specific issue in a GitLab project",
+ inputSchema: zodToJsonSchema(GetIssueSchema),
+ },
+ {
+ name: "update_issue",
+ description: "Update an issue in a GitLab project",
+ inputSchema: zodToJsonSchema(UpdateIssueSchema),
+ },
+ {
+ name: "delete_issue",
+ description: "Delete an issue from a GitLab project",
+ inputSchema: zodToJsonSchema(DeleteIssueSchema),
+ },
+ {
+ name: "list_issue_links",
+ description: "List all issue links for a specific issue",
+ inputSchema: zodToJsonSchema(ListIssueLinksSchema),
+ },
+ {
+ name: "list_issue_discussions",
+ description: "List discussions for an issue in a GitLab project",
+ inputSchema: zodToJsonSchema(ListIssueDiscussionsSchema),
+ },
+ {
+ name: "get_issue_link",
+ description: "Get a specific issue link",
+ inputSchema: zodToJsonSchema(GetIssueLinkSchema),
+ },
+ {
+ name: "create_issue_link",
+ description: "Create an issue link between two issues",
+ inputSchema: zodToJsonSchema(CreateIssueLinkSchema),
+ },
+ {
+ name: "delete_issue_link",
+ description: "Delete an issue link",
+ inputSchema: zodToJsonSchema(DeleteIssueLinkSchema),
+ },
+ {
+ name: "list_namespaces",
+ description: "List all namespaces available to the current user",
+ inputSchema: zodToJsonSchema(ListNamespacesSchema),
+ },
+ {
+ name: "get_namespace",
+ description: "Get details of a namespace by ID or path",
+ inputSchema: zodToJsonSchema(GetNamespaceSchema),
+ },
+ {
+ name: "verify_namespace",
+ description: "Verify if a namespace path exists",
+ inputSchema: zodToJsonSchema(VerifyNamespaceSchema),
+ },
+ {
+ name: "get_project",
+ description: "Get details of a specific project",
+ inputSchema: zodToJsonSchema(GetProjectSchema),
+ },
+ {
+ name: "list_projects",
+ description: "List projects accessible by the current user",
+ inputSchema: zodToJsonSchema(ListProjectsSchema),
+ },
+ {
+ name: "list_project_members",
+ description: "List members of a GitLab project",
+ inputSchema: zodToJsonSchema(ListProjectMembersSchema),
+ },
+ {
+ name: "list_labels",
+ description: "List labels for a project",
+ inputSchema: zodToJsonSchema(ListLabelsSchema),
+ },
+ {
+ name: "get_label",
+ description: "Get a single label from a project",
+ inputSchema: zodToJsonSchema(GetLabelSchema),
+ },
+ {
+ name: "create_label",
+ description: "Create a new label in a project",
+ inputSchema: zodToJsonSchema(CreateLabelSchema),
+ },
+ {
+ name: "update_label",
+ description: "Update an existing label in a project",
+ inputSchema: zodToJsonSchema(UpdateLabelSchema),
+ },
+ {
+ name: "delete_label",
+ description: "Delete a label from a project",
+ inputSchema: zodToJsonSchema(DeleteLabelSchema),
+ },
+ {
+ name: "list_group_projects",
+ description: "List projects in a GitLab group with filtering options",
+ inputSchema: zodToJsonSchema(ListGroupProjectsSchema),
+ },
+ {
+ name: "list_wiki_pages",
+ description: "List wiki pages in a GitLab project",
+ inputSchema: zodToJsonSchema(ListWikiPagesSchema),
+ },
+ {
+ name: "get_wiki_page",
+ description: "Get details of a specific wiki page",
+ inputSchema: zodToJsonSchema(GetWikiPageSchema),
+ },
+ {
+ name: "create_wiki_page",
+ description: "Create a new wiki page in a GitLab project",
+ inputSchema: zodToJsonSchema(CreateWikiPageSchema),
+ },
+ {
+ name: "update_wiki_page",
+ description: "Update an existing wiki page in a GitLab project",
+ inputSchema: zodToJsonSchema(UpdateWikiPageSchema),
+ },
+ {
+ name: "delete_wiki_page",
+ description: "Delete a wiki page from a GitLab project",
+ inputSchema: zodToJsonSchema(DeleteWikiPageSchema),
+ },
+ {
+ name: "get_repository_tree",
+ description: "Get the repository tree for a GitLab project (list files and directories)",
+ inputSchema: zodToJsonSchema(GetRepositoryTreeSchema),
+ },
+ {
+ name: "list_pipelines",
+ description: "List pipelines in a GitLab project with filtering options",
+ inputSchema: zodToJsonSchema(ListPipelinesSchema),
+ },
+ {
+ name: "get_pipeline",
+ description: "Get details of a specific pipeline in a GitLab project",
+ inputSchema: zodToJsonSchema(GetPipelineSchema),
+ },
+ {
+ name: "list_pipeline_jobs",
+ description: "List all jobs in a specific pipeline",
+ inputSchema: zodToJsonSchema(ListPipelineJobsSchema),
+ },
+ {
+ name: "list_pipeline_trigger_jobs",
+ description:
+ "List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines",
+ inputSchema: zodToJsonSchema(ListPipelineTriggerJobsSchema),
+ },
+ {
+ name: "get_pipeline_job",
+ description: "Get details of a GitLab pipeline job number",
+ inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
+ },
+ {
+ name: "get_pipeline_job_output",
+ description:
+ "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
+ inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
+ },
+ {
+ name: "create_pipeline",
+ description: "Create a new pipeline for a branch or tag",
+ inputSchema: zodToJsonSchema(CreatePipelineSchema),
+ },
+ {
+ name: "retry_pipeline",
+ description: "Retry a failed or canceled pipeline",
+ inputSchema: zodToJsonSchema(RetryPipelineSchema),
+ },
+ {
+ name: "cancel_pipeline",
+ description: "Cancel a running pipeline",
+ inputSchema: zodToJsonSchema(CancelPipelineSchema),
+ },
+ {
+ name: "list_merge_requests",
+ description: "List merge requests in a GitLab project with filtering options",
+ inputSchema: zodToJsonSchema(ListMergeRequestsSchema),
+ },
+ {
+ name: "list_milestones",
+ description: "List milestones in a GitLab project with filtering options",
+ inputSchema: zodToJsonSchema(ListProjectMilestonesSchema),
+ },
+ {
+ name: "get_milestone",
+ description: "Get details of a specific milestone",
+ inputSchema: zodToJsonSchema(GetProjectMilestoneSchema),
+ },
+ {
+ name: "create_milestone",
+ description: "Create a new milestone in a GitLab project",
+ inputSchema: zodToJsonSchema(CreateProjectMilestoneSchema),
+ },
+ {
+ name: "edit_milestone",
+ description: "Edit an existing milestone in a GitLab project",
+ inputSchema: zodToJsonSchema(EditProjectMilestoneSchema),
+ },
+ {
+ name: "delete_milestone",
+ description: "Delete a milestone from a GitLab project",
+ inputSchema: zodToJsonSchema(DeleteProjectMilestoneSchema),
+ },
+ {
+ name: "get_milestone_issue",
+ description: "Get issues associated with a specific milestone",
+ inputSchema: zodToJsonSchema(GetMilestoneIssuesSchema),
+ },
+ {
+ name: "get_milestone_merge_requests",
+ description: "Get merge requests associated with a specific milestone",
+ inputSchema: zodToJsonSchema(GetMilestoneMergeRequestsSchema),
+ },
+ {
+ name: "promote_milestone",
+ description: "Promote a milestone to the next stage",
+ inputSchema: zodToJsonSchema(PromoteProjectMilestoneSchema),
+ },
+ {
+ name: "get_milestone_burndown_events",
+ description: "Get burndown events for a specific milestone",
+ inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema),
+ },
+ {
+ name: "get_users",
+ description: "Get GitLab user details by usernames",
+ inputSchema: zodToJsonSchema(GetUsersSchema),
+ },
+ {
+ name: "list_commits",
+ description: "List repository commits with filtering options",
+ inputSchema: zodToJsonSchema(ListCommitsSchema),
+ },
+ {
+ name: "get_commit",
+ description: "Get details of a specific commit",
+ inputSchema: zodToJsonSchema(GetCommitSchema),
+ },
+ {
+ name: "get_commit_diff",
+ description: "Get changes/diffs of a specific commit",
+ inputSchema: zodToJsonSchema(GetCommitDiffSchema),
+ },
+ {
+ name: "list_group_iterations",
+ description: "List group iterations with filtering options",
+ inputSchema: zodToJsonSchema(ListGroupIterationsSchema),
+ },
+ {
+ name: "upload_markdown",
+ description: "Upload a file to a GitLab project for use in markdown content",
+ inputSchema: zodToJsonSchema(MarkdownUploadSchema),
+ },
+ {
+ name: "download_attachment",
+ description: "Download an uploaded file from a GitLab project by secret and filename",
+ inputSchema: zodToJsonSchema(DownloadAttachmentSchema),
+ },
+];
+
+// Define which tools are read-only
+const readOnlyTools = [
+ "search_repositories",
+ "get_file_contents",
+ "get_merge_request",
+ "get_merge_request_diffs",
+ "get_branch_diffs",
+ "mr_discussions",
+ "list_issues",
+ "my_issues",
+ "list_merge_requests",
+ "get_issue",
+ "list_issue_links",
+ "list_issue_discussions",
+ "get_issue_link",
+ "list_namespaces",
+ "get_namespace",
+ "verify_namespace",
+ "get_project",
+ "list_projects",
+ "list_project_members",
+ "get_pipeline",
+ "list_pipelines",
+ "list_pipeline_jobs",
+ "list_pipeline_trigger_jobs",
+ "get_pipeline_job",
+ "get_pipeline_job_output",
+ "list_labels",
+ "get_label",
+ "list_group_projects",
+ "get_repository_tree",
+ "list_milestones",
+ "get_milestone",
+ "get_milestone_issue",
+ "get_milestone_merge_requests",
+ "get_milestone_burndown_events",
+ "list_wiki_pages",
+ "get_wiki_page",
+ "get_users",
+ "list_commits",
+ "get_commit",
+ "get_commit_diff",
+ "list_group_iterations",
+ "get_group_iteration",
+ "download_attachment",
+];
+
+// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
+const wikiToolNames = [
+ "list_wiki_pages",
+ "get_wiki_page",
+ "create_wiki_page",
+ "update_wiki_page",
+ "delete_wiki_page",
+ "upload_wiki_attachment",
+];
+
+// Define which tools are related to milestones and can be toggled by USE_MILESTONE
+const milestoneToolNames = [
+ "list_milestones",
+ "get_milestone",
+ "create_milestone",
+ "edit_milestone",
+ "delete_milestone",
+ "get_milestone_issue",
+ "get_milestone_merge_requests",
+ "promote_milestone",
+ "get_milestone_burndown_events",
+];
+
+// Define which tools are related to pipelines and can be toggled by USE_PIPELINE
+const pipelineToolNames = [
+ "list_pipelines",
+ "get_pipeline",
+ "list_pipeline_jobs",
+ "list_pipeline_trigger_jobs",
+ "get_pipeline_job",
+ "get_pipeline_job_output",
+ "create_pipeline",
+ "retry_pipeline",
+ "cancel_pipeline",
+];
+
+/**
+ * Smart URL handling for GitLab API
+ *
+ * @param {string | undefined} url - Input GitLab API URL
+ * @returns {string} Normalized GitLab API URL with /api/v4 path
+ */
+function normalizeGitLabApiUrl(url?: string): string {
+ if (!url) {
+ return "https://gitlab.com/api/v4";
+ }
+
+ // Remove trailing slash if present
+ let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
+ // Check if URL already has /api/v4
+ if (!normalizedUrl.endsWith("/api/v4") && !normalizedUrl.endsWith("/api/v4/")) {
+ // Append /api/v4 if not already present
+ normalizedUrl = `${normalizedUrl}/api/v4`;
+ }
+
+ return normalizedUrl;
+}
+
+// Use the normalizeGitLabApiUrl function to handle various URL formats
+const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
+const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
+const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(',').map(id => id.trim()).filter(Boolean) || [];
+
+if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
+ logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
+ process.exit(1);
+}
+
+/**
+ * Utility function for handling GitLab API errors
+ * API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
+ *
+ * @param {import("node-fetch").Response} response - The response from GitLab API
+ * @throws {Error} Throws an error with response details if the request failed
+ */
+async function handleGitLabError(response: import("node-fetch").Response): Promise {
if (!response.ok) {
const errorBody = await response.text();
- throw new Error(
- `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
- );
+ // Check specifically for Rate Limit error
+ if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
+ logger.error("GitLab API Rate Limit Exceeded:", errorBody);
+ logger.error("User API Key Rate limit exceeded. Please try again later.");
+ throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
+ } else {
+ // Handle other API errors
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
+ }
}
+}
- const data = await response.json();
- return GitLabCreateUpdateFileResponseSchema.parse(data);
+/**
+ * @param {string} projectId - The project ID parameter passed to the function
+ * @returns {string} The project ID to use for the API call
+ * @throws {Error} If GITLAB_ALLOWED_PROJECT_IDS is set and the requested project is not in the whitelist
+ */
+function getEffectiveProjectId(projectId: string): string {
+ if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
+ // If there's only one allowed project, use it as default
+ if (GITLAB_ALLOWED_PROJECT_IDS.length === 1 && !projectId) {
+ return GITLAB_ALLOWED_PROJECT_IDS[0];
+ }
+
+ // If a project ID is provided, check if it's in the whitelist
+ if (projectId && !GITLAB_ALLOWED_PROJECT_IDS.includes(projectId)) {
+ throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${GITLAB_ALLOWED_PROJECT_IDS.join(', ')}`);
+ }
+
+ // If no project ID provided but we have multiple allowed projects, require an explicit choice
+ if (!projectId && GITLAB_ALLOWED_PROJECT_IDS.length > 1) {
+ throw new Error(`Multiple projects allowed (${GITLAB_ALLOWED_PROJECT_IDS.join(', ')}). Please specify a project ID.`);
+ }
+
+ return projectId || GITLAB_ALLOWED_PROJECT_IDS[0];
+ }
+ return GITLAB_PROJECT_ID || projectId;
}
-async function createTree(
- projectId: string,
- files: FileOperation[],
- ref?: string
-): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/repository/tree`
- );
+/**
+ * Create a fork of a GitLab project
+ * 프로젝트 포크 생성 (Create a project fork)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} [namespace] - The namespace to fork the project to
+ * @returns {Promise} The created fork
+ */
+async function forkProject(projectId: string, namespace?: string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/fork`);
- if (ref) {
- url.searchParams.append("ref", ref);
+ if (namespace) {
+ url.searchParams.append("namespace", namespace);
}
const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
- body: JSON.stringify({
- files: files.map((file) => ({
- file_path: file.path,
- content: file.content,
- encoding: "text",
- })),
- }),
});
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
-
- if (!response.ok) {
- const errorBody = await response.text();
- throw new Error(
- `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
- );
+ // 이미 존재하는 프로젝트인 경우 처리
+ if (response.status === 409) {
+ throw new Error("Project already exists in the target namespace");
}
+ await handleGitLabError(response);
const data = await response.json();
- return GitLabTreeSchema.parse(data);
+ return GitLabForkSchema.parse(data);
}
-async function createCommit(
+/**
+ * Create a new branch in a GitLab project
+ * 새로운 브랜치 생성 (Create a new branch)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {z.infer} options - Branch creation options
+ * @returns {Promise} The created branch reference
+ */
+async function createBranch(
projectId: string,
- message: string,
- branch: string,
- actions: FileOperation[]
-): Promise {
+ options: z.infer
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const effectiveProjectId = getEffectiveProjectId(projectId);
const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/repository/commits`
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/branches`
);
const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
body: JSON.stringify({
- branch,
- commit_message: message,
- actions: actions.map((action) => ({
- action: "create",
- file_path: action.path,
- content: action.content,
- encoding: "text",
- })),
+ branch: options.name,
+ ref: options.ref,
}),
});
- if (response.status === 400) {
- const errorBody = await response.text();
- throw new Error(`Invalid request: ${errorBody}`);
- }
+ await handleGitLabError(response);
+ return GitLabReferenceSchema.parse(await response.json());
+}
- if (!response.ok) {
- const errorBody = await response.text();
- throw new Error(
- `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
- );
+/**
+ * Get the default branch for a GitLab project
+ * 프로젝트의 기본 브랜치 조회 (Get the default branch of a project)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @returns {Promise} The name of the default branch
+ */
+async function getDefaultBranchRef(projectId: string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}`);
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const project = GitLabRepositorySchema.parse(await response.json());
+ return project.default_branch ?? "main";
+}
+
+/**
+ * Get the contents of a file from a GitLab project
+ * 파일 내용 조회 (Get file contents)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} filePath - The path of the file to get
+ * @param {string} [ref] - The name of the branch, tag or commit
+ * @returns {Promise} The file content
+ */
+async function getFileContents(
+ projectId: string,
+ filePath: string,
+ ref?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+ const encodedPath = encodeURIComponent(filePath);
+
+ // ref가 없는 경우 default branch를 가져옴
+ if (!ref) {
+ ref = await getDefaultBranchRef(projectId);
+ }
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`
+ );
+
+ url.searchParams.append("ref", ref);
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ // 파일을 찾을 수 없는 경우 처리
+ if (response.status === 404) {
+ throw new Error(`File not found: ${filePath}`);
}
+ await handleGitLabError(response);
const data = await response.json();
- return GitLabCommitSchema.parse(data);
+ const parsedData = GitLabContentSchema.parse(data);
+
+ // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
+ if (!Array.isArray(parsedData) && parsedData.content) {
+ parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
+ parsedData.encoding = "utf8";
+ }
+
+ return parsedData;
}
-async function searchProjects(
- query: string,
- page: number = 1,
- perPage: number = 20
-): Promise {
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects`);
- url.searchParams.append("search", query);
- url.searchParams.append("page", page.toString());
- url.searchParams.append("per_page", perPage.toString());
- url.searchParams.append("order_by", "id");
- url.searchParams.append("sort", "desc");
+/**
+ * Create a new issue in a GitLab project
+ * 이슈 생성 (Create an issue)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {z.infer} options - Issue creation options
+ * @returns {Promise} The created issue
+ */
+async function createIssue(
+ projectId: string,
+ options: z.infer
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`
+ );
const response = await fetch(url.toString(), {
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify({
+ title: options.title,
+ description: options.description,
+ assignee_ids: options.assignee_ids,
+ milestone_id: options.milestone_id,
+ labels: options.labels?.join(","),
+ }),
});
- if (!response.ok) {
+ // 잘못된 요청 처리
+ if (response.status === 400) {
const errorBody = await response.text();
- throw new Error(
- `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
- );
+ throw new Error(`Invalid request: ${errorBody}`);
}
- const projects = (await response.json()) as GitLabRepository[];
- const totalCount = response.headers.get("x-total");
- const totalPages = response.headers.get("x-total-pages");
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabIssueSchema.parse(data);
+}
- // GitLab API doesn't return these headers for results > 10,000
- const count = totalCount ? parseInt(totalCount) : projects.length;
+/**
+ * List issues across all accessible projects or within a specific project
+ * 프로젝트의 이슈 목록 조회
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project (optional)
+ * @param {Object} options - Options for listing issues
+ * @returns {Promise} List of issues
+ */
+async function listIssues(
+ projectId?: string,
+ options: Omit, "project_id"> = {}
+): Promise {
+ let url: URL;
+ if (projectId) {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+ url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
+ } else {
+ url = new URL(`${GITLAB_API_URL}/issues`);
+ }
- return GitLabSearchResponseSchema.parse({
- count,
- total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
- current_page: page,
- items: projects,
+ // Add all query parameters
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ const keys = ["labels", "assignee_username"];
+ if (keys.includes(key)) {
+ if (Array.isArray(value)) {
+ // Handle array of labels
+ value.forEach(label => {
+ url.searchParams.append(`${key}[]`, label.toString());
+ });
+ } else if (value) {
+ url.searchParams.append(`${key}[]`, value.toString());
+ }
+ } else {
+ url.searchParams.append(key, String(value));
+ }
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabIssueSchema).parse(data);
+}
+
+/**
+ * List merge requests in a GitLab project with optional filtering
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {Object} options - Optional filtering parameters
+ * @returns {Promise} List of merge requests
+ */
+async function listMergeRequests(
+ projectId: string,
+ options: Omit, "project_id"> = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`
+ );
+
+ // Add all query parameters
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (key === "labels" && Array.isArray(value)) {
+ // Handle array of labels
+ url.searchParams.append(key, value.join(","));
+ } else {
+ url.searchParams.append(key, String(value));
+ }
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabMergeRequestSchema).parse(data);
+}
+
+/**
+ * Get a single issue from a GitLab project
+ * 단일 이슈 조회
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @returns {Promise} The issue
+ */
+async function getIssue(projectId: string, issueIid: number | string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
});
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabIssueSchema.parse(data);
}
-async function createRepository(
- options: z.infer
-): Promise {
- const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
+/**
+ * Update an issue in a GitLab project
+ * 이슈 업데이트
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @param {Object} options - Update options for the issue
+ * @returns {Promise} The updated issue
+ */
+async function updateIssue(
+ projectId: string,
+ issueIid: number | string,
+ options: Omit, "project_id" | "issue_iid">
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`
+ );
+
+ // Convert labels array to comma-separated string if present
+ const body: Record = { ...options };
+ if (body.labels && Array.isArray(body.labels)) {
+ body.labels = body.labels.join(",");
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(body),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabIssueSchema.parse(data);
+}
+
+/**
+ * Delete an issue from a GitLab project
+ * 이슈 삭제
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @returns {Promise}
+ */
+async function deleteIssue(projectId: string, issueIid: number | string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "DELETE",
+ });
+
+ await handleGitLabError(response);
+}
+
+/**
+ * List all issue links for a specific issue
+ * 이슈 관계 목록 조회
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @returns {Promise} List of issues with link details
+ */
+async function listIssueLinks(
+ projectId: string,
+ issueIid: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabIssueWithLinkDetailsSchema).parse(data);
+}
+
+/**
+ * Get a specific issue link
+ * 특정 이슈 관계 조회
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @param {number} issueLinkId - The ID of the issue link
+ * @returns {Promise} The issue link
+ */
+async function getIssueLink(
+ projectId: string,
+ issueIid: number | string,
+ issueLinkId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/issues/${issueIid}/links/${issueLinkId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabIssueLinkSchema.parse(data);
+}
+
+/**
+ * Create an issue link between two issues
+ * 이슈 관계 생성
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @param {string} targetProjectId - The ID or URL-encoded path of the target project
+ * @param {number} targetIssueIid - The internal ID of the target project issue
+ * @param {string} linkType - The type of the relation (relates_to, blocks, is_blocked_by)
+ * @returns {Promise} The created issue link
+ */
+async function createIssueLink(
+ projectId: string,
+ issueIid: number | string,
+ targetProjectId: string,
+ targetIssueIid: number | string,
+ linkType: "relates_to" | "blocks" | "is_blocked_by" = "relates_to"
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
- },
body: JSON.stringify({
- name: options.name,
+ target_project_id: targetProjectId,
+ target_issue_iid: targetIssueIid,
+ link_type: linkType,
+ }),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabIssueLinkSchema.parse(data);
+}
+
+/**
+ * Delete an issue link
+ * 이슈 관계 삭제
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @param {number} issueLinkId - The ID of the issue link
+ * @returns {Promise}
+ */
+async function deleteIssueLink(
+ projectId: string,
+ issueIid: number | string,
+ issueLinkId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/issues/${issueIid}/links/${issueLinkId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "DELETE",
+ });
+
+ await handleGitLabError(response);
+}
+
+/**
+ * Create a new merge request in a GitLab project
+ * 병합 요청 생성
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {z.infer} options - Merge request creation options
+ * @returns {Promise} The created merge request
+ */
+async function createMergeRequest(
+ projectId: string,
+ options: z.infer
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify({
+ title: options.title,
description: options.description,
- visibility: options.visibility,
- initialize_with_readme: options.initialize_with_readme,
- default_branch: "main",
- path: options.name.toLowerCase().replace(/\s+/g, "-"),
+ source_branch: options.source_branch,
+ target_branch: options.target_branch,
+ target_project_id: options.target_project_id,
+ assignee_ids: options.assignee_ids,
+ reviewer_ids: options.reviewer_ids,
+ labels: options.labels?.join(","),
+ allow_collaboration: options.allow_collaboration,
+ draft: options.draft,
+ remove_source_branch: options.remove_source_branch,
+ squash: options.squash,
}),
});
+ if (response.status === 400) {
+ const errorBody = await response.text();
+ throw new Error(`Invalid request: ${errorBody}`);
+ }
+
if (!response.ok) {
const errorBody = await response.text();
- throw new Error(
- `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
- );
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const data = await response.json();
- return GitLabRepositorySchema.parse(data);
+ return GitLabMergeRequestSchema.parse(data);
}
-// MR 조회 함수
-async function getMergeRequest(
+/**
+ * Shared helper function for listing discussions
+ * 토론 목록 조회를 위한 공유 헬퍼 함수
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {"issues" | "merge_requests"} resourceType - The type of resource (issues or merge_requests)
+ * @param {number} resourceIid - The IID of the issue or merge request
+ * @param {PaginationOptions} options - Pagination and sorting options
+ * @returns {Promise} Paginated list of discussions
+ */
+async function listDiscussions(
projectId: string,
- mergeRequestIid: number
-): Promise {
+ resourceType: "issues" | "merge_requests",
+ resourceIid: number | string,
+ options: PaginationOptions = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/merge_requests/${mergeRequestIid}`
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/${resourceType}/${resourceIid}/discussions`
);
+ // Add query parameters for pagination and sorting
+ if (options.page) {
+ url.searchParams.append("page", options.page.toString());
+ }
+ if (options.per_page) {
+ url.searchParams.append("per_page", options.per_page.toString());
+ }
+
const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const discussions = await response.json();
+
+ // Extract pagination headers
+ const pagination = {
+ x_next_page: response.headers.get("x-next-page")
+ ? parseInt(response.headers.get("x-next-page")!)
+ : null,
+ x_page: response.headers.get("x-page") ? parseInt(response.headers.get("x-page")!) : undefined,
+ x_per_page: response.headers.get("x-per-page")
+ ? parseInt(response.headers.get("x-per-page")!)
+ : undefined,
+ x_prev_page: response.headers.get("x-prev-page")
+ ? parseInt(response.headers.get("x-prev-page")!)
+ : null,
+ x_total: response.headers.get("x-total") ? parseInt(response.headers.get("x-total")!) : null,
+ x_total_pages: response.headers.get("x-total-pages")
+ ? parseInt(response.headers.get("x-total-pages")!)
+ : null,
+ };
+
+ return PaginatedDiscussionsResponseSchema.parse({
+ items: discussions,
+ pagination: pagination,
});
+}
+
+/**
+ * List merge request discussion items
+ * 병합 요청 토론 목록 조회
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The IID of a merge request
+ * @param {DiscussionPaginationOptions} options - Pagination and sorting options
+ * @returns {Promise} List of discussions
+ */
+async function listMergeRequestDiscussions(
+ projectId: string,
+ mergeRequestIid: number | string,
+ options: PaginationOptions = {}
+): Promise {
+ return listDiscussions(projectId, "merge_requests", mergeRequestIid, options);
+}
+
+/**
+ * List discussions for an issue
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The internal ID of the project issue
+ * @param {DiscussionPaginationOptions} options - Pagination and sorting options
+ * @returns {Promise} List of issue discussions
+ */
+async function listIssueDiscussions(
+ projectId: string,
+ issueIid: number | string,
+ options: PaginationOptions = {}
+): Promise {
+ return listDiscussions(projectId, "issues", issueIid, options);
+}
+
+/**
+ * Modify an existing merge request thread note
+ * 병합 요청 토론 노트 수정
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The IID of a merge request
+ * @param {string} discussionId - The ID of a thread
+ * @param {number} noteId - The ID of a thread note
+ * @param {string} body - The new content of the note
+ * @param {boolean} [resolved] - Resolve/unresolve state
+ * @returns {Promise} The updated note
+ */
+async function updateMergeRequestNote(
+ projectId: string,
+ mergeRequestIid: number | string,
+ discussionId: string,
+ noteId: number | string,
+ body?: string,
+ resolved?: boolean
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`
+ );
+
+ // Only one of body or resolved can be sent according to GitLab API
+ const payload: { body?: string; resolved?: boolean } = {};
+ if (body !== undefined) {
+ payload.body = body;
+ } else if (resolved !== undefined) {
+ payload.resolved = resolved;
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(payload),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabDiscussionNoteSchema.parse(data);
+}
+
+/**
+ * Update an issue discussion note
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The IID of an issue
+ * @param {string} discussionId - The ID of a thread
+ * @param {number} noteId - The ID of a thread note
+ * @param {string} body - The new content of the note
+ * @returns {Promise} The updated note
+ */
+async function updateIssueNote(
+ projectId: string,
+ issueIid: number | string,
+ discussionId: string,
+ noteId: number | string,
+ body: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}`
+ );
+
+ const payload = { body };
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(payload),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabDiscussionNoteSchema.parse(data);
+}
+
+/**
+ * Create a note in an issue discussion
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} issueIid - The IID of an issue
+ * @param {string} discussionId - The ID of a thread
+ * @param {string} body - The content of the new note
+ * @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
+ * @returns {Promise} The created note
+ */
+async function createIssueNote(
+ projectId: string,
+ issueIid: number | string,
+ discussionId: string,
+ body: string,
+ createdAt?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/issues/${issueIid}/discussions/${discussionId}/notes`
+ );
+
+ const payload: { body: string; created_at?: string } = { body };
+ if (createdAt) {
+ payload.created_at = createdAt;
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabDiscussionNoteSchema.parse(data);
+}
+
+/**
+ * Add a new note to an existing merge request thread
+ * 기존 병합 요청 스레드에 새 노트 추가
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The IID of a merge request
+ * @param {string} discussionId - The ID of a thread
+ * @param {string} body - The content of the new note
+ * @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
+ * @returns {Promise} The created note
+ */
+async function createMergeRequestNote(
+ projectId: string,
+ mergeRequestIid: number | string,
+ discussionId: string,
+ body: string,
+ createdAt?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`
+ );
+
+ const payload: { body: string; created_at?: string } = { body };
+ if (createdAt) {
+ payload.created_at = createdAt;
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabDiscussionNoteSchema.parse(data);
+}
+
+/**
+ * Create or update a file in a GitLab project
+ * 파일 생성 또는 업데이트
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} filePath - The path of the file to create or update
+ * @param {string} content - The content of the file
+ * @param {string} commitMessage - The commit message
+ * @param {string} branch - The branch name
+ * @param {string} [previousPath] - The previous path of the file in case of rename
+ * @returns {Promise} The file update response
+ */
+async function createOrUpdateFile(
+ projectId: string,
+ filePath: string,
+ content: string,
+ commitMessage: string,
+ branch: string,
+ previousPath?: string,
+ last_commit_id?: string,
+ commit_id?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const encodedPath = encodeURIComponent(filePath);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`
+ );
+
+ const body: Record = {
+ branch,
+ content,
+ commit_message: commitMessage,
+ encoding: "text",
+ ...(previousPath ? { previous_path: previousPath } : {}),
+ };
+
+ // Check if file exists
+ let method = "POST";
+ try {
+ // Get file contents to check existence and retrieve commit IDs
+ const fileData = await getFileContents(projectId, filePath, branch);
+ method = "PUT";
+
+ // If fileData is not an array, it's a file content object with commit IDs
+ if (!Array.isArray(fileData)) {
+ // Use commit IDs from the file data if not provided in parameters
+ if (!commit_id && fileData.commit_id) {
+ body.commit_id = fileData.commit_id;
+ } else if (commit_id) {
+ body.commit_id = commit_id;
+ }
+
+ if (!last_commit_id && fileData.last_commit_id) {
+ body.last_commit_id = fileData.last_commit_id;
+ } else if (last_commit_id) {
+ body.last_commit_id = last_commit_id;
+ }
+ }
+ } catch (error) {
+ if (!(error instanceof Error && error.message.includes("File not found"))) {
+ throw error;
+ }
+ // File doesn't exist, use POST - no need for commit IDs for new files
+ // But still use any provided as parameters if they exist
+ if (commit_id) {
+ body.commit_id = commit_id;
+ }
+ if (last_commit_id) {
+ body.last_commit_id = last_commit_id;
+ }
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method,
+ body: JSON.stringify(body),
+ });
+
+ 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();
+ return GitLabCreateUpdateFileResponseSchema.parse(data);
+}
+
+/**
+ * Create a tree structure in a GitLab project repository
+ * 저장소에 트리 구조 생성
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {FileOperation[]} files - Array of file operations
+ * @param {string} [ref] - The name of the branch, tag or commit
+ * @returns {Promise} The created tree
+ */
+async function createTree(
+ projectId: string,
+ files: FileOperation[],
+ ref?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/tree`
+ );
+
+ if (ref) {
+ url.searchParams.append("ref", ref);
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify({
+ files: files.map(file => ({
+ file_path: file.path,
+ content: file.content,
+ encoding: "text",
+ })),
+ }),
+ });
+
+ if (response.status === 400) {
+ const errorBody = await response.text();
+ throw new Error(`Invalid request: ${errorBody}`);
+ }
+
+ 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();
+ return GitLabTreeSchema.parse(data);
+}
+
+/**
+ * Create a commit in a GitLab project repository
+ * 저장소에 커밋 생성
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} message - The commit message
+ * @param {string} branch - The branch name
+ * @param {FileOperation[]} actions - Array of file operations for the commit
+ * @returns {Promise} The created commit
+ */
+async function createCommit(
+ projectId: string,
+ message: string,
+ branch: string,
+ actions: FileOperation[]
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify({
+ branch,
+ commit_message: message,
+ actions: actions.map(action => ({
+ action: "create",
+ file_path: action.path,
+ content: action.content,
+ encoding: "text",
+ })),
+ }),
+ });
+
+ if (response.status === 400) {
+ const errorBody = await response.text();
+ throw new Error(`Invalid request: ${errorBody}`);
+ }
+
+ 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();
+ return GitLabCommitSchema.parse(data);
+}
+
+/**
+ * Search for GitLab projects
+ * 프로젝트 검색
+ *
+ * @param {string} query - The search query
+ * @param {number} [page=1] - The page number
+ * @param {number} [perPage=20] - Number of items per page
+ * @returns {Promise} The search results
+ */
+async function searchProjects(
+ query: string,
+ page: number = 1,
+ perPage: number = 20
+): Promise {
+ const url = new URL(`${GITLAB_API_URL}/projects`);
+ url.searchParams.append("search", query);
+ url.searchParams.append("page", page.toString());
+ url.searchParams.append("per_page", perPage.toString());
+ url.searchParams.append("order_by", "id");
+ url.searchParams.append("sort", "desc");
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
+ }
+
+ const projects = (await response.json()) as GitLabRepository[];
+ const totalCount = response.headers.get("x-total");
+ const totalPages = response.headers.get("x-total-pages");
+
+ // GitLab API doesn't return these headers for results > 10,000
+ const count = totalCount ? parseInt(totalCount) : projects.length;
+
+ return GitLabSearchResponseSchema.parse({
+ count,
+ total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
+ current_page: page,
+ items: projects,
+ });
+}
+
+/**
+ * Create a new GitLab repository
+ * 새 저장소 생성
+ *
+ * @param {z.infer} options - Repository creation options
+ * @returns {Promise} The created repository
+ */
+async function createRepository(
+ options: z.infer
+): Promise {
+ const response = await fetch(`${GITLAB_API_URL}/projects`, {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify({
+ name: options.name,
+ description: options.description,
+ visibility: options.visibility,
+ initialize_with_readme: options.initialize_with_readme,
+ default_branch: "main",
+ path: options.name.toLowerCase().replace(/\s+/g, "-"),
+ }),
+ });
+
+ 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();
+ return GitLabRepositorySchema.parse(data);
+}
+
+/**
+ * Get merge request details
+ * MR 조회 함수 (Function to retrieve merge request)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The internal ID of the merge request (Optional)
+ * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Optional)
+ * @returns {Promise} The merge request details
+ */
+async function getMergeRequest(
+ projectId: string,
+ mergeRequestIid?: number | string,
+ branchName?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ let url: URL;
+
+ if (mergeRequestIid) {
+ url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}`
+ );
+ } else if (branchName) {
+ url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests?source_branch=${encodeURIComponent(branchName)}`
+ );
+ } else {
+ throw new Error("Either mergeRequestIid or branchName must be provided");
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+
+ const data = await response.json();
+
+ // If response is an array (Comes from branchName search), return the first item if exist
+ if (Array.isArray(data) && data.length > 0) {
+ return GitLabMergeRequestSchema.parse(data[0]);
+ }
+
+ return GitLabMergeRequestSchema.parse(data);
+}
+
+/**
+ * Get merge request changes/diffs
+ * MR 변경사항 조회 함수 (Function to retrieve merge request changes)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The internal ID of the merge request (Either mergeRequestIid or branchName must be provided)
+ * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Either mergeRequestIid or branchName must be provided)
+ * @param {string} [view] - The view type for the diff (inline or parallel)
+ * @returns {Promise} The merge request diffs
+ */
+async function getMergeRequestDiffs(
+ projectId: string,
+ mergeRequestIid?: number | string,
+ branchName?: string,
+ view?: "inline" | "parallel"
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ if (!mergeRequestIid && !branchName) {
+ throw new Error("Either mergeRequestIid or branchName must be provided");
+ }
+
+ if (branchName && !mergeRequestIid) {
+ const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
+ mergeRequestIid = mergeRequest.iid;
+ }
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/changes`
+ );
+
+ if (view) {
+ url.searchParams.append("view", view);
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = (await response.json()) as { changes: unknown };
+ return z.array(GitLabDiffSchema).parse(data.changes);
+}
+
+/**
+ * Get merge request changes with detailed information including commits, diff_refs, and more
+ * 마지막으로 추가된 상세한 MR 변경사항 조회 함수 (Detailed merge request changes retrieval function)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The internal ID of the merge request (Either mergeRequestIid or branchName must be provided)
+ * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Either mergeRequestIid or branchName must be provided)
+ * @param {boolean} [unidiff] - Return diff in unidiff format
+ * @returns {Promise} The complete merge request changes response
+ */
+async function listMergeRequestDiffs(
+ projectId: string,
+ mergeRequestIid?: number | string,
+ branchName?: string,
+ page?: number,
+ perPage?: number,
+ unidiff?: boolean
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ if (!mergeRequestIid && !branchName) {
+ throw new Error("Either mergeRequestIid or branchName must be provided");
+ }
+
+ if (branchName && !mergeRequestIid) {
+ const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
+ mergeRequestIid = mergeRequest.iid;
+ }
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/diffs`
+ );
+
+ if (page) {
+ url.searchParams.append("page", page.toString());
+ }
+
+ if (perPage) {
+ url.searchParams.append("per_page", perPage.toString());
+ }
+
+ if (unidiff) {
+ url.searchParams.append("unidiff", "true");
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ return await response.json(); // Return full response including commits, diff_refs, changes, etc.
+}
+
+/**
+ * Get branch comparison diffs
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} from - The branch name or commit SHA to compare from
+ * @param {string} to - The branch name or commit SHA to compare to
+ * @param {boolean} [straight] - Comparison method: false for '...' (default), true for '--'
+ * @returns {Promise} Branch comparison results
+ */
+async function getBranchDiffs(
+ projectId: string,
+ from: string,
+ to: string,
+ straight?: boolean
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/compare`
+ );
+
+ url.searchParams.append("from", from);
+ url.searchParams.append("to", to);
+
+ if (straight !== undefined) {
+ url.searchParams.append("straight", straight.toString());
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ 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();
+ return GitLabCompareResultSchema.parse(data);
+}
+
+/**
+ * Update a merge request
+ * MR 업데이트 함수 (Function to update merge request)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The internal ID of the merge request (Optional)
+ * @param {string} branchName - The name of the branch to search for merge request by branch name (Optional)
+ * @param {Object} options - The update options
+ * @returns {Promise} The updated merge request
+ */
+async function updateMergeRequest(
+ projectId: string,
+ options: Omit<
+ z.infer,
+ "project_id" | "merge_request_iid" | "source_branch"
+ >,
+ mergeRequestIid?: number | string,
+ branchName?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ if (!mergeRequestIid && !branchName) {
+ throw new Error("Either mergeRequestIid or branchName must be provided");
+ }
+
+ if (branchName && !mergeRequestIid) {
+ const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
+ mergeRequestIid = mergeRequest.iid;
+ }
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(options),
+ });
+
+ await handleGitLabError(response);
+ return GitLabMergeRequestSchema.parse(await response.json());
+}
+
+/**
+ * Merge a merge request
+ * マージリクエストをマージする
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The internal ID of the merge request
+ * @param {Object} options - Options for merging the merge request
+ * @returns {Promise} The merged merge request
+ */
+async function mergeMergeRequest(
+ projectId: string,
+ options: Omit, "project_id" | "merge_request_iid">,
+ mergeRequestIid?: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/merge`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(options),
+ });
+
+ await handleGitLabError(response);
+ return GitLabMergeRequestSchema.parse(await response.json());
+}
+
+/**
+ * Create a new note (comment) on an issue or merge request
+ * 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
+ * (New function: createNote - Function to add a note (comment) to an issue or merge request)
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {"issue" | "merge_request"} noteableType - The type of the item to add a note to (issue or merge_request)
+ * @param {number} noteableIid - The internal ID of the issue or merge request
+ * @param {string} body - The content of the note
+ * @returns {Promise} The created note
+ */
+async function createNote(
+ projectId: string,
+ noteableType: "issue" | "merge_request", // 'issue' 또는 'merge_request' 타입 명시
+ noteableIid: number | string,
+ body: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify({ body }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ return await response.json();
+}
+
+/**
+ * List draft notes for a merge request
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number|string} mergeRequestIid - The internal ID of the merge request
+ * @returns {Promise} Array of draft notes
+ */
+async function getDraftNote(
+ project_id: string,
+ merge_request_iid: string,
+ draft_note_id: string
+): Promise {
+ const response = await fetch(
+ `/projects/${encodeURIComponent(project_id)}/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}`
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ const data = await response.json();
+ return GitLabDraftNoteSchema.parse(data);
+}
+
+async function listDraftNotes(
+ projectId: string,
+ mergeRequestIid: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/draft_notes`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "GET",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ const data = await response.json();
+ return z.array(GitLabDraftNoteSchema).parse(data);
+}
+
+/**
+ * Create a draft note for a merge request
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number|string} mergeRequestIid - The internal ID of the merge request
+ * @param {string} body - The content of the draft note
+ * @param {MergeRequestThreadPosition} [position] - Position information for diff notes
+ * @param {boolean} [resolveDiscussion] - Whether to resolve the discussion when publishing
+ * @returns {Promise} The created draft note
+ */
+async function createDraftNote(
+ projectId: string,
+ mergeRequestIid: number | string,
+ body: string,
+ position?: MergeRequestThreadPositionCreate,
+ resolveDiscussion?: boolean
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/draft_notes`
+ );
+
+ const requestBody: any = { note: body };
+ if (position) {
+ requestBody.position = position;
+ }
+ if (resolveDiscussion !== undefined) {
+ requestBody.resolve_discussion = resolveDiscussion;
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(requestBody),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ const data = await response.json();
+ return GitLabDraftNoteSchema.parse(data);
+}
+
+/**
+ * Update an existing draft note
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number|string} mergeRequestIid - The internal ID of the merge request
+ * @param {number|string} draftNoteId - The ID of the draft note
+ * @param {string} [body] - The updated content of the draft note
+ * @param {MergeRequestThreadPosition} [position] - Updated position information
+ * @param {boolean} [resolveDiscussion] - Whether to resolve the discussion when publishing
+ * @returns {Promise} The updated draft note
+ */
+async function updateDraftNote(
+ projectId: string,
+ mergeRequestIid: number | string,
+ draftNoteId: number | string,
+ body?: string,
+ position?: MergeRequestThreadPositionCreate,
+ resolveDiscussion?: boolean
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`
+ );
+
+ const requestBody: any = {};
+ if (body !== undefined) {
+ requestBody.note = body;
+ }
+ if (position) {
+ requestBody.position = position;
+ }
+ if (resolveDiscussion !== undefined) {
+ requestBody.resolve_discussion = resolveDiscussion;
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(requestBody),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ const data = await response.json();
+ return GitLabDraftNoteSchema.parse(data);
+}
+
+/**
+ * Delete a draft note
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number|string} mergeRequestIid - The internal ID of the merge request
+ * @param {number|string} draftNoteId - The ID of the draft note
+ * @returns {Promise}
+ */
+async function deleteDraftNote(
+ projectId: string,
+ mergeRequestIid: number | string,
+ draftNoteId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "DELETE",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+}
+
+/**
+ * Publish a single draft note
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number|string} mergeRequestIid - The internal ID of the merge request
+ * @param {number|string} draftNoteId - The ID of the draft note
+ * @returns {Promise} The published note
+ */
+async function publishDraftNote(
+ projectId: string,
+ mergeRequestIid: number | string,
+ draftNoteId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}/publish`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ // Handle empty response (204 No Content) or successful response
+ const responseText = await response.text();
+ if (!responseText || responseText.trim() === '') {
+ // Return a success indicator for empty responses
+ return {
+ id: draftNoteId.toString(),
+ body: "Draft note published successfully",
+ author: { id: "unknown", username: "unknown" },
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ system: false,
+ noteable_id: mergeRequestIid.toString(),
+ noteable_type: "MergeRequest"
+ } as any;
+ }
+
+ try {
+ const data = JSON.parse(responseText);
+ return GitLabDiscussionNoteSchema.parse(data);
+ } catch (parseError) {
+ // If JSON parsing fails but the operation was successful (2xx status),
+ // return a success indicator
+ console.warn(`JSON parse error for successful publish operation: ${parseError}`);
+ return {
+ id: draftNoteId.toString(),
+ body: "Draft note published successfully (response parse error)",
+ author: { id: "unknown", username: "unknown" },
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ system: false,
+ noteable_id: mergeRequestIid.toString(),
+ noteable_type: "MergeRequest"
+ } as any;
+ }
+}
+
+/**
+ * Publish all draft notes for a merge request
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number|string} mergeRequestIid - The internal ID of the merge request
+ * @returns {Promise} Array of published notes
+ */
+async function bulkPublishDraftNotes(
+ projectId: string,
+ mergeRequestIid: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/draft_notes/bulk_publish`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST", // Changed from PUT to POST
+ body: JSON.stringify({}), // Send empty body for POST request
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
+ }
+
+ // Handle empty response (204 No Content) or successful response
+ const responseText = await response.text();
+ if (!responseText || responseText.trim() === '') {
+ // Return empty array for successful bulk publish with no content
+ return [];
+ }
+
+ try {
+ const data = JSON.parse(responseText);
+ return z.array(GitLabDiscussionNoteSchema).parse(data);
+ } catch (parseError) {
+ // If JSON parsing fails but the operation was successful (2xx status),
+ // return empty array indicating successful bulk publish
+ console.warn(`JSON parse error for successful bulk publish operation: ${parseError}`);
+ return [];
+ }
+}
+
+/**
+ * Create a new thread on a merge request
+ * 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
+ * (New function: createMergeRequestThread - Function to create a new thread (discussion) on a merge request)
+ *
+ * This function provides more capabilities than createNote, including the ability to:
+ * - Create diff notes (comments on specific lines of code)
+ * - Specify exact positions for comments
+ * - Set creation timestamps
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} mergeRequestIid - The internal ID of the merge request
+ * @param {string} body - The content of the thread
+ * @param {MergeRequestThreadPosition} [position] - Position information for diff notes
+ * @param {string} [createdAt] - ISO 8601 formatted creation date
+ * @returns {Promise} The created discussion thread
+ */
+async function createMergeRequestThread(
+ projectId: string,
+ mergeRequestIid: number | string,
+ body: string,
+ position?: MergeRequestThreadPosition,
+ createdAt?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/merge_requests/${mergeRequestIid}/discussions`
+ );
+
+ const payload: Record = { body };
+
+ // Add optional parameters if provided
+ if (position) {
+ payload.position = position;
+ }
+
+ if (createdAt) {
+ payload.created_at = createdAt;
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabDiscussionSchema.parse(data);
+}
+
+/**
+ * List all namespaces
+ * 사용 가능한 모든 네임스페이스 목록 조회
+ *
+ * @param {Object} options - Options for listing namespaces
+ * @param {string} [options.search] - Search query to filter namespaces
+ * @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
+ * @param {boolean} [options.top_level_only] - Only return top-level namespaces
+ * @returns {Promise} List of namespaces
+ */
+async function listNamespaces(options: {
+ search?: string;
+ owned_only?: boolean;
+ top_level_only?: boolean;
+}): Promise {
+ const url = new URL(`${GITLAB_API_URL}/namespaces`);
+
+ if (options.search) {
+ url.searchParams.append("search", options.search);
+ }
+
+ if (options.owned_only) {
+ url.searchParams.append("owned_only", "true");
+ }
+
+ if (options.top_level_only) {
+ url.searchParams.append("top_level_only", "true");
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabNamespaceSchema).parse(data);
+}
+
+/**
+ * Get details on a namespace
+ * 네임스페이스 상세 정보 조회
+ *
+ * @param {string} id - The ID or URL-encoded path of the namespace
+ * @returns {Promise} The namespace details
+ */
+async function getNamespace(id: string): Promise {
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(id)}`);
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabNamespaceSchema.parse(data);
+}
+
+/**
+ * Verify if a namespace exists
+ * 네임스페이스 존재 여부 확인
+ *
+ * @param {string} namespacePath - The path of the namespace to check
+ * @param {number} [parentId] - The ID of the parent namespace
+ * @returns {Promise} The verification result
+ */
+async function verifyNamespaceExistence(
+ namespacePath: string,
+ parentId?: number
+): Promise {
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
+
+ if (parentId) {
+ url.searchParams.append("parent_id", parentId.toString());
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabNamespaceExistsResponseSchema.parse(data);
+}
+
+/**
+ * Get a single project
+ * 단일 프로젝트 조회
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {Object} options - Options for getting project details
+ * @param {boolean} [options.license] - Include project license data
+ * @param {boolean} [options.statistics] - Include project statistics
+ * @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
+ * @returns {Promise} Project details
+ */
+async function getProject(
+ projectId: string,
+ options: {
+ license?: boolean;
+ statistics?: boolean;
+ with_custom_attributes?: boolean;
+ } = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`
+ );
+
+ if (options.license) {
+ url.searchParams.append("license", "true");
+ }
+
+ if (options.statistics) {
+ url.searchParams.append("statistics", "true");
+ }
+
+ if (options.with_custom_attributes) {
+ url.searchParams.append("with_custom_attributes", "true");
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabRepositorySchema.parse(data);
+}
+
+/**
+ * List projects
+ * 프로젝트 목록 조회
+ *
+ * @param {Object} options - Options for listing projects
+ * @returns {Promise} List of projects
+ */
+async function listProjects(
+ options: z.infer = {}
+): Promise {
+ // Construct the query parameters
+ const params = new URLSearchParams();
+ for (const [key, value] of Object.entries(options)) {
+ if (value !== undefined && value !== null) {
+ if (typeof value === "boolean") {
+ params.append(key, value ? "true" : "false");
+ } else {
+ params.append(key, String(value));
+ }
+ }
+ }
+
+ // Make the API request
+ const response = await fetch(`${GITLAB_API_URL}/projects?${params.toString()}`, {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ // Handle errors
+ await handleGitLabError(response);
+
+ // Parse and return the data
+ const data = await response.json();
+ return z.array(GitLabProjectSchema).parse(data);
+}
+
+/**
+ * List labels for a project
+ *
+ * @param projectId The ID or URL-encoded path of the project
+ * @param options Optional parameters for listing labels
+ * @returns Array of GitLab labels
+ */
+async function listLabels(
+ projectId: string,
+ options: Omit, "project_id"> = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ // Construct the URL with project path
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`
+ );
+
+ // Add query parameters
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (typeof value === "boolean") {
+ url.searchParams.append(key, value ? "true" : "false");
+ } else {
+ url.searchParams.append(key, String(value));
+ }
+ }
+ });
+
+ // Make the API request
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ // Handle errors
+ await handleGitLabError(response);
+
+ // Parse and return the data
+ const data = await response.json();
+ return data as GitLabLabel[];
+}
+
+/**
+ * Get a single label from a project
+ *
+ * @param projectId The ID or URL-encoded path of the project
+ * @param labelId The ID or name of the label
+ * @param includeAncestorGroups Whether to include ancestor groups
+ * @returns GitLab label
+ */
+async function getLabel(
+ projectId: string,
+ labelId: number | string,
+ includeAncestorGroups?: boolean
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/labels/${encodeURIComponent(String(labelId))}`
+ );
+
+ // Add query parameters
+ if (includeAncestorGroups !== undefined) {
+ url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
+ }
+
+ // Make the API request
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ // Handle errors
+ await handleGitLabError(response);
+
+ // Parse and return the data
+ const data = await response.json();
+ return data as GitLabLabel;
+}
+
+/**
+ * Create a new label in a project
+ *
+ * @param projectId The ID or URL-encoded path of the project
+ * @param options Options for creating the label
+ * @returns Created GitLab label
+ */
+async function createLabel(
+ projectId: string,
+ options: Omit, "project_id">
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ // Make the API request
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`,
+ {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(options),
+ }
+ );
+
+ // Handle errors
+ await handleGitLabError(response);
+
+ // Parse and return the data
+ const data = await response.json();
+ return data as GitLabLabel;
+}
+
+/**
+ * Update an existing label in a project
+ *
+ * @param projectId The ID or URL-encoded path of the project
+ * @param labelId The ID or name of the label to update
+ * @param options Options for updating the label
+ * @returns Updated GitLab label
+ */
+async function updateLabel(
+ projectId: string,
+ labelId: number | string,
+ options: Omit, "project_id" | "label_id">
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ // Make the API request
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/labels/${encodeURIComponent(String(labelId))}`,
+ {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(options),
+ }
+ );
+
+ // Handle errors
+ await handleGitLabError(response);
+
+ // Parse and return the data
+ const data = await response.json();
+ return data as GitLabLabel;
+}
+
+/**
+ * Delete a label from a project
+ *
+ * @param projectId The ID or URL-encoded path of the project
+ * @param labelId The ID or name of the label to delete
+ */
+async function deleteLabel(projectId: string, labelId: number | string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ // Make the API request
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/labels/${encodeURIComponent(String(labelId))}`,
+ {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "DELETE",
+ }
+ );
+
+ // Handle errors
+ await handleGitLabError(response);
+}
+
+/**
+ * List all projects in a GitLab group
+ *
+ * @param {z.infer} options - Options for listing group projects
+ * @returns {Promise} Array of projects in the group
+ */
+async function listGroupProjects(
+ options: z.infer
+): Promise {
+ const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`);
+
+ // Add optional parameters to URL
+ if (options.include_subgroups) url.searchParams.append("include_subgroups", "true");
+ if (options.search) url.searchParams.append("search", options.search);
+ if (options.order_by) url.searchParams.append("order_by", options.order_by);
+ if (options.sort) url.searchParams.append("sort", options.sort);
+ if (options.page) url.searchParams.append("page", options.page.toString());
+ if (options.per_page) url.searchParams.append("per_page", options.per_page.toString());
+ if (options.archived !== undefined)
+ url.searchParams.append("archived", options.archived.toString());
+ if (options.visibility) url.searchParams.append("visibility", options.visibility);
+ if (options.with_issues_enabled !== undefined)
+ url.searchParams.append("with_issues_enabled", options.with_issues_enabled.toString());
+ if (options.with_merge_requests_enabled !== undefined)
+ url.searchParams.append(
+ "with_merge_requests_enabled",
+ options.with_merge_requests_enabled.toString()
+ );
+ if (options.min_access_level !== undefined)
+ url.searchParams.append("min_access_level", options.min_access_level.toString());
+ if (options.with_programming_language)
+ url.searchParams.append("with_programming_language", options.with_programming_language);
+ if (options.starred !== undefined) url.searchParams.append("starred", options.starred.toString());
+ if (options.statistics !== undefined)
+ url.searchParams.append("statistics", options.statistics.toString());
+ if (options.with_custom_attributes !== undefined)
+ url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
+ if (options.with_security_reports !== undefined)
+ url.searchParams.append("with_security_reports", options.with_security_reports.toString());
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const projects = await response.json();
+ return GitLabProjectSchema.array().parse(projects);
+}
+
+// Wiki API helper functions
+/**
+ * List wiki pages in a project
+ */
+async function listWikiPages(
+ projectId: string,
+ options: Omit = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`
+ );
+ if (options.page) url.searchParams.append("page", options.page.toString());
+ if (options.per_page) url.searchParams.append("per_page", options.per_page.toString());
+ if (options.with_content)
+ url.searchParams.append("with_content", options.with_content.toString());
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabWikiPageSchema.array().parse(data);
+}
+
+/**
+ * Get a specific wiki page
+ */
+async function getWikiPage(projectId: string, slug: string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`,
+ { ...DEFAULT_FETCH_CONFIG }
+ );
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabWikiPageSchema.parse(data);
+}
+
+/**
+ * Create a new wiki page
+ */
+async function createWikiPage(
+ projectId: string,
+ title: string,
+ content: string,
+ format?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const body: Record = { title, content };
+ if (format) body.format = format;
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`,
+ {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(body),
+ }
+ );
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabWikiPageSchema.parse(data);
+}
+
+/**
+ * Update an existing wiki page
+ */
+async function updateWikiPage(
+ projectId: string,
+ slug: string,
+ title?: string,
+ content?: string,
+ format?: string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const body: Record = {};
+ if (title) body.title = title;
+ if (content) body.content = content;
+ if (format) body.format = format;
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`,
+ {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(body),
+ }
+ );
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabWikiPageSchema.parse(data);
+}
+
+/**
+ * Delete a wiki page
+ */
+async function deleteWikiPage(projectId: string, slug: string): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`,
+ {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "DELETE",
+ }
+ );
+ await handleGitLabError(response);
+}
+
+/**
+ * List pipelines in a GitLab project
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {ListPipelinesOptions} options - Options for filtering pipelines
+ * @returns {Promise} List of pipelines
+ */
+async function listPipelines(
+ projectId: string,
+ options: Omit = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines`
+ );
+
+ // Add all query parameters
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ url.searchParams.append(key, value.toString());
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabPipelineSchema).parse(data);
+}
+
+/**
+ * Get details of a specific pipeline
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} pipelineId - The ID of the pipeline
+ * @returns {Promise} Pipeline details
+ */
+async function getPipeline(
+ projectId: string,
+ pipelineId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Pipeline not found`);
+ }
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabPipelineSchema.parse(data);
+}
+
+/**
+ * List all jobs in a specific pipeline
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} pipelineId - The ID of the pipeline
+ * @param {Object} options - Options for filtering jobs
+ * @returns {Promise} List of pipeline jobs
+ */
+async function listPipelineJobs(
+ projectId: string,
+ pipelineId: number | string,
+ options: Omit = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/jobs`
+ );
+
+ // Add all query parameters
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (typeof value === "boolean") {
+ url.searchParams.append(key, value ? "true" : "false");
+ } else {
+ url.searchParams.append(key, value.toString());
+ }
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Pipeline not found`);
+ }
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabPipelineJobSchema).parse(data);
+}
+
+/**
+ * List all trigger jobs (bridges) in a specific pipeline
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} pipelineId - The ID of the pipeline
+ * @param {Object} options - Options for filtering trigger jobs
+ * @returns {Promise} List of pipeline trigger jobs
+ */
+async function listPipelineTriggerJobs(
+ projectId: string,
+ pipelineId: number | string,
+ options: Omit = {}
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/bridges`
+ );
+
+ // Add all query parameters
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (typeof value === "boolean") {
+ url.searchParams.append(key, value ? "true" : "false");
+ } else {
+ url.searchParams.append(key, value.toString());
+ }
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Pipeline not found`);
+ }
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabPipelineTriggerJobSchema).parse(data);
+}
+
+async function getPipelineJob(
+ projectId: string,
+ jobId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Job not found`);
+ }
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabPipelineJobSchema.parse(data);
+}
+
+/**
+ * Get the output/trace of a pipeline job
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} jobId - The ID of the job
+ * @param {number} limit - Maximum number of lines to return from the end (default: 1000)
+ * @param {number} offset - Number of lines to skip from the end (default: 0)
+ * @returns {Promise} The job output/trace
+ */
+async function getPipelineJobOutput(
+ projectId: string,
+ jobId: number | string,
+ limit?: number,
+ offset?: number
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/trace`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ headers: {
+ ...DEFAULT_HEADERS,
+ Accept: "text/plain", // Override Accept header to get plain text
+ },
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Job trace not found or job is not finished yet`);
+ }
+
+ await handleGitLabError(response);
+ const fullTrace = await response.text();
+
+ // Apply client-side pagination to limit context window usage
+ if (limit !== undefined || offset !== undefined) {
+ const lines = fullTrace.split("\n");
+ const startOffset = offset || 0;
+ const maxLines = limit || 1000;
+
+ // Return lines from the end, skipping offset lines and limiting to maxLines
+ const startIndex = Math.max(0, lines.length - startOffset - maxLines);
+ const endIndex = lines.length - startOffset;
+
+ const selectedLines = lines.slice(startIndex, endIndex);
+ const result = selectedLines.join("\n");
+
+ // Add metadata about truncation
+ if (startIndex > 0 || endIndex < lines.length) {
+ const totalLines = lines.length;
+ const shownLines = selectedLines.length;
+ const skippedFromStart = startIndex;
+ const skippedFromEnd = startOffset;
+
+ return `[Log truncated: showing ${shownLines} of ${totalLines} lines, skipped ${skippedFromStart} from start, ${skippedFromEnd} from end]\n\n${result}`;
+ }
+
+ return result;
+ }
+
+ return fullTrace;
+}
+
+/**
+ * Create a new pipeline
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} ref - The branch or tag to run the pipeline on
+ * @param {Array} variables - Optional variables for the pipeline
+ * @returns {Promise} The created pipeline
+ */
+async function createPipeline(
+ projectId: string,
+ ref: string,
+ variables?: Array<{ key: string; value: string }>
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`
+ );
+
+ const body: any = { ref };
+ if (variables && variables.length > 0) {
+ body.variables = variables;
+ }
+
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: DEFAULT_HEADERS,
+ body: JSON.stringify(body),
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabPipelineSchema.parse(data);
+}
+
+/**
+ * Retry a pipeline
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} pipelineId - The ID of the pipeline to retry
+ * @returns {Promise} The retried pipeline
+ */
+async function retryPipeline(
+ projectId: string,
+ pipelineId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/retry`
+ );
+
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: DEFAULT_HEADERS,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabPipelineSchema.parse(data);
+}
+
+/**
+ * Cancel a pipeline
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} pipelineId - The ID of the pipeline to cancel
+ * @returns {Promise} The canceled pipeline
+ */
+async function cancelPipeline(
+ projectId: string,
+ pipelineId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId); // Decode project ID
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/cancel`
+ );
+
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: DEFAULT_HEADERS,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabPipelineSchema.parse(data);
+}
+
+/**
+ * Get the repository tree for a project
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {GetRepositoryTreeOptions} options - Options for the tree
+ * @returns {Promise}
+ */
+async function getRepositoryTree(options: GetRepositoryTreeOptions): Promise {
+ options.project_id = decodeURIComponent(options.project_id); // Decode project_id within options
+ const queryParams = new URLSearchParams();
+ if (options.path) queryParams.append("path", options.path);
+ if (options.ref) queryParams.append("ref", options.ref);
+ if (options.recursive) queryParams.append("recursive", "true");
+ if (options.per_page) queryParams.append("per_page", options.per_page.toString());
+ if (options.page_token) queryParams.append("page_token", options.page_token);
+ if (options.pagination) queryParams.append("pagination", options.pagination);
+
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+ if (IS_OLD) {
+ headers["Private-Token"] = `${GITLAB_PERSONAL_ACCESS_TOKEN}`;
+ } else {
+ headers["Authorization"] = `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`;
+ }
+ const response = await fetch(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(options.project_id)
+ )}/repository/tree?${queryParams.toString()}`,
+ {
+ headers,
+ }
+ );
+
+ if (response.status === 404) {
+ throw new Error("Repository or path not found");
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to get repository tree: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return z.array(GitLabTreeItemSchema).parse(data);
+}
+
+/**
+ * List project milestones in a GitLab project
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {Object} options - Options for listing milestones
+ * @returns {Promise} List of milestones
+ */
+async function listProjectMilestones(
+ projectId: string,
+ options: Omit, "project_id">
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`
+ );
+
+ Object.entries(options).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (key === "iids" && Array.isArray(value) && value.length > 0) {
+ value.forEach(iid => {
+ url.searchParams.append("iids[]", iid.toString());
+ });
+ } else if (value !== undefined) {
+ url.searchParams.append(key, value.toString());
+ }
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabMilestonesSchema).parse(data);
+}
+
+/**
+ * Get a single milestone in a GitLab project
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @returns {Promise} Milestone details
+ */
+async function getProjectMilestone(
+ projectId: string,
+ milestoneId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabMilestonesSchema.parse(data);
+}
+
+/**
+ * Create a new milestone in a GitLab project
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {Object} options - Options for creating a milestone
+ * @returns {Promise} Created milestone
+ */
+async function createProjectMilestone(
+ projectId: string,
+ options: Omit, "project_id">
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ body: JSON.stringify(options),
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabMilestonesSchema.parse(data);
+}
+
+/**
+ * Edit an existing milestone in a GitLab project
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @param {Object} options - Options for editing a milestone
+ * @returns {Promise} Updated milestone
+ */
+async function editProjectMilestone(
+ projectId: string,
+ milestoneId: number | string,
+ options: Omit, "project_id" | "milestone_id">
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "PUT",
+ body: JSON.stringify(options),
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabMilestonesSchema.parse(data);
+}
+
+/**
+ * Delete a milestone from a GitLab project
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @returns {Promise}
+ */
+async function deleteProjectMilestone(
+ projectId: string,
+ milestoneId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "DELETE",
+ });
+ await handleGitLabError(response);
+}
+
+/**
+ * Get all issues assigned to a single milestone
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @returns {Promise} List of issues
+ */
+async function getMilestoneIssues(
+ projectId: string,
+ milestoneId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/issues`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabIssueSchema).parse(data);
+}
+
+/**
+ * Get all merge requests assigned to a single milestone
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @returns {Promise} List of merge requests
+ */
+async function getMilestoneMergeRequests(
+ projectId: string,
+ milestoneId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/milestones/${milestoneId}/merge_requests`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabMergeRequestSchema).parse(data);
+}
+
+/**
+ * Promote a project milestone to a group milestone
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @returns {Promise} Promoted milestone
+ */
+async function promoteProjectMilestone(
+ projectId: string,
+ milestoneId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/promote`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ method: "POST",
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabMilestonesSchema.parse(data);
+}
+
+/**
+ * Get all burndown chart events for a single milestone
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {number} milestoneId - The ID of the milestone
+ * @returns {Promise} Burndown chart events
+ */
+async function getMilestoneBurndownEvents(
+ projectId: string,
+ milestoneId: number | string
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(
+ getEffectiveProjectId(projectId)
+ )}/milestones/${milestoneId}/burndown_events`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+ await handleGitLabError(response);
+ const data = await response.json();
+ return data as any[];
+}
+
+/**
+ * Get a single user from GitLab
+ *
+ * @param {string} username - The username to look up
+ * @returns {Promise} The user data or null if not found
+ */
+async function getUser(username: string): Promise {
+ try {
+ const url = new URL(`${GITLAB_API_URL}/users`);
+ url.searchParams.append("username", username);
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+
+ const users = await response.json();
+
+ // GitLab returns an array of users that match the username
+ if (Array.isArray(users) && users.length > 0) {
+ // Find exact match for username (case-sensitive)
+ const exactMatch = users.find(user => user.username === username);
+ if (exactMatch) {
+ return GitLabUserSchema.parse(exactMatch);
+ }
+ }
+
+ // No matching user found
+ return null;
+ } catch (error) {
+ logger.error(`Error fetching user by username '${username}':`, error);
+ return null;
+ }
+}
+
+/**
+ * Get multiple users from GitLab
+ *
+ * @param {string[]} usernames - Array of usernames to look up
+ * @returns {Promise} Object with usernames as keys and user objects or null as values
+ */
+async function getUsers(usernames: string[]): Promise {
+ const users: Record = {};
+
+ // Process usernames sequentially to avoid rate limiting
+ for (const username of usernames) {
+ try {
+ const user = await getUser(username);
+ users[username] = user;
+ } catch (error) {
+ logger.error(`Error processing username '${username}':`, error);
+ users[username] = null;
+ }
+ }
+
+ return GitLabUsersResponseSchema.parse(users);
+}
+
+/**
+ * List repository commits
+ * 저장소 커밋 목록 조회
+ *
+ * @param {string} projectId - Project ID or URL-encoded path
+ * @param {ListCommitsOptions} options - List commits options
+ * @returns {Promise} List of commits
+ */
+async function listCommits(
+ projectId: string,
+ options: Omit = {}
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`
+ );
+
+ // Add query parameters
+ if (options.ref_name) url.searchParams.append("ref_name", options.ref_name);
+ if (options.since) url.searchParams.append("since", options.since);
+ if (options.until) url.searchParams.append("until", options.until);
+ if (options.path) url.searchParams.append("path", options.path);
+ if (options.author) url.searchParams.append("author", options.author);
+ if (options.all) url.searchParams.append("all", options.all.toString());
+ if (options.with_stats) url.searchParams.append("with_stats", options.with_stats.toString());
+ if (options.first_parent)
+ url.searchParams.append("first_parent", options.first_parent.toString());
+ if (options.order) url.searchParams.append("order", options.order);
+ if (options.trailers) url.searchParams.append("trailers", options.trailers.toString());
+ if (options.page) url.searchParams.append("page", options.page.toString());
+ if (options.per_page) url.searchParams.append("per_page", options.per_page.toString());
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+
+ const data = await response.json();
+ return z.array(GitLabCommitSchema).parse(data);
+}
+
+/**
+ * Get a single commit
+ * 단일 커밋 정보 조회
+ *
+ * @param {string} projectId - Project ID or URL-encoded path
+ * @param {string} sha - The commit hash or name of a repository branch or tag
+ * @param {boolean} [stats] - Include commit stats
+ * @returns {Promise} The commit details
+ */
+async function getCommit(projectId: string, sha: string, stats?: boolean): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}`
+ );
+
+ if (stats) {
+ url.searchParams.append("stats", "true");
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+
+ const data = await response.json();
+ return GitLabCommitSchema.parse(data);
+}
+
+/**
+ * Get commit diff
+ * 커밋 변경사항 조회
+ *
+ * @param {string} projectId - Project ID or URL-encoded path
+ * @param {string} sha - The commit hash or name of a repository branch or tag
+ * @returns {Promise} The commit diffs
+ */
+async function getCommitDiff(projectId: string, sha: string): Promise {
+ projectId = decodeURIComponent(projectId);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/diff`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+
+ const data = await response.json();
+ return z.array(GitLabDiffSchema).parse(data);
+}
+
+/**
+ * Get the current authenticated user
+ * 현재 인증된 사용자 가져오기
+ *
+ * @returns {Promise} The current user
+ */
+async function getCurrentUser(): Promise {
+ const response = await fetch(`${GITLAB_API_URL}/user`, DEFAULT_FETCH_CONFIG);
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return GitLabUserSchema.parse(data);
+}
+
+/**
+ * List issues assigned to the current authenticated user
+ * 현재 인증된 사용자에게 할당된 이슈 목록 조회
+ *
+ * @param {MyIssuesOptions} options - Options for filtering issues
+ * @returns {Promise} List of issues assigned to the current user
+ */
+async function myIssues(options: MyIssuesOptions = {}): Promise {
+ // Get current user to find their username
+ const currentUser = await getCurrentUser();
+
+ // Use getEffectiveProjectId to handle project ID resolution
+ const effectiveProjectId = getEffectiveProjectId(options.project_id || "");
+
+ // Use listIssues with assignee_username filter
+ let listIssuesOptions: Omit, "project_id"> = {
+ state: options.state || "opened", // Default to "opened" if not specified
+ labels: options.labels,
+ milestone: options.milestone,
+ search: options.search,
+ created_after: options.created_after,
+ created_before: options.created_before,
+ updated_after: options.updated_after,
+ updated_before: options.updated_before,
+ per_page: options.per_page,
+ page: options.page,
+ };
+
+ if (currentUser.username) {
+ listIssuesOptions.assignee_username = [currentUser.username]
+ } else {
+ listIssuesOptions.assignee_id = currentUser.id
+ }
+ return listIssues(effectiveProjectId, listIssuesOptions);
+}
+
+/**
+ * List members of a GitLab project
+ * GitLab 프로젝트 멤버 목록 조회
+ *
+ * @param {string} projectId - Project ID or URL-encoded path
+ * @param {Omit} options - Options for filtering members
+ * @returns {Promise} List of project members
+ */
+async function listProjectMembers(
+ projectId: string,
+ options: Omit = {}
+): Promise {
+ projectId = decodeURIComponent(projectId);
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/members`);
+
+ // Add query parameters
+ if (options.query) url.searchParams.append("query", options.query);
+ if (options.user_ids) {
+ options.user_ids.forEach(id => url.searchParams.append("user_ids[]", id.toString()));
+ }
+ if (options.skip_users) {
+ options.skip_users.forEach(id => url.searchParams.append("skip_users[]", id.toString()));
+ }
+ if (options.per_page) url.searchParams.append("per_page", options.per_page.toString());
+ if (options.page) url.searchParams.append("page", options.page.toString());
+
+ const response = await fetch(url.toString(), DEFAULT_FETCH_CONFIG);
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ return z.array(GitLabProjectMemberSchema).parse(data);
+}
+
+/**
+ * list group iterations
+ *
+ * @param {string} groupId
+ * @param {Omit} options
+ * @returns {Promise}
+ */
+async function listGroupIterations(
+ groupId: string,
+ options: Omit, "group_id"> = {}
+): Promise {
+ groupId = decodeURIComponent(groupId);
+ const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(groupId)}/iterations`);
+
+ // クエリパラメータの追加
+ if (options.state) url.searchParams.append("state", options.state);
+ if (options.search) url.searchParams.append("search", options.search);
+ if (options.in) url.searchParams.append("in", options.in.join(","));
+ if (options.include_ancestors !== undefined)
+ url.searchParams.append("include_ancestors", options.include_ancestors.toString());
+ if (options.include_descendants !== undefined)
+ url.searchParams.append("include_descendants", options.include_descendants.toString());
+ if (options.updated_before) url.searchParams.append("updated_before", options.updated_before);
+ if (options.updated_after) url.searchParams.append("updated_after", options.updated_after);
+ if (options.page) url.searchParams.append("page", options.page.toString());
+ if (options.per_page) url.searchParams.append("per_page", options.per_page.toString());
+
+ const response = await fetch(url.toString(), DEFAULT_FETCH_CONFIG);
+
+ if (!response.ok) {
+ await handleGitLabError(response);
+ }
+
+ const data = await response.json();
+ return z.array(GroupIteration).parse(data);
+}
+
+/**
+ * Upload a file to a GitLab project for use in markdown content
+ *
+ * @param {string} projectId - The ID or URL-encoded path of the project
+ * @param {string} filePath - Path to the local file to upload
+ * @returns {Promise} The upload response
+ */
+async function markdownUpload(projectId: string, filePath: string): Promise {
+ projectId = decodeURIComponent(projectId);
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+
+ // Check if file exists
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`File not found: ${filePath}`);
+ }
+
+ // Read the file
+ const fileBuffer = fs.readFileSync(filePath);
+ const fileName = path.basename(filePath);
+
+ // Create form data
+ const FormData = (await import("form-data")).default;
+ const form = new FormData();
+ form.append("file", fileBuffer, {
+ filename: fileName,
+ contentType: "application/octet-stream",
+ });
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`
+ );
+
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ ...DEFAULT_HEADERS,
+ // Remove Content-Type header to let form-data set it with boundary
+ "Content-Type": undefined as any,
+ },
+ body: form,
+ });
+
+ if (!response.ok) {
+ await handleGitLabError(response);
+ }
+
+ const data = await response.json();
+ return GitLabMarkdownUploadSchema.parse(data);
+}
+
+async function downloadAttachment(projectId: string, secret: string, filename: string, localPath?: string): Promise {
+ const effectiveProjectId = getEffectiveProjectId(projectId);
+
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`
+ );
+
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: DEFAULT_HEADERS,
+ });
+
+ if (!response.ok) {
+ await handleGitLabError(response);
+ }
+
+ // Get the file content as buffer
+ const buffer = await response.arrayBuffer();
+
+ // Determine the save path
+ const savePath = localPath ? path.join(localPath, filename) : filename;
+
+ // Write the file to disk
+ fs.writeFileSync(savePath, Buffer.from(buffer));
+
+ return savePath;
+}
+
+server.setRequestHandler(ListToolsRequestSchema, async () => {
+ // Apply read-only filter first
+ const tools0 = GITLAB_READ_ONLY_MODE
+ ? allTools.filter(tool => readOnlyTools.includes(tool.name))
+ : allTools;
+ // Toggle wiki tools by USE_GITLAB_WIKI flag
+ const tools1 = USE_GITLAB_WIKI
+ ? tools0
+ : tools0.filter(tool => !wikiToolNames.includes(tool.name));
+ // Toggle milestone tools by USE_MILESTONE flag
+ const tools2 = USE_MILESTONE
+ ? tools1
+ : tools1.filter(tool => !milestoneToolNames.includes(tool.name));
+ // Toggle pipeline tools by USE_PIPELINE flag
+ let tools = USE_PIPELINE ? tools2 : tools2.filter(tool => !pipelineToolNames.includes(tool.name));
+
+ // <<< START: Gemini 호환성을 위해 $schema 제거 >>>
+ tools = tools.map(tool => {
+ // inputSchema가 존재하고 객체인지 확인
+ if (tool.inputSchema && typeof tool.inputSchema === "object" && tool.inputSchema !== null) {
+ // $schema 키가 존재하면 삭제
+ if ("$schema" in tool.inputSchema) {
+ // 불변성을 위해 새로운 객체 생성 (선택적이지만 권장)
+ const modifiedSchema = { ...tool.inputSchema };
+ delete modifiedSchema.$schema;
+ return { ...tool, inputSchema: modifiedSchema };
+ }
+ }
+ // 변경이 필요 없으면 그대로 반환
+ return tool;
+ });
+ // <<< END: Gemini 호환성을 위해 $schema 제거 >>>
+
+ return {
+ tools, // $schema가 제거된 도구 목록 반환
+ };
+});
+
+server.setRequestHandler(CallToolRequestSchema, async request => {
+ try {
+ if (!request.params.arguments) {
+ throw new Error("Arguments are required");
+ }
+
+ // Ensure session is established for every request if cookie authentication is enabled
+ if (GITLAB_AUTH_COOKIE_PATH) {
+ await ensureSessionForRequest();
+ }
+ logger.info(request.params.name);
+ switch (request.params.name) {
+ case "fork_repository": {
+ if (GITLAB_PROJECT_ID) {
+ throw new Error("Direct project ID is set. So fork_repository is not allowed");
+ }
+ const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
+ try {
+ const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
+ return {
+ content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }],
+ };
+ } catch (forkError) {
+ logger.error("Error forking repository:", forkError);
+ let forkErrorMessage = "Failed to fork repository";
+ if (forkError instanceof Error) {
+ forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`;
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify({ error: forkErrorMessage }, null, 2),
+ },
+ ],
+ };
+ }
+ }
+
+ case "create_branch": {
+ const args = CreateBranchSchema.parse(request.params.arguments);
+ let ref = args.ref;
+ if (!ref) {
+ ref = await getDefaultBranchRef(args.project_id);
+ }
+
+ const branch = await createBranch(args.project_id, {
+ name: args.branch,
+ ref,
+ });
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
+ };
+ }
+
+ case "get_branch_diffs": {
+ const args = GetBranchDiffsSchema.parse(request.params.arguments);
+ const diffResp = await getBranchDiffs(args.project_id, args.from, args.to, args.straight);
+
+ if (args.excluded_file_patterns?.length) {
+ const regexPatterns = args.excluded_file_patterns.map(pattern => new RegExp(pattern));
+
+ // Helper function to check if a path matches any regex pattern
+ const matchesAnyPattern = (path: string): boolean => {
+ if (!path) return false;
+ return regexPatterns.some(regex => regex.test(path));
+ };
+
+ // Filter out files that match any of the regex patterns on new files
+ diffResp.diffs = diffResp.diffs.filter(diff => !matchesAnyPattern(diff.new_path));
+ }
+ return {
+ content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }],
+ };
+ }
+
+ case "search_repositories": {
+ const args = SearchRepositoriesSchema.parse(request.params.arguments);
+ const results = await searchProjects(args.search, args.page, args.per_page);
+ return {
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
+ };
+ }
+
+ case "create_repository": {
+ if (GITLAB_PROJECT_ID) {
+ throw new Error("Direct project ID is set. So fork_repository is not allowed");
+ }
+ const args = CreateRepositorySchema.parse(request.params.arguments);
+ const repository = await createRepository(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
+ };
+ }
+
+ case "get_file_contents": {
+ const args = GetFileContentsSchema.parse(request.params.arguments);
+ const contents = await getFileContents(args.project_id, args.file_path, args.ref);
+ return {
+ content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
+ };
+ }
+
+ case "create_or_update_file": {
+ const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
+ const result = await createOrUpdateFile(
+ args.project_id,
+ args.file_path,
+ args.content,
+ args.commit_message,
+ args.branch,
+ args.previous_path,
+ args.last_commit_id,
+ args.commit_id
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
+ };
+ }
+
+ case "push_files": {
+ const args = PushFilesSchema.parse(request.params.arguments);
+ const result = await createCommit(
+ args.project_id,
+ args.commit_message,
+ args.branch,
+ args.files.map(f => ({ path: f.file_path, content: f.content }))
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
+ };
+ }
+
+ case "create_issue": {
+ const args = CreateIssueSchema.parse(request.params.arguments);
+ const { project_id, ...options } = args;
+ const issue = await createIssue(project_id, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
+ };
+ }
+
+ case "create_merge_request": {
+ const args = CreateMergeRequestSchema.parse(request.params.arguments);
+ const { project_id, ...options } = args;
+ const mergeRequest = await createMergeRequest(project_id, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
+ };
+ }
+
+ case "update_merge_request_note": {
+ const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
+ const note = await updateMergeRequestNote(
+ args.project_id,
+ args.merge_request_iid,
+ args.discussion_id,
+ args.note_id,
+ args.body, // Now optional
+ args.resolved // Now one of body or resolved must be provided, not both
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
+ };
+ }
+
+ case "create_merge_request_note": {
+ const args = CreateMergeRequestNoteSchema.parse(request.params.arguments);
+ const note = await createMergeRequestNote(
+ args.project_id,
+ args.merge_request_iid,
+ args.discussion_id,
+ args.body,
+ args.created_at
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
+ };
+ }
+
+ case "update_issue_note": {
+ const args = UpdateIssueNoteSchema.parse(request.params.arguments);
+ const note = await updateIssueNote(
+ args.project_id,
+ args.issue_iid,
+ args.discussion_id,
+ args.note_id,
+ args.body
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
+ };
+ }
+
+ case "create_issue_note": {
+ const args = CreateIssueNoteSchema.parse(request.params.arguments);
+ const note = await createIssueNote(
+ args.project_id,
+ args.issue_iid,
+ args.discussion_id,
+ args.body,
+ args.created_at
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
+ };
+ }
+
+ case "get_merge_request": {
+ const args = GetMergeRequestSchema.parse(request.params.arguments);
+ const mergeRequest = await getMergeRequest(
+ args.project_id,
+ args.merge_request_iid,
+ args.source_branch
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
+ };
+ }
+
+ case "get_merge_request_diffs": {
+ const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
+ const diffs = await getMergeRequestDiffs(
+ args.project_id,
+ args.merge_request_iid,
+ args.source_branch,
+ args.view
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
+ };
+ }
+
+ case "list_merge_request_diffs": {
+ const args = ListMergeRequestDiffsSchema.parse(request.params.arguments);
+ const changes = await listMergeRequestDiffs(
+ args.project_id,
+ args.merge_request_iid,
+ args.source_branch,
+ args.page,
+ args.per_page,
+ args.unidiff
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
+ };
+ }
+
+ case "update_merge_request": {
+ const args = UpdateMergeRequestSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, source_branch, ...options } = args;
+ const mergeRequest = await updateMergeRequest(
+ project_id,
+ options,
+ merge_request_iid,
+ source_branch
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
+ };
+ }
+
+ case "merge_merge_request": {
+ const args = MergeMergeRequestSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, ...options } = args;
+ const mergeRequest = await mergeMergeRequest(project_id, options, merge_request_iid);
+ return {
+ content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
+ };
+ }
+
+ case "mr_discussions": {
+ const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, ...options } = args;
+ const discussions = await listMergeRequestDiscussions(
+ project_id,
+ merge_request_iid,
+ options
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
+ };
+ }
+
+ case "list_namespaces": {
+ const args = ListNamespacesSchema.parse(request.params.arguments);
+ const url = new URL(`${GITLAB_API_URL}/namespaces`);
+
+ if (args.search) {
+ url.searchParams.append("search", args.search);
+ }
+ if (args.page) {
+ url.searchParams.append("page", args.page.toString());
+ }
+ if (args.per_page) {
+ url.searchParams.append("per_page", args.per_page.toString());
+ }
+ if (args.owned) {
+ url.searchParams.append("owned", args.owned.toString());
+ }
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ const namespaces = z.array(GitLabNamespaceSchema).parse(data);
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }],
+ };
+ }
+
+ case "get_namespace": {
+ const args = GetNamespaceSchema.parse(request.params.arguments);
+ const url = new URL(
+ `${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ const namespace = GitLabNamespaceSchema.parse(data);
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
+ };
+ }
+
+ case "verify_namespace": {
+ const args = VerifyNamespaceSchema.parse(request.params.arguments);
+ const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }],
+ };
+ }
+
+ case "get_project": {
+ const args = GetProjectSchema.parse(request.params.arguments);
+ const url = new URL(
+ `${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(args.project_id))}`
+ );
+
+ const response = await fetch(url.toString(), {
+ ...DEFAULT_FETCH_CONFIG,
+ });
+
+ await handleGitLabError(response);
+ const data = await response.json();
+ const project = GitLabProjectSchema.parse(data);
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
+ };
+ }
+
+ case "list_projects": {
+ const args = ListProjectsSchema.parse(request.params.arguments);
+ const projects = await listProjects(args);
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
+ };
+ }
+
+ case "list_project_members": {
+ const args = ListProjectMembersSchema.parse(request.params.arguments);
+ const { project_id, ...options } = args;
+ const members = await listProjectMembers(project_id, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(members, null, 2) }],
+ };
+ }
+
+ case "get_users": {
+ const args = GetUsersSchema.parse(request.params.arguments);
+ const usersMap = await getUsers(args.usernames);
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
+ };
+ }
+
+ case "create_note": {
+ const args = CreateNoteSchema.parse(request.params.arguments);
+ const { project_id, noteable_type, noteable_iid, body } = args;
+
+ const note = await createNote(project_id, noteable_type, noteable_iid, body);
+ return {
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
+ };
+ }
+
+ case "get_draft_note": {
+ const args = GetDraftNoteSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, draft_note_id } = args;
+
+ const draftNote = await getDraftNote(project_id, merge_request_iid, draft_note_id);
+ return {
+ content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
+ };
+ }
+
+ case "list_draft_notes": {
+ const args = ListDraftNotesSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid } = args;
+
+ const draftNotes = await listDraftNotes(project_id, merge_request_iid);
+ return {
+ content: [{ type: "text", text: JSON.stringify(draftNotes, null, 2) }],
+ };
+ }
+
+ case "create_draft_note": {
+ const args = CreateDraftNoteSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, body, position, resolve_discussion } = args;
+
+ const draftNote = await createDraftNote(project_id, merge_request_iid, body, position, resolve_discussion);
+ return {
+ content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
+ };
+ }
+
+ case "update_draft_note": {
+ const args = UpdateDraftNoteSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion } = args;
+
+ const draftNote = await updateDraftNote(project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion);
+ return {
+ content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
+ };
+ }
+
+ case "delete_draft_note": {
+ const args = DeleteDraftNoteSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, draft_note_id } = args;
+
+ await deleteDraftNote(project_id, merge_request_iid, draft_note_id);
+ return {
+ content: [{ type: "text", text: "Draft note deleted successfully" }],
+ };
+ }
+
+ case "publish_draft_note": {
+ const args = PublishDraftNoteSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, draft_note_id } = args;
+
+ const publishedNote = await publishDraftNote(project_id, merge_request_iid, draft_note_id);
+ return {
+ content: [{ type: "text", text: JSON.stringify(publishedNote, null, 2) }],
+ };
+ }
+
+ case "bulk_publish_draft_notes": {
+ const args = BulkPublishDraftNotesSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid } = args;
+
+ const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
+ return {
+ content: [{ type: "text", text: JSON.stringify(publishedNotes, null, 2) }],
+ };
+ }
+
+ case "create_merge_request_thread": {
+ const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
+ const { project_id, merge_request_iid, body, position, created_at } = args;
+
+ const thread = await createMergeRequestThread(
+ project_id,
+ merge_request_iid,
+ body,
+ position,
+ created_at
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
+ };
+ }
+
+ case "list_issues": {
+ const args = ListIssuesSchema.parse(request.params.arguments);
+ const { project_id, ...options } = args;
+ const issues = await listIssues(project_id, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
+ };
+ }
+
+ case "my_issues": {
+ const args = MyIssuesSchema.parse(request.params.arguments);
+ const issues = await myIssues(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
+ };
+ }
+
+ case "get_issue": {
+ const args = GetIssueSchema.parse(request.params.arguments);
+ const issue = await getIssue(args.project_id, args.issue_iid);
+ return {
+ content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
+ };
+ }
+
+ case "update_issue": {
+ const args = UpdateIssueSchema.parse(request.params.arguments);
+ const { project_id, issue_iid, ...options } = args;
+ const issue = await updateIssue(project_id, issue_iid, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
+ };
+ }
+
+ case "delete_issue": {
+ const args = DeleteIssueSchema.parse(request.params.arguments);
+ await deleteIssue(args.project_id, args.issue_iid);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(
+ { status: "success", message: "Issue deleted successfully" },
+ null,
+ 2
+ ),
+ },
+ ],
+ };
+ }
+
+ case "list_issue_links": {
+ const args = ListIssueLinksSchema.parse(request.params.arguments);
+ const links = await listIssueLinks(args.project_id, args.issue_iid);
+ return {
+ content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
+ };
+ }
+
+ case "list_issue_discussions": {
+ const args = ListIssueDiscussionsSchema.parse(request.params.arguments);
+ const { project_id, issue_iid, ...options } = args;
+
+ const discussions = await listIssueDiscussions(project_id, issue_iid, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
+ };
+ }
+
+ case "get_issue_link": {
+ const args = GetIssueLinkSchema.parse(request.params.arguments);
+ const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
+ return {
+ content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
+ };
+ }
+
+ case "create_issue_link": {
+ const args = CreateIssueLinkSchema.parse(request.params.arguments);
+ const link = await createIssueLink(
+ args.project_id,
+ args.issue_iid,
+ args.target_project_id,
+ args.target_issue_iid,
+ args.link_type
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
+ };
+ }
+
+ case "delete_issue_link": {
+ const args = DeleteIssueLinkSchema.parse(request.params.arguments);
+ await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(
+ {
+ status: "success",
+ message: "Issue link deleted successfully",
+ },
+ null,
+ 2
+ ),
+ },
+ ],
+ };
+ }
+
+ case "list_labels": {
+ const args = ListLabelsSchema.parse(request.params.arguments);
+ const labels = await listLabels(args.project_id, args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
+ };
+ }
+
+ case "get_label": {
+ const args = GetLabelSchema.parse(request.params.arguments);
+ const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
+ return {
+ content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
+ };
+ }
+
+ case "create_label": {
+ const args = CreateLabelSchema.parse(request.params.arguments);
+ const label = await createLabel(args.project_id, args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
+ };
+ }
+
+ case "update_label": {
+ const args = UpdateLabelSchema.parse(request.params.arguments);
+ const { project_id, label_id, ...options } = args;
+ const label = await updateLabel(project_id, label_id, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
+ };
+ }
+
+ case "delete_label": {
+ const args = DeleteLabelSchema.parse(request.params.arguments);
+ await deleteLabel(args.project_id, args.label_id);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(
+ { status: "success", message: "Label deleted successfully" },
+ null,
+ 2
+ ),
+ },
+ ],
+ };
+ }
+
+ case "list_group_projects": {
+ const args = ListGroupProjectsSchema.parse(request.params.arguments);
+ const projects = await listGroupProjects(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
+ };
+ }
+
+ case "list_wiki_pages": {
+ const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(
+ request.params.arguments
+ );
+ const wikiPages = await listWikiPages(project_id, {
+ page,
+ per_page,
+ with_content,
+ });
+ return {
+ content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
+ };
+ }
- await handleGitLabError(response);
- return GitLabMergeRequestSchema.parse(await response.json());
-}
+ case "get_wiki_page": {
+ const { project_id, slug } = GetWikiPageSchema.parse(request.params.arguments);
+ const wikiPage = await getWikiPage(project_id, slug);
+ return {
+ content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
+ };
+ }
-// MR 변경사항 조회 함수
-async function getMergeRequestDiffs(
- projectId: string,
- mergeRequestIid: number,
- view?: "inline" | "parallel"
-): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/merge_requests/${mergeRequestIid}/changes`
- );
+ case "create_wiki_page": {
+ const { project_id, title, content, format } = CreateWikiPageSchema.parse(
+ request.params.arguments
+ );
+ const wikiPage = await createWikiPage(project_id, title, content, format);
+ return {
+ content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
+ };
+ }
- if (view) {
- url.searchParams.append("view", view);
- }
+ case "update_wiki_page": {
+ const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse(
+ request.params.arguments
+ );
+ const wikiPage = await updateWikiPage(project_id, slug, title, content, format);
+ return {
+ content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
+ };
+ }
- const response = await fetch(url.toString(), {
- headers: DEFAULT_HEADERS,
- });
+ case "delete_wiki_page": {
+ const { project_id, slug } = DeleteWikiPageSchema.parse(request.params.arguments);
+ await deleteWikiPage(project_id, slug);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(
+ {
+ status: "success",
+ message: "Wiki page deleted successfully",
+ },
+ null,
+ 2
+ ),
+ },
+ ],
+ };
+ }
- await handleGitLabError(response);
- const data = (await response.json()) as { changes: unknown };
- return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
-}
+ case "get_repository_tree": {
+ const args = GetRepositoryTreeSchema.parse(request.params.arguments);
+ const tree = await getRepositoryTree(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(tree, null, 2) }],
+ };
+ }
-// MR 업데이트 함수
-async function updateMergeRequest(
- projectId: string,
- mergeRequestIid: number,
- options: Omit<
- z.infer,
- "project_id" | "merge_request_iid"
- >
-): Promise {
- const url = new URL(
- `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
- projectId
- )}/merge_requests/${mergeRequestIid}`
- );
+ case "list_pipelines": {
+ const args = ListPipelinesSchema.parse(request.params.arguments);
+ const { project_id, ...options } = args;
+ const pipelines = await listPipelines(project_id, options);
+ return {
+ content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
+ };
+ }
- const response = await fetch(url.toString(), {
- method: "PUT",
- headers: DEFAULT_HEADERS,
- body: JSON.stringify(options),
- });
+ case "get_pipeline": {
+ const { project_id, pipeline_id } = GetPipelineSchema.parse(request.params.arguments);
+ const pipeline = await getPipeline(project_id, pipeline_id);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(pipeline, null, 2),
+ },
+ ],
+ };
+ }
- await handleGitLabError(response);
- return GitLabMergeRequestSchema.parse(await response.json());
-}
+ case "list_pipeline_jobs": {
+ const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(
+ request.params.arguments
+ );
+ const jobs = await listPipelineJobs(project_id, pipeline_id, options);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(jobs, null, 2),
+ },
+ ],
+ };
+ }
-server.setRequestHandler(ListToolsRequestSchema, async () => {
- return {
- tools: [
- {
- name: "create_or_update_file",
- description: "Create or update a single file in a GitLab project",
- inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
- },
- {
- name: "search_repositories",
- description: "Search for GitLab projects",
- inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
- },
- {
- name: "create_repository",
- description: "Create a new GitLab project",
- inputSchema: zodToJsonSchema(CreateRepositorySchema),
- },
- {
- name: "get_file_contents",
- description:
- "Get the contents of a file or directory from a GitLab project",
- inputSchema: zodToJsonSchema(GetFileContentsSchema),
- },
- {
- name: "push_files",
- description:
- "Push multiple files to a GitLab project in a single commit",
- inputSchema: zodToJsonSchema(PushFilesSchema),
- },
- {
- name: "create_issue",
- description: "Create a new issue in a GitLab project",
- inputSchema: zodToJsonSchema(CreateIssueSchema),
- },
- {
- name: "create_merge_request",
- description: "Create a new merge request in a GitLab project",
- inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
- },
- {
- name: "fork_repository",
- description:
- "Fork a GitLab project to your account or specified namespace",
- inputSchema: zodToJsonSchema(ForkRepositorySchema),
- },
- {
- name: "create_branch",
- description: "Create a new branch in a GitLab project",
- inputSchema: zodToJsonSchema(CreateBranchSchema),
- },
- {
- name: "get_merge_request",
- description: "Get details of a merge request",
- inputSchema: zodToJsonSchema(GetMergeRequestSchema),
- },
- {
- name: "get_merge_request_diffs",
- description: "Get the changes/diffs of a merge request",
- inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
- },
- {
- name: "update_merge_request",
- description: "Update a merge request",
- inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
- },
- ],
- };
-});
+ case "list_pipeline_trigger_jobs": {
+ const { project_id, pipeline_id, ...options } = ListPipelineTriggerJobsSchema.parse(
+ request.params.arguments
+ );
+ const triggerJobs = await listPipelineTriggerJobs(project_id, pipeline_id, options);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(triggerJobs, null, 2),
+ },
+ ],
+ };
+ }
-server.setRequestHandler(CallToolRequestSchema, async (request) => {
- try {
- if (!request.params.arguments) {
- throw new Error("Arguments are required");
- }
+ case "get_pipeline_job": {
+ const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments);
+ const jobDetails = await getPipelineJob(project_id, job_id);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(jobDetails, null, 2),
+ },
+ ],
+ };
+ }
- switch (request.params.name) {
- case "fork_repository": {
- const args = ForkRepositorySchema.parse(request.params.arguments);
- const fork = await forkProject(args.project_id, args.namespace);
+ case "get_pipeline_job_output": {
+ const { project_id, job_id, limit, offset } = GetPipelineJobOutputSchema.parse(
+ request.params.arguments
+ );
+ const jobOutput = await getPipelineJobOutput(project_id, job_id, limit, offset);
return {
- content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: jobOutput,
+ },
+ ],
};
}
- case "create_branch": {
- const args = CreateBranchSchema.parse(request.params.arguments);
- let ref = args.ref;
- if (!ref) {
- ref = await getDefaultBranchRef(args.project_id);
- }
+ case "create_pipeline": {
+ const { project_id, ref, variables } = CreatePipelineSchema.parse(request.params.arguments);
+ const pipeline = await createPipeline(project_id, ref, variables);
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Created pipeline #${pipeline.id} for ${ref}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
+ },
+ ],
+ };
+ }
- const branch = await createBranch(args.project_id, {
- name: args.branch,
- ref,
- });
+ case "retry_pipeline": {
+ const { project_id, pipeline_id } = RetryPipelineSchema.parse(request.params.arguments);
+ const pipeline = await retryPipeline(project_id, pipeline_id);
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Retried pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
+ },
+ ],
+ };
+ }
+ case "cancel_pipeline": {
+ const { project_id, pipeline_id } = CancelPipelineSchema.parse(request.params.arguments);
+ const pipeline = await cancelPipeline(project_id, pipeline_id);
return {
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: `Canceled pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
+ },
+ ],
};
}
- case "search_repositories": {
- const args = SearchRepositoriesSchema.parse(request.params.arguments);
- const results = await searchProjects(
- args.search,
- args.page,
- args.per_page
- );
+ case "list_merge_requests": {
+ const args = ListMergeRequestsSchema.parse(request.params.arguments);
+ const mergeRequests = await listMergeRequests(args.project_id, args);
return {
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
+ content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }],
};
}
- case "create_repository": {
- const args = CreateRepositorySchema.parse(request.params.arguments);
- const repository = await createRepository(args);
+ case "list_milestones": {
+ const { project_id, ...options } = ListProjectMilestonesSchema.parse(
+ request.params.arguments
+ );
+ const milestones = await listProjectMilestones(project_id, options);
return {
content: [
- { type: "text", text: JSON.stringify(repository, null, 2) },
+ {
+ type: "text",
+ text: JSON.stringify(milestones, null, 2),
+ },
],
};
}
- case "get_file_contents": {
- const args = GetFileContentsSchema.parse(request.params.arguments);
- const contents = await getFileContents(
- args.project_id,
- args.file_path,
- args.ref
+ case "get_milestone": {
+ const { project_id, milestone_id } = GetProjectMilestoneSchema.parse(
+ request.params.arguments
);
+ const milestone = await getProjectMilestone(project_id, milestone_id);
return {
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(milestone, null, 2),
+ },
+ ],
};
}
- case "create_or_update_file": {
- const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
- const result = await createOrUpdateFile(
- args.project_id,
- args.file_path,
- args.content,
- args.commit_message,
- args.branch,
- args.previous_path
+ case "create_milestone": {
+ const { project_id, ...options } = CreateProjectMilestoneSchema.parse(
+ request.params.arguments
);
+ const milestone = await createProjectMilestone(project_id, options);
return {
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(milestone, null, 2),
+ },
+ ],
};
}
- case "push_files": {
- const args = PushFilesSchema.parse(request.params.arguments);
- const result = await createCommit(
- args.project_id,
- args.commit_message,
- args.branch,
- args.files.map((f) => ({ path: f.file_path, content: f.content }))
+ case "edit_milestone": {
+ const { project_id, milestone_id, ...options } = EditProjectMilestoneSchema.parse(
+ request.params.arguments
);
+ const milestone = await editProjectMilestone(project_id, milestone_id, options);
return {
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(milestone, null, 2),
+ },
+ ],
};
}
- case "create_issue": {
- const args = CreateIssueSchema.parse(request.params.arguments);
- const { project_id, ...options } = args;
- const issue = await createIssue(project_id, options);
+ case "delete_milestone": {
+ const { project_id, milestone_id } = DeleteProjectMilestoneSchema.parse(
+ request.params.arguments
+ );
+ await deleteProjectMilestone(project_id, milestone_id);
return {
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(
+ {
+ status: "success",
+ message: "Milestone deleted successfully",
+ },
+ null,
+ 2
+ ),
+ },
+ ],
};
}
- case "create_merge_request": {
- const args = CreateMergeRequestSchema.parse(request.params.arguments);
- const { project_id, ...options } = args;
- const mergeRequest = await createMergeRequest(project_id, options);
+ case "get_milestone_issue": {
+ const { project_id, milestone_id } = GetMilestoneIssuesSchema.parse(
+ request.params.arguments
+ );
+ const issues = await getMilestoneIssues(project_id, milestone_id);
return {
content: [
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
+ {
+ type: "text",
+ text: JSON.stringify(issues, null, 2),
+ },
],
};
}
- case "get_merge_request": {
- const args = GetMergeRequestSchema.parse(request.params.arguments);
- const mergeRequest = await getMergeRequest(
- args.project_id,
- args.merge_request_iid
+ case "get_milestone_merge_requests": {
+ const { project_id, milestone_id } = GetMilestoneMergeRequestsSchema.parse(
+ request.params.arguments
);
+ const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id);
return {
content: [
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
+ {
+ type: "text",
+ text: JSON.stringify(mergeRequests, null, 2),
+ },
],
};
}
- case "get_merge_request_diffs": {
- const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
- const diffs = await getMergeRequestDiffs(
- args.project_id,
- args.merge_request_iid,
- args.view
+ case "promote_milestone": {
+ const { project_id, milestone_id } = PromoteProjectMilestoneSchema.parse(
+ request.params.arguments
);
+ const milestone = await promoteProjectMilestone(project_id, milestone_id);
return {
- content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(milestone, null, 2),
+ },
+ ],
};
}
- case "update_merge_request": {
- const args = UpdateMergeRequestSchema.parse(request.params.arguments);
- const { project_id, merge_request_iid, ...options } = args;
- const mergeRequest = await updateMergeRequest(
- project_id,
- merge_request_iid,
- options
+ case "get_milestone_burndown_events": {
+ const { project_id, milestone_id } = GetMilestoneBurndownEventsSchema.parse(
+ request.params.arguments
);
+ const events = await getMilestoneBurndownEvents(project_id, milestone_id);
return {
content: [
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
+ {
+ type: "text",
+ text: JSON.stringify(events, null, 2),
+ },
],
};
}
+ case "list_commits": {
+ const args = ListCommitsSchema.parse(request.params.arguments);
+ const commits = await listCommits(args.project_id, args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
+ };
+ }
+
+ case "get_commit": {
+ const args = GetCommitSchema.parse(request.params.arguments);
+ const commit = await getCommit(args.project_id, args.sha, args.stats);
+ return {
+ content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
+ };
+ }
+
+ case "get_commit_diff": {
+ const args = GetCommitDiffSchema.parse(request.params.arguments);
+ const diff = await getCommitDiff(args.project_id, args.sha);
+ return {
+ content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
+ };
+ }
+
+ case "list_group_iterations": {
+ const args = ListGroupIterationsSchema.parse(request.params.arguments);
+ const iterations = await listGroupIterations(args.group_id, args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
+ };
+ }
+
+ case "upload_markdown": {
+ const args = MarkdownUploadSchema.parse(request.params.arguments);
+ const upload = await markdownUpload(args.project_id, args.file_path);
+ return {
+ content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
+ };
+ }
+
+ case "download_attachment": {
+ const args = DownloadAttachmentSchema.parse(request.params.arguments);
+ const filePath = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
+ return {
+ content: [{ type: "text", text: JSON.stringify({ success: true, file_path: filePath }, null, 2) }],
+ };
+ }
+
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
+ logger.debug(request.params);
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
- .map((e) => `${e.path.join(".")}: ${e.message}`)
+ .map(e => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
@@ -812,13 +5192,202 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
});
-async function runServer() {
+/**
+ * Color constants for terminal output
+ */
+const colorGreen = "\x1b[32m";
+const colorReset = "\x1b[0m";
+
+/**
+ * Determine the transport mode based on environment variables and availability
+ *
+ * Transport mode priority (highest to lowest):
+ * 1. STREAMABLE_HTTP
+ * 2. SSE
+ * 3. STDIO
+ */
+function determineTransportMode(): TransportMode {
+ // Check for streamable-http support (highest priority)
+ if (STREAMABLE_HTTP) {
+ return TransportMode.STREAMABLE_HTTP;
+ }
+
+ // Check for SSE support (medium priority)
+ if (SSE) {
+ return TransportMode.SSE;
+ }
+
+ // Default to stdio (lowest priority)
+ return TransportMode.STDIO;
+}
+
+/**
+ * Start server with stdio transport
+ */
+async function startStdioServer(): Promise {
const transport = new StdioServerTransport();
await server.connect(transport);
- console.error("GitLab MCP Server running on stdio");
}
-runServer().catch((error) => {
- console.error("Fatal error in main():", error);
+/**
+ * Start server with traditional SSE transport
+ */
+async function startSSEServer(): Promise {
+ const app = express();
+ const transports: { [sessionId: string]: SSEServerTransport } = {};
+
+ app.get("/sse", async (_: Request, res: Response) => {
+ const transport = new SSEServerTransport("/messages", res);
+ transports[transport.sessionId] = transport;
+ res.on("close", () => {
+ delete transports[transport.sessionId];
+ });
+ await server.connect(transport);
+ });
+
+ app.post("/messages", async (req: Request, res: Response) => {
+ const sessionId = req.query.sessionId as string;
+ const transport = transports[sessionId];
+ if (transport) {
+ await transport.handlePostMessage(req, res);
+ } else {
+ res.status(400).send("No transport found for sessionId");
+ }
+ });
+
+ app.get("/health", (_: Request, res: Response) => {
+ res.status(200).json({
+ status: "healthy",
+ version: SERVER_VERSION,
+ transport: TransportMode.SSE,
+ });
+ });
+
+ app.listen(Number(PORT), HOST, () => {
+ logger.info(`GitLab MCP Server running with SSE transport`);
+ const colorGreen = "\x1b[32m";
+ const colorReset = "\x1b[0m";
+ logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/sse${colorReset}`);
+ });
+}
+
+/**
+ * Start server with Streamable HTTP transport
+ */
+async function startStreamableHTTPServer(): Promise {
+ const app = express();
+ const streamableTransports: {
+ [sessionId: string]: StreamableHTTPServerTransport;
+ } = {};
+
+ // Configure Express middleware
+ app.use(express.json());
+
+ // Streamable HTTP endpoint - handles both session creation and message handling
+ app.post("/mcp", async (req: Request, res: Response) => {
+ const sessionId = req.headers["mcp-session-id"] as string;
+
+ try {
+ let transport: StreamableHTTPServerTransport;
+
+ if (sessionId && streamableTransports[sessionId]) {
+ // Reuse existing transport for ongoing session
+ transport = streamableTransports[sessionId];
+ await transport.handleRequest(req, res, req.body);
+ } else {
+ // Create new transport for new session
+ transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: () => randomUUID(),
+ onsessioninitialized: (newSessionId: string) => {
+ streamableTransports[newSessionId] = transport;
+ logger.warn(`Streamable HTTP session initialized: ${newSessionId}`);
+ },
+ });
+
+ // Set up cleanup handler when transport closes
+ transport.onclose = () => {
+ const sid = transport.sessionId;
+ if (sid && streamableTransports[sid]) {
+ logger.warn(`Streamable HTTP transport closed for session ${sid}, cleaning up`);
+ delete streamableTransports[sid];
+ }
+ };
+
+ // Connect transport to MCP server before handling the request
+ await server.connect(transport);
+ await transport.handleRequest(req, res, req.body);
+ }
+ } catch (error) {
+ logger.error("Streamable HTTP error:", error);
+ res.status(500).json({
+ error: "Internal server error",
+ message: error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+ });
+
+ // Health check endpoint
+ app.get("/health", (_: Request, res: Response) => {
+ res.status(200).json({
+ status: "healthy",
+ version: SERVER_VERSION,
+ transport: TransportMode.STREAMABLE_HTTP,
+ activeSessions: Object.keys(streamableTransports).length,
+ });
+ });
+
+ // Start server
+ app.listen(Number(PORT), HOST, () => {
+ logger.info(`GitLab MCP Server running with Streamable HTTP transport`);
+ logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/mcp${colorReset}`);
+ });
+}
+
+/**
+ * Initialize server with specific transport mode
+ * Handle transport-specific initialization logic
+ */
+async function initializeServerByTransportMode(mode: TransportMode): Promise {
+ logger.info("Initializing server with transport mode:", mode);
+ switch (mode) {
+ case TransportMode.STDIO:
+ logger.warn("Starting GitLab MCP Server with stdio transport");
+ await startStdioServer();
+ break;
+
+ case TransportMode.SSE:
+ logger.warn("Starting GitLab MCP Server with SSE transport");
+ await startSSEServer();
+ break;
+
+ case TransportMode.STREAMABLE_HTTP:
+ logger.warn("Starting GitLab MCP Server with Streamable HTTP transport");
+ await startStreamableHTTPServer();
+ break;
+
+ default:
+ // This should never happen with proper enum usage, but TypeScript requires it
+ const exhaustiveCheck: never = mode;
+ throw new Error(`Unknown transport mode: ${exhaustiveCheck}`);
+ }
+}
+
+/**
+ * Initialize and run the server
+ * Main entry point for server startup
+ */
+async function runServer() {
+ try {
+ const transportMode = determineTransportMode();
+ await initializeServerByTransportMode(transportMode);
+ } catch (error) {
+ logger.error("Error initializing server:", error);
+ process.exit(1);
+ }
+}
+
+// 下記の2行を追記
+runServer().catch(error => {
+ logger.error("Fatal error in main():", error);
process.exit(1);
});
diff --git a/package-lock.json b/package-lock.json
index f743abd..34b24ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,323 +1,3209 @@
{
- "name": "@modelcontextprotocol/server-gitlab",
- "version": "0.6.2",
+ "name": "@zereight/mcp-gitlab",
+ "version": "1.0.76",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "@modelcontextprotocol/server-gitlab",
- "version": "0.6.2",
+ "name": "@zereight/mcp-gitlab",
+ "version": "1.0.76",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "1.0.1",
+ "@modelcontextprotocol/sdk": "^1.10.0",
"@types/node-fetch": "^2.6.12",
- "dotenv": "^16.4.7",
+ "express": "^5.1.0",
+ "fetch-cookie": "^3.1.0",
+ "form-data": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
"node-fetch": "^3.3.2",
+ "pino": "^9.7.0",
+ "pino-pretty": "^13.0.0",
+ "socks-proxy-agent": "^8.0.5",
+ "tough-cookie": "^5.1.2",
"zod-to-json-schema": "^3.23.5"
},
"bin": {
- "mcp-server-gitlab": "build/index.js"
+ "mcp-gitlab": "build/index.js"
},
"devDependencies": {
- "shx": "^0.3.4",
- "typescript": "^5.6.2"
+ "@types/express": "^5.0.2",
+ "@types/node": "^22.13.10",
+ "@typescript-eslint/eslint-plugin": "^8.21.0",
+ "@typescript-eslint/parser": "^8.21.0",
+ "auto-changelog": "^2.4.0",
+ "eslint": "^9.18.0",
+ "prettier": "^3.4.2",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.8.2",
+ "zod": "^3.24.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
+ "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
+ "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.27.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
+ "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
+ "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.14.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.3.tgz",
+ "integrity": "sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.6",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+ "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz",
+ "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
+ "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.13.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
+ "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.7",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
+ "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.33.0",
+ "@typescript-eslint/type-utils": "8.33.0",
+ "@typescript-eslint/utils": "8.33.0",
+ "@typescript-eslint/visitor-keys": "8.33.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.33.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
+ "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.33.0",
+ "@typescript-eslint/types": "8.33.0",
+ "@typescript-eslint/typescript-estree": "8.33.0",
+ "@typescript-eslint/visitor-keys": "8.33.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
+ "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.33.0",
+ "@typescript-eslint/types": "^8.33.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
+ "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.33.0",
+ "@typescript-eslint/visitor-keys": "8.33.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
+ "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
+ "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.33.0",
+ "@typescript-eslint/utils": "8.33.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
+ "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
+ "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.33.0",
+ "@typescript-eslint/tsconfig-utils": "8.33.0",
+ "@typescript-eslint/types": "8.33.0",
+ "@typescript-eslint/visitor-keys": "8.33.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
+ "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.33.0",
+ "@typescript-eslint/types": "8.33.0",
+ "@typescript-eslint/typescript-estree": "8.33.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
+ "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.33.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/atomic-sleep": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/auto-changelog": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.5.0.tgz",
+ "integrity": "sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^7.2.0",
+ "handlebars": "^4.7.7",
+ "import-cwd": "^3.0.0",
+ "node-fetch": "^2.6.1",
+ "parse-github-url": "^1.0.3",
+ "semver": "^7.3.5"
+ },
+ "bin": {
+ "auto-changelog": "src/index.js"
+ },
+ "engines": {
+ "node": ">=8.3"
+ }
+ },
+ "node_modules/auto-changelog/node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/dateformat": {
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
+ "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.27.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
+ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.20.0",
+ "@eslint/config-helpers": "^0.2.1",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.27.0",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.3.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
+ "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz",
+ "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+ "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": "^4.11 || 5 || ^5.0.0-beta.1"
+ }
+ },
+ "node_modules/fast-copy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
+ "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-redact": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
+ "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/fetch-cookie": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.1.0.tgz",
+ "integrity": "sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==",
+ "license": "Unlicense",
+ "dependencies": {
+ "set-cookie-parser": "^2.4.8",
+ "tough-cookie": "^5.0.0"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/help-me": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
+ "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
+ "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
+ "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "import-from": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-from": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
+ "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-from/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "license": "MIT",
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/joycon": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+ "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "license": "MIT"
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-exit-leak-free": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-github-url": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz",
+ "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "parse-github-url": "cli.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/@modelcontextprotocol/sdk": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz",
- "integrity": "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==",
+ "node_modules/pino": {
+ "version": "9.7.0",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz",
+ "integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==",
"license": "MIT",
"dependencies": {
- "content-type": "^1.0.5",
- "raw-body": "^3.0.0",
- "zod": "^3.23.8"
+ "atomic-sleep": "^1.0.0",
+ "fast-redact": "^3.1.1",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^2.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^3.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
}
},
- "node_modules/@types/node": {
- "version": "22.13.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
- "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
+ "node_modules/pino-abstract-transport": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
+ "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
- "undici-types": "~6.20.0"
+ "split2": "^4.0.0"
}
},
- "node_modules/@types/node-fetch": {
- "version": "2.6.12",
- "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
- "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
+ "node_modules/pino-pretty": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
+ "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"license": "MIT",
"dependencies": {
- "@types/node": "*",
- "form-data": "^4.0.0"
+ "colorette": "^2.0.7",
+ "dateformat": "^4.6.3",
+ "fast-copy": "^3.0.2",
+ "fast-safe-stringify": "^2.1.1",
+ "help-me": "^5.0.0",
+ "joycon": "^3.1.1",
+ "minimist": "^1.2.6",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^2.0.0",
+ "pump": "^3.0.0",
+ "secure-json-parse": "^2.4.0",
+ "sonic-boom": "^4.0.1",
+ "strip-json-comments": "^3.1.1"
+ },
+ "bin": {
+ "pino-pretty": "bin.js"
}
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "node_modules/pino-std-serializers": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
+ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "node_modules/pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
},
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "node_modules/prettier": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
}
},
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "node_modules/process-warning": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
+ "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.10"
}
},
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
- "delayed-stream": "~1.0.0"
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
},
"engines": {
- "node": ">= 0.8"
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
"license": "MIT"
},
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "node_modules/quick-format-unescaped": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+ "license": "MIT"
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
- "node_modules/data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
"engines": {
- "node": ">= 12"
+ "node": ">= 0.8"
}
},
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "node_modules/real-require": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
- "node": ">=0.4.0"
+ "node": ">= 12.13.0"
}
},
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">= 0.8"
+ "node": ">=4"
}
},
- "node_modules/dotenv": {
- "version": "16.4.7",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
- "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
- "license": "BSD-2-Clause",
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=12"
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
},
- "funding": {
- "url": "https://dotenvx.com"
+ "engines": {
+ "node": ">= 18"
}
},
- "node_modules/fetch-blob": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
"funding": [
{
"type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
+ "url": "https://github.com/sponsors/feross"
},
{
- "type": "paypal",
- "url": "https://paypal.me/jimmywarting"
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/secure-json-parse": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
+ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
},
"engines": {
- "node": "^12.20 || >= 14.13"
+ "node": ">=10"
}
},
- "node_modules/form-data": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
- "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT",
"dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
},
"engines": {
- "node": ">= 6"
+ "node": ">= 18"
}
},
- "node_modules/formdata-polyfill": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
- "fetch-blob": "^3.1.2"
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
},
"engines": {
- "node": ">=12.20.0"
+ "node": ">= 18"
}
},
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true,
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
- "dev": true,
- "license": "ISC",
"dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "shebang-regex": "^3.0.0"
},
"engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">=8"
}
},
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
"engines": {
- "node": ">= 0.4"
+ "node": ">=8"
}
},
- "node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
},
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
},
"engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/interpret": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
- "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
- "dev": true,
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
"engines": {
- "node": ">= 0.10"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "dev": true,
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
- "hasown": "^2.0.2"
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -326,244 +3212,268 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
}
},
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "node_modules/socks": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
+ "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
"license": "MIT",
"dependencies": {
- "mime-db": "1.52.0"
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
}
},
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "license": "MIT",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
},
"engines": {
- "node": "*"
+ "node": ">= 14"
}
},
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
+ "node_modules/sonic-boom": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
+ "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
}
},
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
- "node": ">=10.5.0"
+ "node": ">= 0.8"
}
},
- "node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"license": "MIT",
- "dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- },
"engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ "node": ">=8"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
- "wrappy": "1"
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
+ "node_modules/thread-stream": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
+ "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
- "engines": {
- "node": ">=0.10.0"
+ "dependencies": {
+ "real-require": "^0.2.0"
}
},
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/raw-body": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
- "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.6.3",
- "unpipe": "1.0.0"
+ "tldts-core": "^6.1.86"
},
- "engines": {
- "node": ">= 0.8"
+ "bin": {
+ "tldts": "bin/cli.js"
}
},
- "node_modules/rechoir": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
- "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "resolve": "^1.1.6"
+ "is-number": "^7.0.0"
},
"engines": {
- "node": ">= 0.10"
+ "node": ">=8.0"
}
},
- "node_modules/resolve": {
- "version": "1.22.10",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
- "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
- "dev": true,
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "license": "BSD-3-Clause",
"dependencies": {
- "is-core-module": "^2.16.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
+ "tldts": "^6.1.32"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": ">=16"
}
},
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/shelljs": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
- "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "glob": "^7.0.0",
- "interpret": "^1.0.0",
- "rechoir": "^0.6.2"
- },
- "bin": {
- "shjs": "bin/shjs"
- },
+ "license": "MIT",
"engines": {
- "node": ">=4"
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
}
},
- "node_modules/shx": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
- "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "minimist": "^1.2.3",
- "shelljs": "^0.8.5"
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
},
"bin": {
- "shx": "lib/cli.js"
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
},
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
}
},
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "engines": {
+ "node": ">= 0.8.0"
}
},
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
"engines": {
- "node": ">=0.6"
+ "node": ">= 0.6"
}
},
"node_modules/typescript": {
- "version": "5.7.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "version": "5.8.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
+ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -574,6 +3484,20 @@
"node": ">=14.17"
}
},
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
@@ -589,6 +3513,31 @@
"node": ">= 0.8"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
@@ -598,26 +3547,98 @@
"node": ">= 8"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
"license": "ISC"
},
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/zod": {
- "version": "3.24.1",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
- "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "version": "3.24.2",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
+ "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
- "version": "3.24.1",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
- "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
+ "version": "3.24.5",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
+ "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
diff --git a/package.json b/package.json
index dacb51f..d3b7a52 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@zereight/mcp-gitlab",
- "version": "1.0.2",
+ "version": "2.0.3",
"description": "MCP server for using the GitLab API",
"license": "MIT",
"author": "zereight",
@@ -18,15 +18,43 @@
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
- "watch": "tsc --watch"
+ "dev": "npm run build && node build/index.js",
+ "watch": "tsc --watch",
+ "deploy": "npm publish --access public",
+ "changelog": "auto-changelog -p",
+ "test": "node test/validate-api.js",
+ "test:integration": "node test/validate-api.js",
+ "test:server": "npm run build && node build/test/test-all-transport-server.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.0.1",
+ "@modelcontextprotocol/sdk": "^1.10.0",
"@types/node-fetch": "^2.6.12",
+ "express": "^5.1.0",
+ "fetch-cookie": "^3.1.0",
+ "form-data": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
"node-fetch": "^3.3.2",
+ "pino": "^9.7.0",
+ "pino-pretty": "^13.0.0",
+ "socks-proxy-agent": "^8.0.5",
+ "tough-cookie": "^5.1.2",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
- "typescript": "^5.6.2"
+ "@types/express": "^5.0.2",
+ "@types/node": "^22.13.10",
+ "@typescript-eslint/eslint-plugin": "^8.21.0",
+ "@typescript-eslint/parser": "^8.21.0",
+ "auto-changelog": "^2.4.0",
+ "eslint": "^9.18.0",
+ "prettier": "^3.4.2",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.8.2",
+ "zod": "^3.24.2"
}
}
diff --git a/schemas.ts b/schemas.ts
index b014291..370f2f7 100644
--- a/schemas.ts
+++ b/schemas.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { flexibleBoolean, flexibleBooleanNullable } from "./customSchemas.js";
// Base schemas for common types
export const GitLabAuthorSchema = z.object({
@@ -7,32 +8,440 @@ export const GitLabAuthorSchema = z.object({
date: z.string(),
});
+// Pipeline related schemas
+export const GitLabPipelineSchema = z.object({
+ id: z.coerce.string(),
+ project_id: z.coerce.string(),
+ 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.coerce.string(),
+ 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: flexibleBoolean.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.coerce.string(),
+ status: z.string(),
+ stage: z.string(),
+ name: z.string(),
+ ref: z.string(),
+ tag: flexibleBoolean,
+ 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.coerce.string(),
+ 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.coerce.string(),
+ project_id: z.coerce.string(),
+ status: z.string(),
+ ref: z.string(),
+ sha: z.string(),
+ })
+ .optional(),
+ web_url: z.string().optional(),
+});
+
+// Pipeline trigger job (bridge) schema
+export const GitLabPipelineTriggerJobSchema = z.object({
+ id: z.coerce.string(),
+ status: z.string(),
+ stage: z.string(),
+ name: z.string(),
+ ref: z.string(),
+ tag: flexibleBoolean,
+ 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(),
+ queued_duration: z.number().nullable().optional(),
+ user: z
+ .object({
+ id: z.coerce.string(),
+ 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.coerce.string(),
+ project_id: z.coerce.string(),
+ status: z.string(),
+ ref: z.string(),
+ sha: z.string(),
+ created_at: z.string().optional(),
+ updated_at: z.string().optional(),
+ web_url: z.string().optional(),
+ })
+ .optional(),
+ web_url: z.string().optional(),
+ allow_failure: flexibleBoolean.optional(),
+ archived: flexibleBoolean.optional(),
+ source: z.string().optional(),
+ erased_at: z.string().nullable().optional(),
+ project: z
+ .object({
+ ci_job_token_scope_enabled: flexibleBoolean.optional(),
+ })
+ .optional(),
+ downstream_pipeline: z
+ .object({
+ id: z.coerce.string(),
+ sha: z.string(),
+ ref: z.string(),
+ status: z.string(),
+ created_at: z.string(),
+ updated_at: z.string(),
+ web_url: z.string(),
+ })
+ .nullable()
+ .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.coerce.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: flexibleBoolean
+ .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.coerce.string().describe("Project ID or URL-encoded path"),
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline"),
+});
+
+// Schema for listing jobs in a pipeline
+export const ListPipelineJobsSchema = z
+ .object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ pipeline_id: z.coerce.string().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: flexibleBoolean.optional().describe("Whether to include retried jobs"),
+ })
+ .merge(PaginationOptionsSchema);
+
+// Schema for listing trigger jobs (bridges) in a pipeline
+export const ListPipelineTriggerJobsSchema = z
+ .object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline"),
+ scope: z
+ // https://docs.gitlab.com/api/jobs/#job-status-values
+ .enum([
+ "canceled",
+ "canceling",
+ "created",
+ "failed",
+ "manual",
+ "pending",
+ "preparing",
+ "running",
+ "scheduled",
+ "skipped",
+ "success",
+ "waiting_for_resource",
+ ])
+ .optional()
+ .describe("The scope of trigger jobs to show"),
+ })
+ .merge(PaginationOptionsSchema);
+
+// Schema for creating a new pipeline
+export const CreatePipelineSchema = z.object({
+ project_id: z.coerce.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.coerce.string().describe("Project ID or URL-encoded path"),
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline to retry"),
+});
+
+// Schema for canceling a pipeline
+export const CancelPipelineSchema = RetryPipelineSchema;
+
+// Schema for the input parameters for pipeline job operations
+export const GetPipelineJobOutputSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ job_id: z.coerce.string().describe("The ID of the job"),
+ limit: z
+ .number()
+ .optional()
+ .describe("Maximum number of lines to return from the end of the log (default: 1000)"),
+ offset: z
+ .number()
+ .optional()
+ .describe("Number of lines to skip from the end of the log (default: 0)"),
+});
+
+// User schemas
+export const GitLabUserSchema = z.object({
+ username: z.string().optional(), // Changed from login to match GitLab API
+ id: z.coerce.string(),
+ name: z.string().optional(),
+ avatar_url: z.string().nullable().optional(),
+ web_url: z.string().optional(), // 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.coerce.string(),
+ 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.coerce.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.coerce.string(),
+ name: z.string(),
+ path: z.string(),
+ kind: z.enum(["user", "group"]),
+ full_path: z.string(),
+ parent_id: z.coerce.string().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: flexibleBoolean.optional(),
+ root_repository_size: z.number().optional(),
+ projects_count: z.number().optional(),
+});
+
+export const GitLabNamespaceExistsResponseSchema = z.object({
+ exists: flexibleBoolean,
+ 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(),
+ id: z.coerce.string(),
+ 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(),
+ id: z.coerce.string(),
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(),
+ fork: flexibleBoolean.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.coerce.string(),
+ 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: flexibleBoolean.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().nullable().optional(),
+ })
+ .optional()
+ .nullable(),
+ group_access: z
+ .object({
+ access_level: z.number(),
+ notification_level: z.number().nullable().optional(),
+ })
+ .optional()
+ .nullable(),
+ })
+ .optional(),
+ container_registry_enabled: flexibleBoolean.optional(),
+ container_registry_access_level: z.string().optional(),
+ issues_enabled: flexibleBoolean.optional(),
+ merge_requests_enabled: flexibleBoolean.optional(),
+ merge_requests_template: z.string().nullable().optional(),
+ wiki_enabled: flexibleBoolean.optional(),
+ jobs_enabled: flexibleBoolean.optional(),
+ snippets_enabled: flexibleBoolean.optional(),
+ can_create_merge_request_in: flexibleBoolean.optional(),
+ resolve_outdated_diff_discussions: flexibleBooleanNullable.optional(),
+ shared_runners_enabled: flexibleBoolean.optional(),
+ shared_with_groups: z
+ .array(
+ z.object({
+ group_id: z.coerce.string(),
+ 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
@@ -43,7 +452,9 @@ export const GitLabFileContentSchema = z.object({
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: flexibleBoolean.optional(), // Added to match GitLab API
});
export const GitLabDirectoryContentSchema = z.object({
@@ -67,17 +478,30 @@ export const FileOperationSchema = z.object({
});
// Tree and commit schemas
-export const GitLabTreeEntrySchema = z.object({
- id: z.string(), // Changed from sha to match GitLab API
+export const GitLabTreeItemSchema = z.object({
+ id: z.string(),
name: z.string(),
- type: z.enum(["blob", "tree"]),
+ type: z.enum(["tree", "blob"]),
path: z.string(),
mode: z.string(),
});
+export const GetRepositoryTreeSchema = z.object({
+ project_id: z.coerce.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: flexibleBoolean.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(GitLabTreeEntrySchema),
+ tree: z.array(GitLabTreeItemSchema),
});
export const GitLabCommitSchema = z.object({
@@ -90,8 +514,19 @@ export const GitLabCommitSchema = z.object({
committer_name: z.string(),
committer_email: z.string(),
committed_date: z.string(),
+ created_at: z.string().optional(), // Add created_at field
+ message: z.string().optional(), // Add full message field
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
+ stats: z
+ .object({
+ additions: z.number().optional().nullable(),
+ deletions: z.number().optional().nullable(),
+ total: z.number().optional().nullable(),
+ })
+ .optional(), // Only present when with_stats=true
+ trailers: z.record(z.string()).optional().default({}), // Git trailers, may be empty object
+ extended_trailers: z.record(z.array(z.string())).optional().default({}), // Extended trailers, may be empty object
});
// Reference schema
@@ -103,42 +538,54 @@ export const GitLabReferenceSchema = z.object({
}),
});
+// Milestones rest api output schemas
+export const GitLabMilestonesSchema = z.object({
+ id: z.coerce.string(),
+ iid: z.coerce.string(),
+ project_id: z.coerce.string(),
+ 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: flexibleBoolean,
+ 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
+ initialize_with_readme: flexibleBoolean.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
+ milestone_id: z.coerce.string().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
- allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
- draft: z.boolean().optional(),
-});
-
-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 GitLabDiffSchema = z.object({
+ old_path: z.string(),
+ new_path: z.string(),
+ a_mode: z.string(),
+ b_mode: z.string(),
+ diff: z.string(),
+ new_file: flexibleBoolean,
+ renamed_file: flexibleBoolean,
+ deleted_file: flexibleBoolean,
});
// Response schemas for operations
export const GitLabCreateUpdateFileResponseSchema = z.object({
file_path: z.string(),
branch: z.string(),
- commit_id: z.string(), // Changed from sha to match GitLab API
+ commit_id: z.string().optional(), // Optional since it's not always returned by the API
content: GitLabFileContentSchema.optional(),
});
@@ -149,62 +596,115 @@ export const GitLabSearchResponseSchema = z.object({
items: z.array(GitLabRepositorySchema),
});
-// 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(),
- }),
- web_url: z.string(), // Changed from html_url to match GitLab API
+// 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 GitLabForkSchema = GitLabRepositorySchema.extend({
- forked_from_project: GitLabForkParentSchema, // Changed from parent to match GitLab API
+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: flexibleBoolean.optional(),
+ compare_same_ref: flexibleBoolean.optional(),
});
// Issue related schemas
export const GitLabLabelSchema = z.object({
- id: z.number(),
+ id: z.coerce.string(),
name: z.string(),
color: z.string(),
- description: z.string().optional(),
-});
-
-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(),
- web_url: z.string(), // Changed from html_url to match GitLab API
+ 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: flexibleBoolean.optional(),
+ priority: z.number().nullable().optional(),
+ is_project_label: flexibleBoolean.optional(),
});
export const GitLabMilestoneSchema = z.object({
- id: z.number(),
- iid: z.number(), // Added to match GitLab API
+ id: z.coerce.string(),
+ iid: z.coerce.string(), // Added to match GitLab API
title: z.string(),
- description: 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
+ id: z.coerce.string(),
+ iid: z.coerce.string(), // Added to match GitLab API
+ project_id: z.coerce.string(), // Added to match GitLab API
title: z.string(),
- description: z.string(), // Changed from body to match GitLab API
+ 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),
+ 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: flexibleBoolean.optional(),
+ due_date: z.string().nullable().optional(),
+ discussion_locked: flexibleBooleanNullable.optional(),
+ weight: z.number().nullable().optional(),
+ issue_type: z.string().describe("the type of issue.").nullish(),
+});
+
+// NEW SCHEMA: For issue with link details (used in listing issue links)
+export const GitLabIssueWithLinkDetailsSchema = GitLabIssueSchema.extend({
+ issue_link_id: z.coerce.string(),
+ 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.coerce.string(),
+ 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)
@@ -215,19 +715,20 @@ export const GitLabMergeRequestDiffRefSchema = z.object({
});
export const GitLabMergeRequestSchema = z.object({
- id: z.number(),
- iid: z.number(),
- project_id: z.number(),
+ id: z.coerce.string(),
+ iid: z.coerce.string(),
+ project_id: z.coerce.string(),
title: z.string(),
description: z.string().nullable(),
state: z.string(),
- merged: z.boolean().optional(),
- draft: z.boolean().optional(),
+ merged: flexibleBoolean.optional(),
+ draft: flexibleBoolean.optional(),
author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema).optional(),
+ reviewers: z.array(GitLabUserSchema).optional(),
source_branch: z.string(),
target_branch: z.string(),
- diff_refs: GitLabMergeRequestDiffRefSchema.optional(),
+ diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(),
web_url: z.string(),
created_at: z.string(),
updated_at: z.string(),
@@ -237,45 +738,247 @@ export const GitLabMergeRequestSchema = z.object({
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().optional(),
- allow_collaboration: z.boolean().optional(),
- allow_maintainer_to_push: z.boolean().optional(),
- changes_count: z.string().optional(),
- merge_when_pipeline_succeeds: z.boolean().optional(),
- squash: z.boolean().optional(),
+ work_in_progress: flexibleBoolean.optional(),
+ blocking_discussions_resolved: flexibleBoolean.optional(),
+ should_remove_source_branch: flexibleBooleanNullable.optional(),
+ force_remove_source_branch: flexibleBooleanNullable.optional(),
+ allow_collaboration: flexibleBoolean.optional(),
+ allow_maintainer_to_push: flexibleBoolean.optional(),
+ changes_count: z.string().nullable().optional(),
+ merge_when_pipeline_succeeds: flexibleBoolean.optional(),
+ squash: flexibleBoolean.optional(),
labels: z.array(z.string()).optional(),
});
-// API Operation Parameter Schemas
-const ProjectParamsSchema = z.object({
- project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API
+export const LineRangeSchema = z
+ .object({
+ start: z
+ .object({
+ line_code: z
+ .string()
+ .nullable()
+ .optional()
+ .describe(
+ "CRITICAL: Line identifier in format '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. USUALLY REQUIRED for GitLab diff comments despite being optional in schema. Example: 'a1b2c3d4e5f6_10_15'. Get this from GitLab diff API response, never fabricate."
+ ),
+ type: z
+ .enum(["new", "old", "expanded"])
+ .nullable()
+ .optional()
+ .describe(
+ "Line type: 'old' = deleted/original line, 'new' = added/modified line, null = unchanged context. MUST match the line_code format and old_line/new_line values."
+ ),
+ old_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in original file (before changes). REQUIRED when type='old', NULL when type='new' (for purely added lines), can be present for context lines."
+ ),
+ new_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in modified file (after changes). REQUIRED when type='new', NULL when type='old' (for purely deleted lines), can be present for context lines."
+ ),
+ })
+ .describe(
+ "Start line position for multiline comment range. MUST specify either old_line OR new_line (or both for context), never neither."
+ ),
+ end: z
+ .object({
+ line_code: z
+ .string()
+ .nullable()
+ .optional()
+ .describe(
+ "CRITICAL: Line identifier in format '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. USUALLY REQUIRED for GitLab diff comments despite being optional in schema. Example: 'a1b2c3d4e5f6_12_17'. Must be from same file as start.line_code."
+ ),
+ type: z
+ .enum(["new", "old", "expanded"])
+ .nullable()
+ .optional()
+ .describe(
+ "Line type: 'old' = deleted/original line, 'new' = added/modified line, null = unchanged context. SHOULD MATCH start.type for consistent ranges (don't mix old/new types)."
+ ),
+ old_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in original file (before changes). REQUIRED when type='old', NULL when type='new' (for purely added lines), can be present for context lines. MUST be >= start.old_line if both specified."
+ ),
+ new_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in modified file (after changes). REQUIRED when type='new', NULL when type='old' (for purely deleted lines), can be present for context lines. MUST be >= start.new_line if both specified."
+ ),
+ })
+ .describe(
+ "End line position for multiline comment range. MUST specify either old_line OR new_line (or both for context), never neither. Range must be valid (end >= start)."
+ ),
+ })
+ .describe(
+ "Line range for multiline comments on GitLab merge request diffs. VALIDATION RULES: 1) line_code is critical for GitLab API success, 2) start/end must have consistent types, 3) line numbers must form valid range, 4) get line_code from GitLab diff API, never generate manually."
+ );
+
+// Discussion related schemas
+export const GitLabDiscussionNoteSchema = z.object({
+ id: z.coerce.string(),
+ type: z.enum(["DiscussionNote", "DiffNote", "Note"]).nullable().optional(), // Allow null type for regular notes
+ body: z.string().optional(),
+ attachment: z.any().nullable().optional(), // Can be string or object, handle appropriately
+ author: GitLabUserSchema.optional(),
+ created_at: z.string().optional(),
+ updated_at: z.string().optional(),
+ system: flexibleBoolean.optional(),
+ noteable_id: z.coerce.string().optional(),
+ noteable_type: z.enum(["Issue", "MergeRequest", "Snippet", "Commit", "Epic"]).optional(),
+ project_id: z.coerce.string().optional(),
+ noteable_iid: z.coerce.string().nullable().optional(),
+ resolvable: flexibleBoolean.optional(),
+ resolved: flexibleBoolean.optional(),
+ resolved_by: GitLabUserSchema.nullable().optional(),
+ resolved_at: z.string().nullable().optional(),
+ position: z
+ .object({
+ // Only present for DiffNote
+ base_sha: z.string().optional(),
+ start_sha: z.string().optional(),
+ head_sha: z.string().optional(),
+ old_path: z.string().nullable().optional().describe("File path before change"),
+ new_path: z.string().nullable().optional().describe("File path after change"),
+ position_type: z.enum(["text", "image", "file"]).optional(),
+ new_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in the modified file (after changes). Used for added lines and context lines. Null for deleted lines."
+ ),
+ old_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in the original file (before changes). Used for deleted lines and context lines. Null for newly added lines."
+ ),
+ line_range: LineRangeSchema.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
+ })
+ .passthrough() // Allow additional fields
+ .optional(),
+}).passthrough(); // Allow additional fields that GitLab might return
+export type GitLabDiscussionNote = z.infer;
+
+// 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;
+
+// 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.coerce.string(),
+ individual_note: flexibleBoolean,
+ notes: z.array(GitLabDiscussionNoteSchema),
+});
+export type GitLabDiscussion = z.infer;
+
+// 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;
+
+export const ListIssueDiscussionsSchema = z
+ .object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().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.coerce.string().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.coerce.string().describe("The IID of a merge request"),
+ discussion_id: z.coerce.string().describe("The ID of a thread"),
+ note_id: z.coerce.string().describe("The ID of a thread note"),
+ body: z.string().optional().describe("The content of the note or reply"),
+ resolved: flexibleBoolean.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.coerce.string().describe("The IID of a merge request"),
+ discussion_id: z.coerce.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.coerce.string().describe("The IID of an issue"),
+ discussion_id: z.coerce.string().describe("The ID of a thread"),
+ note_id: z.coerce.string().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.coerce.string().describe("The IID of an issue"),
+ discussion_id: z.coerce.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"),
+ 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
- page: z
- .number()
- .optional()
- .describe("Page number for pagination (default: 1)"),
- per_page: z
- .number()
- .optional()
- .describe("Number of results per page (default: 20)"),
-});
+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"),
@@ -284,10 +987,7 @@ export const CreateRepositorySchema = z.object({
.enum(["private", "internal", "public"])
.optional()
.describe("Repository visibility level"),
- initialize_with_readme: z
- .boolean()
- .optional()
- .describe("Initialize with README.md"),
+ initialize_with_readme: flexibleBoolean.optional().describe("Initialize with README.md"),
});
export const GetFileContentsSchema = ProjectParamsSchema.extend({
@@ -311,63 +1011,74 @@ export const PushFilesSchema = ProjectParamsSchema.extend({
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"),
+ 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"),
+ milestone_id: z.coerce.string().optional().describe("Milestone ID to assign"),
+ issue_type: z.enum(["issue", "incident", "test_case", "task"]).describe("the type of issue. One of issue, incident, test_case or task.").nullish().default("issue"),
});
-export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
+const MergeRequestOptionsSchema = {
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"),
- draft: z.boolean().optional().describe("Create as draft merge request"),
- allow_collaboration: z
- .boolean()
+ target_project_id: z.coerce.string().optional().describe("Numeric ID of the target project."),
+ 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("Allow commits from upstream members"),
-});
+ .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: flexibleBoolean.optional().describe("Create as draft merge request"),
+ allow_collaboration: z.boolean().optional().describe("Allow commits from upstream members"),
+ remove_source_branch: flexibleBooleanNullable
+ .optional()
+ .describe("Flag indicating if a merge request should remove the source branch when merging."),
+ squash: flexibleBooleanNullable
+ .optional()
+ .describe("If true, squash all commits into a single commit on merge."),
+};
+export const CreateMergeRequestOptionsSchema = z.object(MergeRequestOptionsSchema);
+export const CreateMergeRequestSchema = ProjectParamsSchema.extend(MergeRequestOptionsSchema);
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 GitLabMergeRequestDiffSchema = 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(),
+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: flexibleBoolean
+ .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()
- .describe("The internal ID of the merge request"),
+ merge_request_iid: z.coerce.string().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"),
+ description: z.string().optional().describe("The description of the merge request"),
target_branch: z.string().optional().describe("The target branch"),
- assignee_ids: z
+ 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 the MR to"),
+ .describe("The ID of the users to assign as reviewers of the MR"),
labels: z.array(z.string()).optional().describe("Labels for the MR"),
state_event: z
.enum(["close", "reopen"])
@@ -377,44 +1088,902 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
.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"),
+ squash: flexibleBoolean.optional().describe("Squash commits into a single commit when merging"),
+ draft: flexibleBoolean.optional().describe("Work in progress merge request"),
+});
+
+export const MergeMergeRequestSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
+ auto_merge: flexibleBoolean.optional().default(false).describe("If true, the merge request merges when the pipeline succeeds."),
+ merge_commit_message: z.string().optional().describe("Custom merge commit message"),
+ merge_when_pipeline_succeeds: flexibleBoolean.optional().default(false).describe("If true, the merge request merges when the pipeline succeeds.in GitLab 17.11. Use"),
+ should_remove_source_branch: flexibleBoolean.optional().default(false).describe("Remove source branch after merge"),
+ squash_commit_message: z.string().optional().describe("Custom squash commit message"),
+ squash: flexibleBoolean.optional().default(false).describe("Squash commits into a single commit when merging"),
});
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
});
+export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
+ 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)"),
+ unidiff: flexibleBoolean
+ .optional()
+ .describe(
+ "Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."
+ ),
+});
+
+export const CreateNoteSchema = z.object({
+ project_id: z.coerce.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.coerce.string().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.coerce
+ .string()
+ .optional()
+ .describe(
+ "Project ID or URL-encoded path (optional - if not provided, lists issues across all accessible projects)"
+ ),
+ assignee_id: z.coerce
+ .string()
+ .optional()
+ .describe("Return issues assigned to the given user ID. user id or none or any"),
+ assignee_username: z
+ .array(z.string())
+ .optional()
+ .describe("Return issues assigned to the given username"),
+ author_id: z.coerce.string().optional().describe("Return issues created by the given user ID"),
+ author_username: z.string().optional().describe("Return issues created by the given username"),
+ confidential: flexibleBoolean.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"),
+ issue_type: z
+ .string()
+ .optional()
+ .nullable()
+ .describe("Filter to a given type of issue. One of issue, incident, test_case or task"),
+ iteration_id: z.coerce
+ .string()
+ .optional()
+ .nullable()
+ .describe(
+ "Return issues assigned to the given iteration ID. None returns issues that do not belong to an iteration. Any returns issues that belong to an iteration. "
+ ),
+ 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: flexibleBoolean.optional().describe("Return more details for each label"),
+ })
+ .merge(PaginationOptionsSchema);
+
+// Merge Requests API operation schemas
+export const ListMergeRequestsSchema = z
+ .object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ assignee_id: z.coerce
+ .string()
+ .optional()
+ .describe("Return issues assigned to the given user ID. user id or none or any"),
+ assignee_username: z
+ .string()
+ .optional()
+ .describe("Returns merge requests assigned to the given username"),
+ author_id: z.coerce
+ .string()
+ .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.coerce
+ .string()
+ .optional()
+ .describe("Returns merge requests which have the user as a reviewer. user id or none or any"),
+ 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: flexibleBoolean.optional().describe("Return more details for each label"),
+ })
+ .merge(PaginationOptionsSchema);
+
+export const GetIssueSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
+});
+
+export const UpdateIssueSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().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: flexibleBoolean.optional().describe("Set the issue to be confidential"),
+ discussion_locked: flexibleBoolean.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.coerce.string().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)"),
+ issue_type: z.enum(["issue", "incident", "test_case", "task"]).describe("the type of issue. One of issue, incident, test_case or task."),
+
+});
+
+export const DeleteIssueSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().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.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
+});
+
+export const GetIssueLinkSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
+ issue_link_id: z.coerce.string().describe("ID of an issue relationship"),
+});
+
+export const CreateIssueLinkSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
+ target_project_id: z.coerce.string().describe("The ID or URL-encoded path of a target project"),
+ target_issue_iid: z.coerce.string().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.coerce.string().describe("Project ID or URL-encoded path"),
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
+ issue_link_id: z.coerce.string().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: flexibleBoolean.optional().describe("Filter for namespaces owned by current user"),
+ })
+ .merge(PaginationOptionsSchema);
+
+export const GetNamespaceSchema = z.object({
+ namespace_id: z.coerce.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.coerce.string().describe("Project ID or URL-encoded path"),
+});
+
+export const ListProjectsSchema = z
+ .object({
+ search: z.string().optional().describe("Search term for projects"),
+ search_namespaces: flexibleBoolean
+ .optional()
+ .describe("Needs to be true if search is full path"),
+ owned: flexibleBoolean.optional().describe("Filter for projects owned by current user"),
+ membership: flexibleBoolean
+ .optional()
+ .describe("Filter for projects where current user is a member"),
+ simple: flexibleBoolean.optional().describe("Return only limited fields"),
+ archived: flexibleBoolean.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.coerce.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: flexibleBoolean.optional().describe("Include ancestor groups"),
+ search: z.string().optional().describe("Keyword to filter labels by"),
+});
+
+export const GetLabelSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
+ include_ancestor_groups: flexibleBoolean.optional().describe("Include ancestor groups"),
+});
+
+export const CreateLabelSchema = z.object({
+ project_id: z.coerce.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.coerce.string().describe("Project ID or URL-encoded path"),
+ label_id: z.coerce.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.coerce.string().describe("Project ID or URL-encoded path"),
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
+});
+
+// Group projects schema
+export const ListGroupProjectsSchema = z
+ .object({
+ group_id: z.coerce.string().describe("Group ID or path"),
+ include_subgroups: flexibleBoolean.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: flexibleBoolean.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: flexibleBoolean.optional().describe("Filter by starred projects"),
+ statistics: flexibleBoolean.optional().describe("Include project statistics"),
+ with_custom_attributes: flexibleBoolean.optional().describe("Include custom attributes"),
+ with_security_reports: flexibleBoolean.optional().describe("Include security reports"),
+ })
+ .merge(PaginationOptionsSchema);
+
+// Add wiki operation schemas
+export const ListWikiPagesSchema = z
+ .object({
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
+ with_content: flexibleBoolean.optional().describe("Include content of the wiki pages"),
+ })
+ .merge(PaginationOptionsSchema);
+
+export const GetWikiPageSchema = z.object({
+ project_id: z.coerce.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.coerce.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.coerce.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.coerce.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
+// Extremely flexible position schema for API responses - accepts any structure
+
+// Strict position schema for creating draft notes and merge request threads
+export const MergeRequestThreadPositionCreateSchema = z.object({
+ base_sha: z.string().describe("REQUIRED: Base commit SHA in the source branch. Get this from merge request diff_refs.base_sha."),
+ head_sha: z.string().describe("REQUIRED: SHA referencing HEAD of the source branch. Get this from merge request diff_refs.head_sha."),
+ start_sha: z.string().describe("REQUIRED: SHA referencing the start commit of the source branch. Get this from merge request diff_refs.start_sha."),
+ position_type: z.enum(["text", "image", "file"]).describe("REQUIRED: Position type. Use 'text' for code diffs, 'image' for image diffs, 'file' for file-level comments."),
+ new_path: z.string().nullable().optional().describe("File path after changes. REQUIRED for most diff comments. Use same as old_path if file wasn't renamed."),
+ old_path: z.string().nullable().optional().describe("File path before changes. REQUIRED for most diff comments. Use same as new_path if file wasn't renamed."),
+ new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). Use for added lines or context lines. NULL for deleted lines. For single-line comments on new lines."),
+ old_line: z.number().nullable().optional().describe("Line number in original file (before changes). Use for deleted lines or context lines. NULL for added lines. For single-line comments on old lines."),
+ line_range: LineRangeSchema.optional().describe("MULTILINE COMMENTS: Specify start/end line positions for commenting on multiple lines. Alternative to single old_line/new_line."),
+ width: z.number().optional().describe("IMAGE DIFFS ONLY: Width of the image (for position_type='image')."),
+ height: z.number().optional().describe("IMAGE DIFFS ONLY: Height of the image (for position_type='image')."),
+ x: z.number().optional().describe("IMAGE DIFFS ONLY: X coordinate on the image (for position_type='image')."),
+ y: z.number().optional().describe("IMAGE DIFFS ONLY: Y coordinate on the image (for position_type='image')."),
+});
+
+export const MergeRequestThreadPositionSchema = z.object({
+ base_sha: z
+ .string()
+ .describe(
+ "REQUIRED: Base commit SHA in the source branch. Get this from merge request diff_refs.base_sha."
+ ),
+ head_sha: z
+ .string()
+ .describe(
+ "REQUIRED: SHA referencing HEAD of the source branch. Get this from merge request diff_refs.head_sha."
+ ),
+ start_sha: z
+ .string()
+ .describe(
+ "REQUIRED: SHA referencing the start commit of the source branch. Get this from merge request diff_refs.start_sha."
+ ),
+ position_type: z
+ .enum(["text", "image", "file"])
+ .describe(
+ "REQUIRED: Position type. Use 'text' for code diffs, 'image' for image diffs, 'file' for file-level comments."
+ ),
+ new_path: z
+ .string()
+ .nullable()
+ .optional()
+ .describe(
+ "File path after changes. REQUIRED for most diff comments. Use same as old_path if file wasn't renamed."
+ ),
+ old_path: z
+ .string()
+ .nullable()
+ .optional()
+ .describe(
+ "File path before changes. REQUIRED for most diff comments. Use same as new_path if file wasn't renamed."
+ ),
+ new_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in modified file (after changes). Use for added lines or context lines. NULL for deleted lines. For single-line comments on new lines."
+ ),
+ old_line: z
+ .number()
+ .nullable()
+ .optional()
+ .describe(
+ "Line number in original file (before changes). Use for deleted lines or context lines. NULL for added lines. For single-line comments on old lines."
+ ),
+ line_range: LineRangeSchema.optional().describe(
+ "MULTILINE COMMENTS: Specify start/end line positions for commenting on multiple lines. Alternative to single old_line/new_line."
+ ),
+ width: z
+ .number()
+ .optional()
+ .describe("IMAGE DIFFS ONLY: Width of the image (for position_type='image')."),
+ height: z
+ .number()
+ .optional()
+ .describe("IMAGE DIFFS ONLY: Height of the image (for position_type='image')."),
+ x: z
+ .number()
+ .optional()
+ .describe("IMAGE DIFFS ONLY: X coordinate on the image (for position_type='image')."),
+ y: z
+ .number()
+ .optional()
+ .describe("IMAGE DIFFS ONLY: Y coordinate on the image (for position_type='image')."),
+});
+
+// Draft Notes API schemas
+export const GitLabDraftNoteSchema = z.object({
+ id: z.coerce.string(),
+ author: GitLabUserSchema.optional(),
+ body: z.string().optional(),
+ note: z.string().optional(), // Some APIs might use 'note' instead of 'body'
+ created_at: z.string().optional(),
+ updated_at: z.string().optional(),
+ position: MergeRequestThreadPositionSchema.nullable().optional(),
+ resolve_discussion: flexibleBoolean.optional(),
+}).transform((data) => ({
+ // Normalize the response to always have consistent field names
+ id: data.id,
+ author: data.author,
+ body: data.body || data.note || "",
+ created_at: data.created_at || "",
+ updated_at: data.updated_at || "",
+ position: data.position,
+ resolve_discussion: data.resolve_discussion,
+}));
+
+export type GitLabDraftNote = z.infer;
+
+// Get draft note schema
+export const GetDraftNoteSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+ draft_note_id: z.coerce.string().describe("The ID of the draft note"),
+});
+
+// List draft notes schema
+export const ListDraftNotesSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+});
+
+// Create draft note schema
+export const CreateDraftNoteSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+ body: z.string().describe("The content of the draft note"),
+ position: MergeRequestThreadPositionCreateSchema.optional().describe("Position when creating a diff note"),
+ resolve_discussion: flexibleBoolean.optional().describe("Whether to resolve the discussion when publishing"),
+});
+
+// Update draft note schema
+export const UpdateDraftNoteSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+ draft_note_id: z.coerce.string().describe("The ID of the draft note"),
+ body: z.string().optional().describe("The content of the draft note"),
+ position: MergeRequestThreadPositionCreateSchema.optional().describe("Position when creating a diff note"),
+ resolve_discussion: flexibleBoolean.optional().describe("Whether to resolve the discussion when publishing"),
+});
+
+// Delete draft note schema
+export const DeleteDraftNoteSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+ draft_note_id: z.coerce.string().describe("The ID of the draft note"),
+});
+
+// Publish draft note schema
+export const PublishDraftNoteSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+ draft_note_id: z.coerce.string().describe("The ID of the draft note"),
+});
+
+// Bulk publish draft notes schema
+export const BulkPublishDraftNotesSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
+});
+
+// Schema for creating a new merge request thread
+export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
+ merge_request_iid: z.coerce.string().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: flexibleBoolean.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.coerce.string().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);
+
+// Add schemas for commit operations
+export const ListCommitsSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
+ ref_name: z
+ .string()
+ .optional()
+ .describe(
+ "The name of a repository branch, tag or revision range, or if not given the default branch"
+ ),
+ since: z
+ .string()
+ .optional()
+ .describe(
+ "Only commits after or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"
+ ),
+ until: z
+ .string()
+ .optional()
+ .describe(
+ "Only commits before or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"
+ ),
+ path: z.string().optional().describe("The file path"),
+ author: z.string().optional().describe("Search commits by commit author"),
+ all: flexibleBoolean.optional().describe("Retrieve every commit from the repository"),
+ with_stats: flexibleBoolean
+ .optional()
+ .describe("Stats about each commit are added to the response"),
+ first_parent: flexibleBoolean
+ .optional()
+ .describe("Follow only the first parent commit upon seeing a merge commit"),
+ order: z.enum(["default", "topo"]).optional().describe("List commits in order"),
+ trailers: flexibleBoolean.optional().describe("Parse and include Git trailers for every commit"),
+ 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)"),
+});
+
+export const GetCommitSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
+ sha: z.string().describe("The commit hash or name of a repository branch or tag"),
+ stats: flexibleBoolean.optional().describe("Include commit stats"),
+});
+
+export const GetCommitDiffSchema = z.object({
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
+ sha: z.string().describe("The commit hash or name of a repository branch or tag"),
+});
+
+// Schema for listing issues assigned to the current user
+export const MyIssuesSchema = z.object({
+ project_id: z.string().optional().describe("Project ID or URL-encoded path (optional when GITLAB_PROJECT_ID is set)"),
+ state: z
+ .enum(["opened", "closed", "all"])
+ .optional()
+ .describe("Return issues with a specific state (default: opened)"),
+ labels: z.array(z.string()).optional().describe("Array of label names to filter by"),
+ milestone: z.string().optional().describe("Milestone title to filter by"),
+ search: z.string().optional().describe("Search for specific terms in title and description"),
+ created_after: z.string().optional().describe("Return issues created after the given time (ISO 8601)"),
+ created_before: z.string().optional().describe("Return issues created before the given time (ISO 8601)"),
+ updated_after: z.string().optional().describe("Return issues updated after the given time (ISO 8601)"),
+ updated_before: z.string().optional().describe("Return issues updated before the given time (ISO 8601)"),
+ per_page: z.number().optional().describe("Number of items per page (default: 20, max: 100)"),
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
+});
+
+// Schema for listing project members
+export const ListProjectMembersSchema = z.object({
+ project_id: z.string().describe("Project ID or URL-encoded path"),
+ query: z.string().optional().describe("Search for members by name or username"),
+ user_ids: z.array(z.number()).optional().describe("Filter by user IDs"),
+ skip_users: z.array(z.number()).optional().describe("User IDs to exclude"),
+ per_page: z.number().optional().describe("Number of items per page (default: 20, max: 100)"),
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
+});
+
+// Schema for GitLab project member
+export const GitLabProjectMemberSchema = z.object({
+ id: z.number(),
+ username: z.string(),
+ name: z.string(),
+ state: z.string(),
+ avatar_url: z.string().nullable(),
+ web_url: z.string(),
+ access_level: z.number(),
+ access_level_description: z.string().optional(),
+ created_at: z.string(),
+ expires_at: z.string().nullable().optional(),
+ email: z.string().optional(),
+
+});
+
+// Markdown upload schemas
+export const GitLabMarkdownUploadSchema = z.object({
+ id: z.number(),
+ alt: z.string(),
+ url: z.string(),
+ full_path: z.string(),
+ markdown: z.string(),
+});
+
+export const MarkdownUploadSchema = z.object({
+ project_id: z.string().describe("Project ID or URL-encoded path of the project"),
+ file_path: z.string().describe("Path to the file to upload"),
+});
+
+export const DownloadAttachmentSchema = z.object({
+ project_id: z.string().describe("Project ID or URL-encoded path of the project"),
+ secret: z.string().describe("The 32-character secret of the upload"),
+ filename: z.string().describe("The filename of the upload"),
+ local_path: z.string().optional().describe("Local path to save the file (optional, defaults to current directory)"),
+});
+
+export const GroupIteration = z.object({
+ id: z.coerce.string(),
+ iid: z.coerce.string(),
+ sequence: z.number(),
+ group_id: z.coerce.string(),
+ title: z.string().optional().nullable(),
+ description: z.string().optional().nullable(),
+ state: z.number(),
+ created_at: z.string(),
+ updated_at: z.string(),
+ due_date: z.string().optional().nullable(),
+ start_date: z.string().optional().nullable(),
+ web_url: z.string().optional().nullable(),
+});
+
+export const ListGroupIterationsSchema = z
+ .object({
+ group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
+ state: z
+ .enum(["opened", "upcoming", "current", "closed", "all"])
+ .optional()
+ .describe("Return opened, upcoming, current, closed, or all iterations."),
+ search: z
+ .string()
+ .optional()
+ .describe("Return only iterations with a title matching the provided string."),
+ in: z
+ .array(z.enum(["title", "cadence_title"]))
+ .optional()
+ .describe(
+ "Fields in which fuzzy search should be performed with the query given in the argument search. The available options are title and cadence_title. Default is [title]."
+ ),
+ include_ancestors: flexibleBoolean
+ .optional()
+ .describe("Include iterations for group and its ancestors. Defaults to true."),
+ include_descendants: flexibleBoolean
+ .optional()
+ .describe("Include iterations for group and its descendants. Defaults to false."),
+ updated_before: z
+ .string()
+ .optional()
+ .describe(
+ "Return only iterations updated before the given datetime. Expected in ISO 8601 format (2019-03-15T08:00:00Z)."
+ ),
+ updated_after: z
+ .string()
+ .optional()
+ .describe(
+ "Return only iterations updated after the given datetime. Expected in ISO 8601 format (2019-03-15T08:00:00Z)."
+ ),
+ })
+ .merge(PaginationOptionsSchema);
+
// Export types
export type GitLabAuthor = z.infer;
export type GitLabFork = z.infer;
export type GitLabIssue = z.infer;
+export type GitLabIssueWithLinkDetails = z.infer;
export type GitLabMergeRequest = z.infer;
export type GitLabRepository = z.infer;
export type GitLabFileContent = z.infer;
-export type GitLabDirectoryContent = z.infer<
- typeof GitLabDirectoryContentSchema
->;
+export type GitLabDirectoryContent = z.infer;
export type GitLabContent = z.infer;
export type FileOperation = z.infer;
export type GitLabTree = z.infer;
+export type GitLabCompareResult = z.infer;
export type GitLabCommit = z.infer;
export type GitLabReference = z.infer;
-export type CreateRepositoryOptions = z.infer<
- typeof CreateRepositoryOptionsSchema
->;
+export type CreateRepositoryOptions = z.infer;
export type CreateIssueOptions = z.infer;
-export type CreateMergeRequestOptions = z.infer<
- typeof CreateMergeRequestOptionsSchema
->;
+export type CreateMergeRequestOptions = z.infer;
export type CreateBranchOptions = z.infer;
-export type GitLabCreateUpdateFileResponse = z.infer<
- typeof GitLabCreateUpdateFileResponseSchema
->;
+export type GitLabCreateUpdateFileResponse = z.infer;
export type GitLabSearchResponse = z.infer;
-export type GitLabMergeRequestDiff = z.infer<
- typeof GitLabMergeRequestDiffSchema
->;
+export type GitLabMergeRequestDiff = z.infer