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. +[![Star History Chart](https://api.star-history.com/svg?repos=zereight/gitlab-mcp&type=Date)](https://www.star-history.com/#zereight/gitlab-mcp&Date) -## Installation and Execution +## @zereight/mcp-gitlab -```bash -npx @zereight/mcp-gitlab -``` +[![smithery badge](https://smithery.ai/badge/@zereight/gitlab-mcp)](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: +gitlab mcp MCP server -```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; +export type CreateNoteOptions = z.infer; +export type GitLabIssueLink = z.infer; +export type ListIssueDiscussionsOptions = z.infer; +export type ListMergeRequestDiscussionsOptions = z.infer; +export type UpdateIssueNoteOptions = z.infer; +export type CreateIssueNoteOptions = z.infer; +export type GitLabNamespace = z.infer; +export type GitLabNamespaceExistsResponse = z.infer; +export type GitLabProject = z.infer; +export type GitLabLabel = z.infer; +export type ListWikiPagesOptions = z.infer; +export type GetWikiPageOptions = z.infer; +export type CreateWikiPageOptions = z.infer; +export type UpdateWikiPageOptions = z.infer; +export type DeleteWikiPageOptions = z.infer; +export type GitLabWikiPage = z.infer; +export type GitLabTreeItem = z.infer; +export type GetRepositoryTreeOptions = z.infer; +export type MergeRequestThreadPosition = z.infer; +export type MergeRequestThreadPositionCreate = z.infer; +export type CreateMergeRequestThreadOptions = z.infer; +export type CreateMergeRequestNoteOptions = z.infer; +export type GitLabPipelineJob = z.infer; +export type GitLabPipelineTriggerJob = z.infer; +export type GitLabPipeline = z.infer; +export type ListPipelinesOptions = z.infer; +export type GetPipelineOptions = z.infer; +export type ListPipelineJobsOptions = z.infer; +export type ListPipelineTriggerJobsOptions = z.infer; +export type CreatePipelineOptions = z.infer; +export type RetryPipelineOptions = z.infer; +export type CancelPipelineOptions = z.infer; +export type GitLabMilestones = z.infer; +export type ListProjectMilestonesOptions = z.infer; +export type GetProjectMilestoneOptions = z.infer; +export type CreateProjectMilestoneOptions = z.infer; +export type EditProjectMilestoneOptions = z.infer; +export type DeleteProjectMilestoneOptions = z.infer; +export type GetMilestoneIssuesOptions = z.infer; +export type GetMilestoneMergeRequestsOptions = z.infer; +export type PromoteProjectMilestoneOptions = z.infer; +export type GetMilestoneBurndownEventsOptions = z.infer; +export type GitLabUser = z.infer; +export type GitLabUsersResponse = z.infer; +export type PaginationOptions = z.infer; +export type ListCommitsOptions = z.infer; +export type GetCommitOptions = z.infer; +export type GetCommitDiffOptions = z.infer; +export type MyIssuesOptions = z.infer; +export type ListProjectMembersOptions = z.infer; +export type GitLabProjectMember = z.infer; +export type GroupIteration = z.infer; +export type ListGroupIterationsOptions = z.infer; + +// Draft Notes type exports +export type ListDraftNotesOptions = z.infer; +export type CreateDraftNoteOptions = z.infer; +export type UpdateDraftNoteOptions = z.infer; +export type DeleteDraftNoteOptions = z.infer; +export type PublishDraftNoteOptions = z.infer; +export type BulkPublishDraftNotesOptions = z.infer; +export type GitLabMarkdownUpload = z.infer; +export type MarkdownUploadOptions = z.infer; diff --git a/scripts/image_push.sh b/scripts/image_push.sh new file mode 100644 index 0000000..7a6b0d0 --- /dev/null +++ b/scripts/image_push.sh @@ -0,0 +1,41 @@ +#!/bin/bash +git fetch + +if [ -z "$1" ]; then + echo "Error: docker user name required." + exit 1 +fi + +git checkout main + +DOCKER_USER=$1 +IMAGE_NAME=gitlab-mcp +IMAGE_VERSION=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" main) + +BASE_VERSION="${IMAGE_VERSION#v}" +version_package=$(jq -r '.version' package.json) +version_package_lock=$(jq -r '.version' package-lock.json) +version_package_lock_version=$(jq -r '.packages[""].version' package-lock.json) + +echo " BASE_VERSION : $BASE_VERSION" +echo " package.json : $version_package" +echo " package-lock.json : $version_package_lock" +echo " package-lock.packages[\"\"] : $version_package_lock_version" + +if [ "$BASE_VERSION" = "$version_package" ] && + [ "$BASE_VERSION" = "$version_package_lock" ] && + [ "$BASE_VERSION" = "$version_package_lock_version" ]; then + echo "✅ version check ok" +else + echo "❌ invalid version" + exit 1 +fi +git checkout "${IMAGE_VERSION}" +IMAGE_VERSION=${IMAGE_VERSION#v} +echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" +npm install && npm run build +docker buildx build --platform linux/arm64,linux/amd64 \ + -t "${DOCKER_USER}/${IMAGE_NAME}:latest" \ + -t "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" \ + --push \ + . diff --git a/smithery.yaml b/smithery.yaml new file mode 100644 index 0000000..4adef2d --- /dev/null +++ b/smithery.yaml @@ -0,0 +1,21 @@ +# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml + +startCommand: + type: stdio + configSchema: + # JSON Schema defining the configuration options for the MCP. + type: object + required: + - gitlabPersonalAccessToken + properties: + gitlabPersonalAccessToken: + type: string + description: Your GitLab personal access token. + gitlabApiUrl: + type: string + default: https://gitlab.com/api/v4 + description: "Your GitLab API URL. Default: https://gitlab.com/api/v4" + commandFunction: + # A function that produces the CLI command to start the MCP on stdio. + |- + (config) => ({ command: 'node', args: ['build/index.js'], env: { GITLAB_PERSONAL_ACCESS_TOKEN: config.gitlabPersonalAccessToken, GITLAB_API_URL: config.gitlabApiUrl } }) diff --git a/test-note.ts b/test-note.ts new file mode 100644 index 0000000..867bb94 --- /dev/null +++ b/test-note.ts @@ -0,0 +1,64 @@ +/** + * This test file verifies that the createNote function works correctly + * with the fixed endpoint URL construction that uses plural resource names + * (issues instead of issue, merge_requests instead of merge_request). + */ + +import fetch from "node-fetch"; + +// GitLab API configuration (replace with actual values when testing) +const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; +const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_TOKEN || ""; +const PROJECT_ID = process.env.PROJECT_ID || "your/project"; +const ISSUE_IID = Number(process.env.ISSUE_IID || "1"); + +async function testCreateIssueNote() { + try { + // Using plural form "issues" in the URL + const url = new URL( + `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + PROJECT_ID + )}/issues/${ISSUE_IID}/notes` + ); + + const response = await fetch(url.toString(), { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + }, + body: JSON.stringify({ body: "Test note from API - with plural endpoint" }), + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); + } + + const data = await response.json(); + console.log("Successfully created note:"); + console.log(JSON.stringify(data, null, 2)); + return true; + } catch (error) { + console.error("Error creating note:", error); + return false; + } +} + +// Only run the test if executed directly +if (require.main === module) { + console.log("Testing note creation with plural 'issues' endpoint..."); + testCreateIssueNote().then(success => { + if (success) { + console.log("✅ Test successful!"); + process.exit(0); + } else { + console.log("❌ Test failed!"); + process.exit(1); + } + }); +} + +// Export for use in other tests +export { testCreateIssueNote }; diff --git a/test/clients/client.ts b/test/clients/client.ts new file mode 100644 index 0000000..5f76c51 --- /dev/null +++ b/test/clients/client.ts @@ -0,0 +1,67 @@ +/** + * MCP Client Interface and error classes for testing + */ + +import { CallToolResult, ListToolsResult } from "@modelcontextprotocol/sdk/types.js"; + +export interface MCPClientInterface { + /** + * Connect to MCP server + */ + connect(connectionString: string, options?: Record): Promise; + + /** + * Disconnect from server + */ + disconnect(): Promise; + + /** + * List available tools from server + */ + listTools(): Promise; + + /** + * Call a tool on the server + */ + callTool(name: string, arguments_?: Record): Promise; + + /** + * Test connection by listing tools + */ + testConnection(): Promise; + + /** + * Get client connection status + */ + get isConnected(): boolean; +} + +/** + * Base error class for MCP client errors + */ +export class MCPClientError extends Error { + constructor(message: string, public readonly cause?: Error) { + super(message); + this.name = 'MCPClientError'; + } +} + +/** + * Connection error for MCP clients + */ +export class MCPConnectionError extends MCPClientError { + constructor(message: string, cause?: Error) { + super(message, cause); + this.name = 'MCPConnectionError'; + } +} + +/** + * Tool call error for MCP clients + */ +export class MCPToolCallError extends MCPClientError { + constructor(message: string, public readonly toolName?: string, cause?: Error) { + super(message, cause); + this.name = 'MCPToolCallError'; + } +} diff --git a/test/clients/sse-client.ts b/test/clients/sse-client.ts new file mode 100644 index 0000000..1b86397 --- /dev/null +++ b/test/clients/sse-client.ts @@ -0,0 +1,113 @@ +/** + * SSE MCP Client for testing + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { CallToolResult, ListToolsResult } from "@modelcontextprotocol/sdk/types.js"; +import { MCPClientInterface, MCPConnectionError, MCPToolCallError } from "./client.js"; + +export class SSETestClient implements MCPClientInterface { + private client: Client; + private transport: SSEClientTransport | null = null; + + constructor() { + this.client = new Client({ name: "test-client", version: "1.0.0" }); + } + + /** + * Connect to MCP server via SSE + */ + async connect(url: string): Promise { + if (this.transport) { + throw new MCPConnectionError('Client is already connected'); + } + + try { + this.transport = new SSEClientTransport(new URL(url)); + await this.client.connect(this.transport); + } catch (error) { + this.transport = null; + throw new MCPConnectionError( + `Failed to connect to SSE server: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Disconnect from server + */ + async disconnect(): Promise { + if (this.transport) { + try { + await this.transport.close(); + } catch (error) { + // Log but don't throw on disconnect errors + console.warn('Warning during disconnect:', error); + } finally { + this.transport = null; + } + } + } + + /** + * List available tools from server + */ + async listTools(): Promise { + if (!this.transport) { + throw new MCPConnectionError('Client is not connected'); + } + + try { + const response = await this.client.listTools(); + return response; + } catch (error) { + throw new MCPToolCallError( + `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, + 'listTools', + error instanceof Error ? error : undefined + ); + } + } + + /** + * Call a tool on the server + */ + async callTool(name: string, arguments_: Record = {}): Promise { + if (!this.transport) { + throw new MCPConnectionError('Client is not connected'); + } + + try { + const response = await this.client.callTool({ name, arguments: arguments_ }); + // Ensure the response conforms to CallToolResult interface + return response as CallToolResult; + } catch (error) { + throw new MCPToolCallError( + `Failed to call tool '${name}': ${error instanceof Error ? error.message : String(error)}`, + name, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Test connection by listing tools + */ + async testConnection(): Promise { + try { + const tools = await this.listTools(); + return Array.isArray(tools.tools) && tools.tools.length > 0; + } catch (error) { + return false; + } + } + + /** + * Get client connection status + */ + get isConnected(): boolean { + return this.transport !== null; + } +} \ No newline at end of file diff --git a/test/clients/stdio-client.ts b/test/clients/stdio-client.ts new file mode 100644 index 0000000..41605f3 --- /dev/null +++ b/test/clients/stdio-client.ts @@ -0,0 +1,136 @@ +/** + * Stdio MCP Client for testing + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { CallToolResult, ListToolsResult } from "@modelcontextprotocol/sdk/types.js"; +import { MCPClientInterface, MCPConnectionError, MCPToolCallError } from "./client.js"; + +export class StdioTestClient implements MCPClientInterface { + private client: Client; + private transport: StdioClientTransport | null = null; + + constructor() { + this.client = new Client({ name: "test-client", version: "1.0.0" }); + } + + /** + * Connect to MCP server via stdio + */ + async connect(serverPath: string, env?: Record): Promise { + if (this.transport) { + throw new MCPConnectionError('Client is already connected'); + } + + try { + const command = process.execPath; + const args = [serverPath]; + + // Prepare environment variables for the server process + const serverEnv: Record = {}; + + // Copy process.env, filtering out undefined values + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + serverEnv[key] = value; + } + } + + // Add custom environment variables + if (env) { + Object.assign(serverEnv, env); + } + + this.transport = new StdioClientTransport({ + command, + args, + env: serverEnv + }); + + await this.client.connect(this.transport); + } catch (error) { + this.transport = null; + throw new MCPConnectionError( + `Failed to connect to stdio server: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Disconnect from server + */ + async disconnect(): Promise { + if (this.transport) { + try { + await this.transport.close(); + } catch (error) { + // Log but don't throw on disconnect errors + console.warn('Warning during disconnect:', error); + } finally { + this.transport = null; + } + } + } + + /** + * List available tools from server + */ + async listTools(): Promise { + if (!this.transport) { + throw new MCPConnectionError('Client is not connected'); + } + + try { + const response = await this.client.listTools(); + return response; + } catch (error) { + throw new MCPToolCallError( + `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, + 'listTools', + error instanceof Error ? error : undefined + ); + } + } + + /** + * Call a tool on the server + */ + async callTool(name: string, arguments_: Record = {}): Promise { + if (!this.transport) { + throw new MCPConnectionError('Client is not connected'); + } + + try { + const response = await this.client.callTool({ name, arguments: arguments_ }); + // Ensure the response conforms to CallToolResult interface + return response as CallToolResult; + } catch (error) { + throw new MCPToolCallError( + `Failed to call tool '${name}': ${error instanceof Error ? error.message : String(error)}`, + name, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Test connection by listing tools + */ + async testConnection(): Promise { + try { + const tools = await this.listTools(); + return Array.isArray(tools.tools) && tools.tools.length > 0; + } catch (error) { + return false; + } + } + + /** + * Get client connection status + */ + get isConnected(): boolean { + return this.transport !== null; + } +} \ No newline at end of file diff --git a/test/clients/streamable-http-client.ts b/test/clients/streamable-http-client.ts new file mode 100644 index 0000000..390f8df --- /dev/null +++ b/test/clients/streamable-http-client.ts @@ -0,0 +1,113 @@ +/** + * Streamable HTTP MCP Client for testing + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { CallToolResult, ListToolsResult } from "@modelcontextprotocol/sdk/types.js"; +import { MCPClientInterface, MCPConnectionError, MCPToolCallError } from "./client.js"; + +export class StreamableHTTPTestClient implements MCPClientInterface { + private client: Client; + private transport: StreamableHTTPClientTransport | null = null; + + constructor() { + this.client = new Client({ name: "test-client", version: "1.0.0" }); + } + + /** + * Connect to MCP server via Streamable HTTP + */ + async connect(url: string): Promise { + if (this.transport) { + throw new MCPConnectionError('Client is already connected'); + } + + try { + this.transport = new StreamableHTTPClientTransport(new URL(url)); + await this.client.connect(this.transport); + } catch (error) { + this.transport = null; + throw new MCPConnectionError( + `Failed to connect to Streamable HTTP server: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Disconnect from server + */ + async disconnect(): Promise { + if (this.transport) { + try { + await this.transport.close(); + } catch (error) { + // Log but don't throw on disconnect errors + console.warn('Warning during disconnect:', error); + } finally { + this.transport = null; + } + } + } + + /** + * List available tools from server + */ + async listTools(): Promise { + if (!this.transport) { + throw new MCPConnectionError('Client is not connected'); + } + + try { + const response = await this.client.listTools(); + return response; + } catch (error) { + throw new MCPToolCallError( + `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, + 'listTools', + error instanceof Error ? error : undefined + ); + } + } + + /** + * Call a tool on the server + */ + async callTool(name: string, arguments_: Record = {}): Promise { + if (!this.transport) { + throw new MCPConnectionError('Client is not connected'); + } + + try { + const response = await this.client.callTool({ name, arguments: arguments_ }); + // Ensure the response conforms to CallToolResult interface + return response as CallToolResult; + } catch (error) { + throw new MCPToolCallError( + `Failed to call tool '${name}': ${error instanceof Error ? error.message : String(error)}`, + name, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Test connection by listing tools + */ + async testConnection(): Promise { + try { + const tools = await this.listTools(); + return Array.isArray(tools.tools) && tools.tools.length > 0; + } catch (error) { + return false; + } + } + + /** + * Get client connection status + */ + get isConnected(): boolean { + return this.transport !== null; + } +} \ No newline at end of file diff --git a/test/test-all-transport-server.ts b/test/test-all-transport-server.ts new file mode 100644 index 0000000..26cb0ec --- /dev/null +++ b/test/test-all-transport-server.ts @@ -0,0 +1,294 @@ +/** + * GitLab MCP Server Transport Tests + * Tests all three transport modes: stdio, SSE, and streamable-http + */ + +import * as path from 'path'; +import { describe, test, after, before } from 'node:test'; +import assert from 'node:assert'; +import { launchServer, findAvailablePort, cleanupServers, ServerInstance, TransportMode, checkHealthEndpoint, HOST } from './utils/server-launcher.js'; +import { StdioTestClient } from './clients/stdio-client.js'; +import { SSETestClient } from './clients/sse-client.js'; +import { StreamableHTTPTestClient } from './clients/streamable-http-client.js'; +import { MCPClientInterface } from './clients/client.js'; +import { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'; + +console.log('🚀 GitLab MCP Server Tests'); +console.log(''); + +// Configuration check +const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; +const GITLAB_TOKEN = process.env.GITLAB_TOKEN_TEST || process.env.GITLAB_TOKEN; +const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID; + +console.log('🔧 Test Configuration:'); +console.log(` GitLab URL: ${GITLAB_API_URL}`); +console.log(` Token: ${GITLAB_TOKEN ? '✅ Provided' : '❌ Missing'}`); +console.log(` Project ID: ${TEST_PROJECT_ID || '❌ Missing'}`); + +// Validate required configuration +if (!GITLAB_TOKEN) { + console.error('❌ Error: GITLAB_TOKEN_TEST or GITLAB_TOKEN environment variable is required for testing'); + console.error(' Set one of these variables to your GitLab API token'); + process.exit(1); +} + +if (!TEST_PROJECT_ID) { + console.error('❌ Error: TEST_PROJECT_ID environment variable is required for testing'); + console.error(' Set this variable to a valid GitLab project ID (e.g., "123" or "group/project")'); + process.exit(1); +} + +console.log('✅ Configuration validated'); +console.log(''); + +let servers: ServerInstance[] = []; + +// Cleanup function for all tests +const cleanup = () => { + cleanupServers(servers); + servers = []; +}; + +// Handle process termination +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); +process.on('exit', cleanup); + +describe('GitLab MCP Server - Stdio Transport', () => { + let client: MCPClientInterface; + + // Prepare environment variables for stdio server + const stdioEnv: Record = { + GITLAB_PERSONAL_ACCESS_TOKEN: GITLAB_TOKEN, + GITLAB_API_URL: `${GITLAB_API_URL}/api/v4`, + GITLAB_PROJECT_ID: TEST_PROJECT_ID, + GITLAB_READ_ONLY_MODE: 'true', + // Explicitly disable other transport modes to ensure stdio mode + SSE: 'false', + STREAMABLE_HTTP: 'false' + }; + + before(async () => { + client = new StdioTestClient(); + const serverPath = path.resolve(process.cwd(), 'build/index.js'); + await client.connect(serverPath, stdioEnv); + assert.ok(client.isConnected, 'Client should be connected'); + console.log('Client connected to stdio server'); + }); + + after(async () => { + if (client && client.isConnected) { + await client.disconnect(); + } + }); + + test('should list tools via stdio', async () => { + const tools = await client.listTools(); + assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined'); + assert.ok('tools' in tools, 'Response should have tools property'); + assert.ok(Array.isArray(tools.tools) && tools.tools.length > 0, 'Tools array should not be empty'); + + // Check for specific GitLab tools with proper typing + const toolNames = tools.tools.map(tool => tool.name); + assert.ok(toolNames.includes('list_merge_requests'), 'Should have list_merge_requests tool'); + assert.ok(toolNames.includes('get_project'), 'Should have get_project tool'); + + // Verify tools have proper structure + const gitlabTools = tools.tools.filter(tool => + tool.name === 'list_merge_requests' || tool.name === 'get_project' + ); + assert.ok(gitlabTools.length >= 2, 'Should have at least 2 GitLab tools'); + + for (const tool of gitlabTools) { + assert.ok(tool.description !== null && tool.description !== undefined, `Tool ${tool.name} should have description`); + assert.ok('inputSchema' in tool, `Tool ${tool.name} should have input schema`); + } + }); + + test('should call list_merge_requests tool via stdio', async () => { + const result = await client.callTool('list_merge_requests', { + project_id: TEST_PROJECT_ID + }); + + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); + assert.ok('content' in result, 'Result should have content property'); + }); + + test('should call get_project tool via stdio', async () => { + const result = await client.callTool('get_project', { + project_id: TEST_PROJECT_ID + }); + + // Verify proper CallToolResult structure + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); + assert.ok('content' in result, 'Result should have content property'); + assert.ok(Array.isArray(result.content), 'Content should be an array'); + assert.ok(result.content.length > 0, 'Content array should not be empty'); + + // Check content structure + const firstContent = result.content[0]; + assert.ok(firstContent !== null && firstContent !== undefined, 'First content item should be defined'); + assert.ok('type' in firstContent, 'Content item should have type'); + assert.strictEqual(firstContent.type, 'text', 'Content type should be text'); + assert.ok('text' in firstContent, 'Text content should have text property'); + + // Verify it's valid JSON containing project info + const projectData = JSON.parse((firstContent as any).text); + assert.ok(projectData !== null && projectData !== undefined, 'Project data should be parseable JSON'); + assert.ok('id' in projectData, 'Project should have id'); + assert.ok('name' in projectData, 'Project should have name'); + }); +}); + +describe('GitLab MCP Server - SSE Transport', () => { + let server: ServerInstance; + let client: MCPClientInterface; + let port: number; + + before(async () => { + port = await findAvailablePort(); + server = await launchServer({ + mode: TransportMode.SSE, + port, + timeout: 3000, + env: { + SSE: 'true', + STREAMABLE_HTTP: 'false' + } + }); + servers.push(server); + + // Verify server started successfully + assert.ok(server.process.pid !== undefined, 'Server process should have PID'); + assert.strictEqual(server.mode, TransportMode.SSE, 'Server mode should be SSE'); + assert.strictEqual(server.port, port, 'Server should use correct port'); + + // Verify health check + const health = await checkHealthEndpoint(server.port); + assert.strictEqual(health.status, 'healthy', 'Health status should be healthy'); + assert.strictEqual(health.transport, 'sse', 'Transport should be SSE'); + assert.ok(health.version !== null && health.version !== undefined, 'Version should be defined'); + + // Create and connect client + client = new SSETestClient(); + await client.connect(`http://${HOST}:${port}/sse`); + assert.ok(client.isConnected, 'Client should be connected'); + assert.ok(await client.testConnection(), 'Connection test should pass'); + console.log('Client connected to SSE server'); + }); + + after(async () => { + if (client && client.isConnected) { + await client.disconnect(); + } + cleanup(); + console.log('Client disconnected from SSE server'); + }); + + test('should list tools via SSE', async () => { + const tools = await client.listTools(); + assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined'); + assert.ok('tools' in tools, 'Response should have tools property'); + assert.ok(Array.isArray(tools.tools) && tools.tools.length > 0, 'Tools array should not be empty'); + + // Check for specific GitLab tools + const toolNames = tools.tools.map((tool: any) => tool.name); + assert.ok(toolNames.includes('list_merge_requests'), 'Should have list_merge_requests tool'); + assert.ok(toolNames.includes('get_project'), 'Should have get_project tool'); + }); + + test('should call list_merge_requests tool via SSE', async () => { + const result = await client.callTool('list_merge_requests', { + project_id: TEST_PROJECT_ID + }); + + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); + assert.ok('content' in result, 'Result should have content property'); + }); + + test('should call get_project tool via SSE', async () => { + const result = await client.callTool('get_project', { + project_id: TEST_PROJECT_ID + }); + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); + assert.ok('content' in result, 'Result should have content property'); + }); +}); + +describe('GitLab MCP Server - Streamable HTTP Transport', () => { + let server: ServerInstance; + let client: MCPClientInterface; + let port: number; + + before(async () => { + port = await findAvailablePort(); + server = await launchServer({ + mode: TransportMode.STREAMABLE_HTTP, + port, + timeout: 3000, + env: { + SSE: 'false', + STREAMABLE_HTTP: 'true' + } + }); + servers.push(server); + + // Verify server started successfully + assert.ok(server.process.pid !== undefined, 'Server process should have PID'); + assert.strictEqual(server.mode, TransportMode.STREAMABLE_HTTP, 'Server mode should be streamable-http'); + assert.strictEqual(server.port, port, 'Server should use correct port'); + + // Verify health check + const health = await checkHealthEndpoint(server.port); + assert.strictEqual(health.status, 'healthy', 'Health status should be healthy'); + assert.strictEqual(health.transport, 'streamable-http', 'Transport should be streamable-http'); + assert.ok(health.version !== null && health.version !== undefined, 'Version should be defined'); + assert.ok(health.activeSessions !== null && health.activeSessions !== undefined, 'Active sessions should be defined'); + + // Create and connect client + client = new StreamableHTTPTestClient(); + await client.connect(`http://${HOST}:${port}/mcp`); + assert.ok(client.isConnected, 'Client should be connected'); + assert.ok(await client.testConnection(), 'Connection test should pass'); + + console.log('Client connected to Streamable HTTP server'); + }); + + after(async () => { + if (client && client.isConnected) { + await client.disconnect(); + } + cleanup(); + console.log('Client disconnected from Streamable HTTP server'); + }); + + test('should list tools via Streamable HTTP', async () => { + const tools: ListToolsResult = await client.listTools(); + assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined'); + assert.ok('tools' in tools, 'Response should have tools property'); + assert.ok(Array.isArray(tools.tools) && tools.tools.length > 0, 'Tools array should not be empty'); + + // Check for specific GitLab tools + const toolNames = tools.tools.map((tool: any) => tool.name); + assert.ok(toolNames.includes('list_merge_requests'), 'Should have list_merge_requests tool'); + assert.ok(toolNames.includes('get_project'), 'Should have get_project tool'); + }); + + test('should call list_merge_requests tool via Streamable HTTP', async () => { + const result = await client.callTool('list_merge_requests', { + project_id: TEST_PROJECT_ID + }); + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); + assert.ok('content' in result, 'Result should have content property'); + }); + + test('should call get_project tool via Streamable HTTP', async () => { + const result = await client.callTool('get_project', { + project_id: TEST_PROJECT_ID + }); + + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); + assert.ok('content' in result, 'Result should have content property'); + }); +}); \ No newline at end of file diff --git a/test/utils/server-launcher.ts b/test/utils/server-launcher.ts new file mode 100644 index 0000000..6444368 --- /dev/null +++ b/test/utils/server-launcher.ts @@ -0,0 +1,271 @@ +/** + * Server launcher utility for testing different transport modes + * Manages server processes and provides clean shutdown + */ + +import { spawn, ChildProcess } from 'child_process'; +import * as path from 'path'; + +export const HOST = process.env.HOST || '127.0.0.1'; + +export enum TransportMode { + STDIO = 'stdio', + SSE = 'sse', + STREAMABLE_HTTP = 'streamable-http' +} + +export interface ServerConfig { + mode: TransportMode; + port?: number; + env?: Record; + timeout?: number; +} + +export interface ServerInstance { + process: ChildProcess; + port?: number; + mode: TransportMode; + kill: () => void; +} + +/** + * Launch a server with specified configuration + */ +export async function launchServer(config: ServerConfig): Promise { + const { + mode, + port = 3002, + env = {}, + timeout = 3000 + } = config; + + // Prepare environment variables based on transport mode + // Use same configuration pattern as existing validate-api.js + const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; + const GITLAB_TOKEN = process.env.GITLAB_TOKEN_TEST || process.env.GITLAB_TOKEN; + const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID; + + // Validate that we have required configuration + if (!GITLAB_TOKEN) { + throw new Error('GITLAB_TOKEN_TEST or GITLAB_TOKEN environment variable is required for server testing'); + } + if (!TEST_PROJECT_ID) { + throw new Error('TEST_PROJECT_ID environment variable is required for server testing'); + } + + const serverEnv: Record = { + // Add all environment variables from the current process + ...process.env, + GITLAB_API_URL: `${GITLAB_API_URL}/api/v4`, + GITLAB_PROJECT_ID: TEST_PROJECT_ID, + GITLAB_READ_ONLY_MODE: 'true', // Use read-only mode for testing + ...env, + }; + + // Set transport-specific environment variables + switch (mode) { + case TransportMode.SSE: + serverEnv.SSE = 'true'; + serverEnv.PORT = port.toString(); + break; + case TransportMode.STREAMABLE_HTTP: + serverEnv.STREAMABLE_HTTP = 'true'; + serverEnv.PORT = port.toString(); + break; + case TransportMode.STDIO: + // Stdio mode doesn't need port configuration - uses process communication + throw new Error(`${TransportMode.STDIO} mode is not supported for server testing, because it uses process communication.`); + } + + const serverPath = path.resolve(process.cwd(), 'build/index.js'); + + const serverProcess = spawn('node', [serverPath], { + env: serverEnv, + stdio: ['pipe', 'pipe', 'pipe'], + detached: false + }); + + // Wait for server to start + await waitForServerStart(serverProcess, mode, port, timeout); + + const instance: ServerInstance = { + process: serverProcess, + port: port, + mode, + kill: () => { + if (!serverProcess.killed) { + serverProcess.kill('SIGTERM'); + + // Force kill if not terminated within 5 seconds + setTimeout(() => { + if (!serverProcess.killed) { + serverProcess.kill('SIGKILL'); + } + }, 5000); + } + } + }; + + return instance; +} + +/** + * Wait for server to start based on transport mode + */ +async function waitForServerStart( + process: ChildProcess, + mode: TransportMode, + port: number, + timeout: number +): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Server failed to start within ${timeout}ms for mode ${mode}`)); + }, timeout); + + let outputBuffer = ''; + + const onData = (data: Buffer) => { + const output = data.toString(); + outputBuffer += output; + + // Check for server start messages + const startMessages = [ + 'Starting GitLab MCP Server with stdio transport', + 'Starting GitLab MCP Server with SSE transport', + 'Starting GitLab MCP Server with Streamable HTTP transport', + 'GitLab MCP Server running', + `port ${port}` + ]; + + const hasStartMessage = startMessages.some(msg => + outputBuffer.includes(msg) + ); + + if (hasStartMessage) { + clearTimeout(timer); + process.stdout?.removeListener('data', onData); + process.stderr?.removeListener('data', onData); + + // Additional wait for HTTP servers to be fully ready + if (mode !== TransportMode.STDIO) { + setTimeout(resolve, 1000); + } else { + resolve(); + } + } + }; + + const onError = (error: Error) => { + clearTimeout(timer); + reject(new Error(`Server process error: ${error.message}`)); + }; + + const onExit = (code: number | null) => { + clearTimeout(timer); + reject(new Error(`Server process exited with code ${code} before starting`)); + }; + + process.stdout?.on('data', onData); + process.stderr?.on('data', onData); + process.on('error', onError); + process.on('exit', onExit); + }); +} + +/** + * Find an available port starting from a base port + */ +export async function findAvailablePort(basePort: number = 3002): Promise { + const net = await import('net'); + + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.listen(basePort, () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : basePort; + + server.close(() => resolve(port)); + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + // Port is in use, try next one + resolve(findAvailablePort(basePort + 1)); + } else { + reject(err); + } + }); + }); +} + +/** + * Clean shutdown for multiple server instances + */ +export function cleanupServers(servers: ServerInstance[]): void { + servers.forEach(server => { + try { + server.kill(); + } catch (error) { + console.warn(`Failed to kill server process: ${error}`); + } + }); +} + + +/** + * Health check response interface + */ +export interface HealthCheckResponse { + status: string; + version: string; + transport: string; + activeSessions?: number; +} + +/** + * Create AbortController with timeout + */ +export function createTimeoutController(timeout: number): AbortController { + const controller = new AbortController(); + setTimeout(() => controller.abort(), timeout); + return controller; +} + +/** + * Check if a health endpoint is responding + */ +export async function checkHealthEndpoint(port: number, maxRetries: number = 5): Promise { + let lastError: Error; + + for (let i = 0; i < maxRetries; i++) { + try { + const controller = createTimeoutController(5000); + const response = await fetch(`http://${HOST}:${port}/health`, { + method: 'GET', + signal: controller.signal + }); + + if (response.ok) { + const healthData = await response.json() as HealthCheckResponse; + return healthData; + } else { + throw new Error(`Health check failed with status ${response.status}`); + } + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + lastError = new Error('Request timeout after 5000ms'); + } else { + lastError = error instanceof Error ? error : new Error(String(error)); + } + + if (i < maxRetries - 1) { + // Wait before retry + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } + + throw lastError!; +} \ No newline at end of file diff --git a/test/validate-api.js b/test/validate-api.js new file mode 100755 index 0000000..2ade02c --- /dev/null +++ b/test/validate-api.js @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +// Simple API validation script for PR testing +import fetch from "node-fetch"; + +const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; +const GITLAB_TOKEN = process.env.GITLAB_TOKEN_TEST || process.env.GITLAB_TOKEN; +const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID; + +async function validateGitLabAPI() { + console.log("🔍 Validating GitLab API connection...\n"); + + if (!GITLAB_TOKEN) { + console.warn("⚠️ No GitLab token provided. Skipping API validation."); + console.log("Set GITLAB_TOKEN_TEST or GITLAB_TOKEN to enable API validation.\n"); + return true; + } + + if (!TEST_PROJECT_ID) { + console.warn("⚠️ No test project ID provided. Skipping API validation."); + console.log("Set TEST_PROJECT_ID to enable API validation.\n"); + return true; + } + + const tests = [ + { + name: "Fetch project info", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}`, + validate: data => data.id && data.name, + }, + { + name: "List issues", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues?per_page=1`, + validate: data => Array.isArray(data), + }, + { + name: "List merge requests", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/merge_requests?per_page=1`, + validate: data => Array.isArray(data), + }, + { + name: "List branches", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`, + validate: data => Array.isArray(data), + }, + { + name: "List pipelines", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`, + validate: data => Array.isArray(data), + }, + ]; + + let allPassed = true; + let firstPipelineId = null; + + for (const test of tests) { + try { + console.log(`Testing: ${test.name}`); + const response = await fetch(test.url, { + headers: { + Authorization: `Bearer ${GITLAB_TOKEN}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (test.validate(data)) { + console.log(`✅ ${test.name} - PASSED\n`); + + // If we found pipelines, save the first one for additional testing + if (test.name === "List pipelines" && data.length > 0) { + firstPipelineId = data[0].id; + } + } else { + console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); + allPassed = false; + } + } catch (error) { + console.log(`❌ ${test.name} - FAILED`); + console.log(` Error: ${error.message}\n`); + allPassed = false; + } + } + + // Test pipeline-specific endpoints if we have a pipeline ID + if (firstPipelineId) { + console.log(`Found pipeline #${firstPipelineId}, testing pipeline-specific endpoints...\n`); + + const pipelineTests = [ + { + name: `Get pipeline #${firstPipelineId} details`, + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}`, + validate: data => data.id === firstPipelineId && data.status, + }, + { + name: `List pipeline #${firstPipelineId} jobs`, + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/jobs`, + validate: data => Array.isArray(data), + }, + { + name: `List pipeline #${firstPipelineId} trigger jobs (bridges)`, + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/bridges`, + validate: data => Array.isArray(data), + }, + ]; + + for (const test of pipelineTests) { + try { + console.log(`Testing: ${test.name}`); + const response = await fetch(test.url, { + headers: { + Authorization: `Bearer ${GITLAB_TOKEN}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (test.validate(data)) { + console.log(`✅ ${test.name} - PASSED\n`); + } else { + console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); + allPassed = false; + } + } catch (error) { + console.log(`❌ ${test.name} - FAILED`); + console.log(` Error: ${error.message}\n`); + allPassed = false; + } + } + } + + if (allPassed) { + console.log("✅ All API validation tests passed!"); + } else { + console.log("❌ Some API validation tests failed!"); + } + + return allPassed; +} + +// Run validation +validateGitLabAPI() + .then(success => process.exit(success ? 0 : 1)) + .catch(error => { + console.error("Unexpected error:", error); + process.exit(1); + }); + +export { validateGitLabAPI };