diff --git a/.mokogitea/mcp/CHANGELOG.md b/.mokogitea/mcp/CHANGELOG.md deleted file mode 100644 index 394ce04ab8..0000000000 --- a/.mokogitea/mcp/CHANGELOG.md +++ /dev/null @@ -1,129 +0,0 @@ - - -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Changed -- **Renamed** package from `@mokoconsulting/gitea-api-mcp` to `@mokoconsulting/mokogitea-api-mcp` to distinguish Moko's forked Gitea MCP from upstream -- **Renamed** McpServer name and bin entry to `mokogitea-api-mcp` - - -## [0.0] - 2026-05-07 - -### Added - -#### User / Auth (3 tools) -- `gitea_me` -- Get the authenticated user info -- `gitea_user_orgs` -- List organizations the authenticated user belongs to -- `gitea_user_repos` -- List repositories owned by the authenticated user - -#### Repositories (8 tools) -- `gitea_repo_get` -- Get repository details -- `gitea_repo_create` -- Create a new repository -- `gitea_repo_delete` -- Delete a repository -- `gitea_repo_edit` -- Edit repository settings -- `gitea_repo_fork` -- Fork a repository -- `gitea_repo_search` -- Search repositories -- `gitea_org_repos` -- List repositories in an organization -- `gitea_list_connections` -- List configured Gitea connections - -#### File Contents (5 tools) -- `gitea_file_get` -- Get file contents from a repository -- `gitea_dir_get` -- Get directory contents (file listing) from a repository -- `gitea_file_create_or_update` -- Create or update a file in a repository -- `gitea_file_delete` -- Delete a file from a repository -- `gitea_tree_get` -- Get the git tree for a repository (recursive file listing) - -#### Branches (4 tools) -- `gitea_branches_list` -- List branches in a repository -- `gitea_branch_get` -- Get a specific branch -- `gitea_branch_create` -- Create a new branch -- `gitea_branch_delete` -- Delete a branch - -#### Commits (2 tools) -- `gitea_commits_list` -- List commits in a repository -- `gitea_commit_get` -- Get a specific commit - -#### Issues (7 tools) -- `gitea_issues_list` -- List issues in a repository -- `gitea_issue_get` -- Get a single issue by number -- `gitea_issue_create` -- Create a new issue -- `gitea_issue_update` -- Update an issue -- `gitea_issue_comments_list` -- List comments on an issue -- `gitea_issue_comment_create` -- Add a comment to an issue -- `gitea_issue_search` -- Search issues across all repositories - -#### Labels (2 tools) -- `gitea_labels_list` -- List labels in a repository -- `gitea_label_create` -- Create a label - -#### Milestones (2 tools) -- `gitea_milestones_list` -- List milestones in a repository -- `gitea_milestone_create` -- Create a milestone - -#### Pull Requests (6 tools) -- `gitea_pulls_list` -- List pull requests -- `gitea_pull_get` -- Get a single pull request -- `gitea_pull_create` -- Create a pull request -- `gitea_pull_merge` -- Merge a pull request -- `gitea_pull_files` -- List files changed in a pull request -- `gitea_pull_review_create` -- Create a pull request review - -#### Releases (5 tools) -- `gitea_releases_list` -- List releases -- `gitea_release_get` -- Get a single release by ID -- `gitea_release_latest` -- Get the latest release -- `gitea_release_create` -- Create a new release -- `gitea_release_delete` -- Delete a release - -#### Tags (3 tools) -- `gitea_tags_list` -- List tags -- `gitea_tag_create` -- Create a tag -- `gitea_tag_delete` -- Delete a tag - -#### Actions (2 tools) -- `gitea_actions_runs_list` -- List workflow runs for a repository -- `gitea_actions_run_get` -- Get a specific workflow run - -#### Organizations (3 tools) -- `gitea_org_get` -- Get organization details -- `gitea_org_teams_list` -- List teams in an organization -- `gitea_org_members_list` -- List members of an organization - -#### Users (2 tools) -- `gitea_user_get` -- Get a user profile -- `gitea_users_search` -- Search users - -#### Webhooks (2 tools) -- `gitea_webhooks_list` -- List webhooks for a repository -- `gitea_webhook_create` -- Create a webhook - -#### Wiki (2 tools) -- `gitea_wiki_pages_list` -- List wiki pages -- `gitea_wiki_page_get` -- Get a wiki page - -#### Notifications (2 tools) -- `gitea_notifications_list` -- List notifications for the authenticated user -- `gitea_notifications_read` -- Mark all notifications as read - -#### Generic (2 tools) -- `gitea_api_request` -- Make a raw API request to any Gitea v1 endpoint -- `gitea_list_connections` -- List configured Gitea connections - -### Infrastructure -- Multi-connection config support via `~/.gitea-api-mcp.json` -- Token-based authentication (Gitea native `Authorization: token` header) -- Built on `node:https` / `node:http` (zero HTTP dependencies) -- MCP SDK v1.12.x with stdio transport - -[0.0.1]: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp/releases/tag/v0.0.1 diff --git a/.mokogitea/mcp/Dockerfile b/.mokogitea/mcp/Dockerfile deleted file mode 100644 index e2997344ff..0000000000 --- a/.mokogitea/mcp/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package.json package-lock.json ./ -RUN npm ci --production=false - -COPY tsconfig.json ./ -COPY src/ ./src/ -RUN npx tsc && npm prune --production - -EXPOSE 3100 - -ENV PORT=3100 -ENV NODE_ENV=production - -# SSE mode by default for Docker deployments -CMD ["node", "dist/sse.js"] diff --git a/.mokogitea/mcp/README.md b/.mokogitea/mcp/README.md deleted file mode 100644 index 7402d834d4..0000000000 --- a/.mokogitea/mcp/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# MokoGitea MCP Server - -A comprehensive [Model Context Protocol](https://modelcontextprotocol.io) server for [Gitea](https://gitea.com) and [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea). 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests. - -Works with any Gitea instance. MokoGitea-specific features degrade gracefully on vanilla Gitea. - -## Quick Start - -### npx (no install) - -```bash -GITEA_URL=https://gitea.example.com GITEA_TOKEN=your_token npx @mokoconsulting/mokogitea-mcp -``` - -### Claude Code - -Add to `.claude.json`: - -```json -{ - "mcpServers": { - "mokogitea": { - "command": "npx", - "args": ["@mokoconsulting/mokogitea-mcp"], - "env": { - "GITEA_URL": "https://gitea.example.com", - "GITEA_TOKEN": "your_token" - } - } - } -} -``` - -### Docker (SSE mode) - -```bash -docker run -p 3100:3100 \ - -e GITEA_URL=https://gitea.example.com \ - -e GITEA_TOKEN=your_token \ - mokoconsulting/mokogitea-mcp -``` - -Connect MCP client to `http://localhost:3100/sse`. - -### Multi-instance config - -Create `~/.mcp_mokogitea.json`: - -```json -{ - "defaultConnection": "production", - "connections": { - "production": { "baseUrl": "https://gitea.example.com", "token": "your_token" }, - "dev": { "baseUrl": "https://dev.gitea.example.com", "token": "dev_token" } - } -} -``` - -## Configuration - -| Method | Use Case | -|--------|----------| -| `GITEA_URL` + `GITEA_TOKEN` env vars | Single instance, quick setup | -| `~/.mcp_mokogitea.json` config file | Multiple instances | -| `GITEA_API_MCP_CONFIG` env var | Custom config path | -| `GITEA_INSECURE=true` | Skip TLS verification | - -## Tools (120+) - -### Repositories -`gitea_repo_create` `gitea_repo_get` `gitea_repo_edit` `gitea_repo_delete` `gitea_repo_search` `gitea_repo_fork` `gitea_repo_generate` `gitea_repo_languages` `gitea_repo_contributors` `gitea_repo_topics` `gitea_repo_topics_set` - -### Issues -`gitea_issue_create` (dedup by title) `gitea_issue_get` `gitea_issue_update` `gitea_issues_list` `gitea_issue_search` `gitea_issue_comment_create` `gitea_issue_comments_list` `gitea_issue_labels_set` `gitea_issue_bulk_set_status` - -### Pull Requests -`gitea_pull_create` `gitea_pull_get` `gitea_pulls_list` `gitea_pull_merge` `gitea_pull_files` `gitea_pull_review_create` - -### Branches and Tags -`gitea_branches_list` `gitea_branch_create` `gitea_branch_delete` `gitea_branch_get` `gitea_tags_list` `gitea_tag_create` `gitea_tag_delete` - -### Releases -`gitea_releases_list` `gitea_release_create` `gitea_release_get` `gitea_release_latest` `gitea_release_delete` `gitea_release_asset_upload` `gitea_release_asset_delete` - -### Files and Trees -`gitea_file_get` `gitea_file_create_or_update` `gitea_file_delete` `gitea_dir_get` `gitea_tree_get` `gitea_bulk_file_push` - -### Projects -`gitea_project_list` `gitea_project_create` `gitea_project_get` `gitea_project_update` `gitea_project_delete` `gitea_project_overview` `gitea_project_columns_list` `gitea_project_column_create` `gitea_project_column_delete` `gitea_project_cards_list` `gitea_project_card_add` `gitea_project_card_move` `gitea_project_card_remove` - -### Organizations -`gitea_org_get` `gitea_org_repos` `gitea_org_members_list` `gitea_org_teams_list` `gitea_org_labels_list` `gitea_org_label_create` - -### Wiki -`gitea_wiki_pages_list` `gitea_wiki_page_get` - -### MokoGitea Extensions -`gitea_manifest_get` `gitea_manifest_update` `gitea_org_custom_fields_list` `gitea_org_custom_field_create` `gitea_org_custom_field_delete` `gitea_issue_custom_fields_get` `gitea_issue_custom_fields_set` `gitea_org_issue_statuses_list` `gitea_issue_set_status` `gitea_org_issue_priorities_list` `gitea_issue_set_priority` - -### Admin and Other -`gitea_me` `gitea_users_search` `gitea_user_get` `gitea_notifications_list` `gitea_notifications_read` `gitea_commits_list` `gitea_commit_get` `gitea_compare` `gitea_webhooks_list` `gitea_webhook_create` `gitea_admin_users_list` `gitea_admin_orgs_list` `gitea_admin_cron_list` `gitea_admin_cron_run` `gitea_list_connections` - -## SSE Server - -For hosted deployments: - -``` -GET / Server info -GET /sse SSE connection endpoint -POST /message Tool call messages -GET /health Health check -``` - -## License - -GPL-3.0-or-later - [Moko Consulting](https://mokoconsulting.tech) diff --git a/.mokogitea/mcp/config.example.json b/.mokogitea/mcp/config.example.json deleted file mode 100644 index 763922d4be..0000000000 --- a/.mokogitea/mcp/config.example.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "defaultConnection": "moko", - "connections": { - "moko": { - "baseUrl": "https://git.mokoconsulting.tech", - "token": "your-gitea-access-token" - }, - "github-mirror": { - "baseUrl": "https://gitea.example.com", - "token": "your-other-token" - } - } -} diff --git a/.mokogitea/mcp/package-lock.json b/.mokogitea/mcp/package-lock.json deleted file mode 100644 index be4e013711..0000000000 --- a/.mokogitea/mcp/package-lock.json +++ /dev/null @@ -1,1198 +0,0 @@ -{ - "name": "@mokoconsulting/mcp-mokogitea-api", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@mokoconsulting/mcp-mokogitea-api", - "version": "1.0.0", - "license": "GPL-3.0-or-later", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", - "zod": "^3.24.4" - }, - "bin": { - "mokogitea-api-mcp": "dist/index.js" - }, - "devDependencies": { - "@types/node": "^22.15.3", - "typescript": "^5.8.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "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.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "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/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "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.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "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/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/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/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/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": { - "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/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.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": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "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-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/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/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/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.21", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz", - "integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "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-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/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "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/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.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/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/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-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/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-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.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "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.10" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "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" - }, - "engines": { - "node": ">= 18" - } - }, - "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/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/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", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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", - "engines": { - "node": ">=8" - } - }, - "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": { - "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.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/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/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } -} diff --git a/.mokogitea/mcp/package.json b/.mokogitea/mcp/package.json deleted file mode 100644 index 952874d1ef..0000000000 --- a/.mokogitea/mcp/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@mokoconsulting/mokogitea-mcp", - "version": "1.1.0", - "description": "MCP server for Gitea and MokoGitea - 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests", - "type": "module", - "main": "dist/index.js", - "bin": { - "mokogitea-mcp": "dist/index.js", - "mokogitea-mcp-sse": "dist/sse.js" - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "start": "node dist/index.js", - "start:sse": "node dist/sse.js", - "setup": "node scripts/setup.mjs", - "clean": "rm -rf dist/" - }, - "keywords": [ - "mcp", - "gitea", - "mokogitea", - "model-context-protocol", - "claude", - "ai", - "git", - "self-hosted", - "api", - "devops" - ], - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", - "zod": "^3.24.4" - }, - "devDependencies": { - "@types/node": "^22.15.3", - "typescript": "^5.8.3" - }, - "engines": { - "node": ">=20.0.0" - }, - "license": "GPL-3.0-or-later", - "author": "Moko Consulting ", - "homepage": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api", - "repository": { - "type": "git", - "url": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api.git" - }, - "files": [ - "dist/", - "config.example.json", - "README.md", - "LICENSE" - ], - "publishConfig": { - "access": "public" - } -} diff --git a/.mokogitea/mcp/profile.ps1 b/.mokogitea/mcp/profile.ps1 deleted file mode 100644 index d1c1b8ee14..0000000000 --- a/.mokogitea/mcp/profile.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -# mcp_mokogitea_api PowerShell Profile -# Source this with: . ./profile.ps1 - -$env:MCP_ROOT = $PSScriptRoot -$env:TEMP = 'A:\temp' -$env:TMP = 'A:\temp' - -function mcp { Set-Location $PSScriptRoot } -function mcp-src { Set-Location (Join-Path $PSScriptRoot 'src') } -function mcp-build { Set-Location $PSScriptRoot; npm run build } -function mcp-dev { Set-Location $PSScriptRoot; npm run dev } - -Write-Host "mcp_mokogitea_api profile loaded" -ForegroundColor Cyan -Write-Host " Commands: mcp-build, mcp-dev" -ForegroundColor DarkGray -Write-Host " Navigate: mcp, mcp-src" -ForegroundColor DarkGray diff --git a/.mokogitea/mcp/scripts/setup.mjs b/.mokogitea/mcp/scripts/setup.mjs deleted file mode 100644 index 125fa068d0..0000000000 --- a/.mokogitea/mcp/scripts/setup.mjs +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env node -/* Copyright (C) 2026 Moko Consulting - * SPDX-License-Identifier: GPL-3.0-or-later - * BRIEF: Interactive setup — prompts for Gitea connection details - */ -import { createInterface } from 'node:readline/promises'; -import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { homedir } from 'node:os'; - -const CONFIG_PATH = resolve(homedir(), '.gitea-api-mcp.json'); -const rl = createInterface({ input: process.stdin, output: process.stdout }); - -async function prompt(q, d) { const a = await rl.question(`${q}${d ? ` [${d}]` : ''}: `); return a.trim() || d || ''; } -async function promptRequired(q) { let a = ''; while (!a) { a = (await rl.question(`${q}: `)).trim(); if (!a) console.log(' Required.'); } return a; } - -async function main() { - console.log('\n=== gitea-api-mcp Setup ===\n'); - let existing = null; - try { existing = JSON.parse(await readFile(CONFIG_PATH, 'utf-8')); console.log(`Existing: ${Object.keys(existing.connections).join(', ')}\n`); } catch {} - - const name = await prompt('Connection name', 'moko'); - const baseUrl = await promptRequired('Gitea URL (e.g. https://git.mokoconsulting.tech)'); - const token = await promptRequired('Access token (Settings > Applications > Generate Token)'); - const insecure = (await prompt('Skip TLS verification? (y/N)', 'N')).toLowerCase() === 'y'; - - const conn = { baseUrl: baseUrl.replace(/\/+$/, ''), token }; - if (insecure) conn.insecure = true; - - const config = existing ?? { defaultConnection: name, connections: {} }; - config.connections[name] = conn; - if (!existing) config.defaultConnection = name; - else if ((await prompt(`Set "${name}" as default? (y/N)`, 'N')).toLowerCase() === 'y') config.defaultConnection = name; - - await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8'); - console.log(`\nConfig written to ${CONFIG_PATH}\n`); - rl.close(); -} - -main().catch(e => { console.error(e.message); rl.close(); process.exit(1); }); diff --git a/.mokogitea/mcp/src/client.ts b/.mokogitea/mcp/src/client.ts deleted file mode 100644 index ec1e8ffbfc..0000000000 --- a/.mokogitea/mcp/src/client.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* Copyright (C) 2026 Moko Consulting - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: gitea-api-mcp.Client - * INGROUP: gitea-api-mcp - * REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp - * PATH: /src/client.ts - * VERSION: 01.00.00 - * BRIEF: HTTP client for Gitea REST API v1 - */ - -import * as https from 'node:https'; -import * as http from 'node:http'; -import type { GiteaConnection, ApiResponse } from './types.js'; - -const API_PREFIX = '/api/v1'; -const TIMEOUT_MS = 30_000; - -export class GiteaClient { - private readonly base_url: string; - private readonly headers: Record; - private readonly insecure: boolean; - - constructor(conn: GiteaConnection) { - this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX; - this.headers = { - 'Authorization': `token ${conn.token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }; - this.insecure = conn.insecure ?? false; - } - - async get(endpoint: string, params?: Record): Promise { - return this.request(this.buildUrl(endpoint, params), 'GET'); - } - - async post(endpoint: string, body?: unknown): Promise { - return this.request(this.buildUrl(endpoint), 'POST', body); - } - - async patch(endpoint: string, body: unknown): Promise { - return this.request(this.buildUrl(endpoint), 'PATCH', body); - } - - async put(endpoint: string, body: unknown): Promise { - return this.request(this.buildUrl(endpoint), 'PUT', body); - } - - async delete(endpoint: string): Promise { - return this.request(this.buildUrl(endpoint), 'DELETE'); - } - - private buildUrl(endpoint: string, params?: Record): string { - const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; - const url = new URL(`${this.base_url}${path}`); - if (params) { - for (const [key, value] of Object.entries(params)) { - url.searchParams.set(key, value); - } - } - return url.toString(); - } - - private request(url: string, method: string, body?: unknown): Promise { - return new Promise((resolve, reject) => { - const parsed = new URL(url); - const is_https = parsed.protocol === 'https:'; - const transport = is_https ? https : http; - - const options: https.RequestOptions = { - hostname: parsed.hostname, - port: parsed.port || (is_https ? 443 : 80), - path: parsed.pathname + parsed.search, - method, - headers: { ...this.headers }, - timeout: TIMEOUT_MS, - }; - - if (this.insecure && is_https) { - options.rejectUnauthorized = false; - } - - const payload = body !== undefined ? JSON.stringify(body) : undefined; - if (payload) { - (options.headers as Record)['Content-Length'] = Buffer.byteLength(payload).toString(); - } - - const req = transport.request(options, (res) => { - const chunks: Buffer[] = []; - res.on('data', (chunk: Buffer) => chunks.push(chunk)); - res.on('end', () => { - const raw = Buffer.concat(chunks).toString('utf-8'); - let data: unknown; - try { - data = JSON.parse(raw); - } catch { - data = raw; - } - resolve({ status: res.statusCode ?? 0, data }); - }); - }); - - req.on('error', (err) => reject(err)); - req.on('timeout', () => { - req.destroy(); - reject(new Error('Request timed out')); - }); - - if (payload) { - req.write(payload); - } - req.end(); - }); - } -} diff --git a/.mokogitea/mcp/src/config.ts b/.mokogitea/mcp/src/config.ts deleted file mode 100644 index 4ff2283397..0000000000 --- a/.mokogitea/mcp/src/config.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2026 Moko Consulting -// SPDX-License-Identifier: GPL-3.0-or-later - -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { homedir } from 'node:os'; -import type { GiteaConfig, GiteaConnection } from './types.js'; - -const CONFIG_FILENAME = '.mcp_mokogitea.json'; - -export async function loadConfig(): Promise { - // Priority 1: Environment variables (zero-config single instance) - if (process.env.GITEA_URL && process.env.GITEA_TOKEN) { - const conn: GiteaConnection = { - baseUrl: process.env.GITEA_URL, - token: process.env.GITEA_TOKEN, - insecure: process.env.GITEA_INSECURE === 'true', - }; - return { - connections: { default: conn }, - defaultConnection: 'default', - }; - } - - // Priority 2: Config file - const config_path = process.env.GITEA_API_MCP_CONFIG - ? resolve(process.env.GITEA_API_MCP_CONFIG) - : resolve(homedir(), CONFIG_FILENAME); - - try { - const raw = await readFile(config_path, 'utf-8'); - const parsed = JSON.parse(raw) as Partial; - - if (!parsed.connections || Object.keys(parsed.connections).length === 0) { - throw new Error('No connections defined in config'); - } - - return { - connections: parsed.connections, - defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0], - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - throw new Error( - `Failed to load config from ${config_path}: ${message}\n` + - `Option 1: Set GITEA_URL and GITEA_TOKEN environment variables\n` + - `Option 2: Create ${config_path} - see config.example.json for format`, - ); - } -} - -export function getConnection(config: GiteaConfig, name?: string): GiteaConnection { - const key = name ?? config.defaultConnection; - const conn = config.connections[key]; - if (!conn) { - throw new Error( - `Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`, - ); - } - return conn; -} diff --git a/.mokogitea/mcp/src/index.ts b/.mokogitea/mcp/src/index.ts deleted file mode 100644 index 0b7fc508b1..0000000000 --- a/.mokogitea/mcp/src/index.ts +++ /dev/null @@ -1,2069 +0,0 @@ -#!/usr/bin/env node -/* Copyright (C) 2026 Moko Consulting - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: gitea-api-mcp.Server - * INGROUP: gitea-api-mcp - * REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp - * PATH: /src/index.ts - * VERSION: 01.00.00 - * BRIEF: MCP server entry point — registers all Gitea API tools - */ - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { z } from 'zod'; -import { loadConfig, getConnection } from './config.js'; -import { GiteaClient } from './client.js'; -import type { GiteaConfig, ApiResponse } from './types.js'; - -export let config: GiteaConfig; - -export function initConfig(c: GiteaConfig): void { - config = c; -} - -function clientFor(connection?: string): GiteaClient { - return new GiteaClient(getConnection(config, connection)); -} - -function formatResponse(res: ApiResponse): { content: Array<{ type: 'text'; text: string }> } { - if (res.status >= 400) { - const err = res.data as { message?: string }; - const msg = err?.message ?? `HTTP ${res.status}: ${JSON.stringify(res.data, null, 2)}`; - return { content: [{ type: 'text' as const, text: `Error: ${msg}` }] }; - } - return { - content: [{ type: 'text' as const, text: JSON.stringify(res.data, null, 2) }], - }; -} - -const ConnectionParam = { - connection: z.string().optional().describe('Named connection from config (uses default if omitted)'), -}; - -const PaginationParams = { - page: z.number().optional().describe('Page number (1-based)'), - limit: z.number().optional().describe('Items per page (max 50)'), -}; - -function pageQuery(params: { page?: number; limit?: number }): Record { - const q: Record = {}; - if (params.page !== undefined) q['page'] = String(params.page); - if (params.limit !== undefined) q['limit'] = String(params.limit); - return q; -} - -const OwnerRepo = { - owner: z.string().describe('Repository owner (user or org)'), - repo: z.string().describe('Repository name'), -}; - -export const server = new McpServer({ - name: '@mokoconsulting/mokogitea-mcp', - version: '1.1.0', -}); - -// ── User / Auth ───────────────────────────────────────────────────────── - -server.tool( - 'gitea_me', - 'Get the authenticated user info', - { ...ConnectionParam }, - async ({ connection }) => formatResponse(await clientFor(connection).get('/user')), -); - -server.tool( - 'gitea_user_orgs', - 'List organizations the authenticated user belongs to', - { ...PaginationParams, ...ConnectionParam }, - async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/user/orgs', pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_user_repos', - 'List repositories owned by the authenticated user', - { ...PaginationParams, ...ConnectionParam }, - async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/user/repos', pageQuery({ page, limit }))), -); - -// ── Repository CRUD ───────────────────────────────────────────────────── - -server.tool( - 'gitea_repo_get', - 'Get repository details', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}`)), -); - -server.tool( - 'gitea_repo_create', - 'Create a new repository', - { - name: z.string().describe('Repository name'), - org: z.string().optional().describe('Organization (omit for personal)'), - description: z.string().optional().describe('Description'), - private: z.boolean().optional().describe('Private repository'), - auto_init: z.boolean().optional().describe('Initialize with README'), - default_branch: z.string().optional().describe('Default branch (default "main")'), - template: z.boolean().optional().describe('Mark as template repository'), - ...ConnectionParam, - }, - async ({ name, org, description, private: priv, auto_init, default_branch, template: tmpl, connection }) => { - const client = clientFor(connection); - const body: Record = { name }; - if (description) body.description = description; - if (priv !== undefined) body.private = priv; - if (auto_init !== undefined) body.auto_init = auto_init; - if (default_branch) body.default_branch = default_branch; - if (tmpl !== undefined) body.template = tmpl; - const endpoint = org ? `/orgs/${org}/repos` : '/user/repos'; - return formatResponse(await client.post(endpoint, body)); - }, -); - -server.tool( - 'gitea_repo_delete', - 'Delete a repository', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}`)), -); - -server.tool( - 'gitea_repo_edit', - 'Edit repository settings', - { - ...OwnerRepo, - description: z.string().optional().describe('New description'), - private: z.boolean().optional().describe('Set private/public'), - has_issues: z.boolean().optional().describe('Enable issues'), - has_wiki: z.boolean().optional().describe('Enable wiki'), - has_pull_requests: z.boolean().optional().describe('Enable PRs'), - default_branch: z.string().optional().describe('Default branch'), - archived: z.boolean().optional().describe('Archive/unarchive'), - ...ConnectionParam, - }, - async ({ owner, repo, description, private: priv, has_issues, has_wiki, has_pull_requests, default_branch, archived, connection }) => { - const body: Record = {}; - if (description !== undefined) body.description = description; - if (priv !== undefined) body.private = priv; - if (has_issues !== undefined) body.has_issues = has_issues; - if (has_wiki !== undefined) body.has_wiki = has_wiki; - if (has_pull_requests !== undefined) body.has_pull_requests = has_pull_requests; - if (default_branch !== undefined) body.default_branch = default_branch; - if (archived !== undefined) body.archived = archived; - return formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}`, body)); - }, -); - -server.tool( - 'gitea_repo_fork', - 'Fork a repository', - { - ...OwnerRepo, - organization: z.string().optional().describe('Fork to this org (omit for personal)'), - name: z.string().optional().describe('Custom name for fork'), - ...ConnectionParam, - }, - async ({ owner, repo, organization, name, connection }) => { - const body: Record = {}; - if (organization) body.organization = organization; - if (name) body.name = name; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/forks`, body)); - }, -); - -server.tool( - 'gitea_repo_generate', - 'Create a new repository from a template repository', - { - template_owner: z.string().describe('Owner of the template repository'), - template_repo: z.string().describe('Name of the template repository'), - owner: z.string().describe('Target owner (user or org) for the new repo'), - name: z.string().describe('Name for the new repository'), - description: z.string().optional().describe('Description for the new repo'), - private: z.boolean().optional().describe('Make the new repo private'), - git_content: z.boolean().optional().describe('Copy git content (commits, branches) from template (default true)'), - topics: z.boolean().optional().describe('Copy topics from template'), - git_hooks: z.boolean().optional().describe('Copy git hooks from template'), - webhooks: z.boolean().optional().describe('Copy webhooks from template'), - labels: z.boolean().optional().describe('Copy labels from template'), - default_branch: z.string().optional().describe('Default branch for new repo'), - ...ConnectionParam, - }, - async ({ template_owner, template_repo, owner, name, description, private: priv, git_content, topics, git_hooks, webhooks, labels, default_branch, connection }) => { - const body: Record = { owner, name }; - if (description) body.description = description; - if (priv !== undefined) body.private = priv; - if (git_content !== undefined) body.git_content = git_content; - if (topics !== undefined) body.topics = topics; - if (git_hooks !== undefined) body.git_hooks = git_hooks; - if (webhooks !== undefined) body.webhooks = webhooks; - if (labels !== undefined) body.labels = labels; - if (default_branch) body.default_branch = default_branch; - return formatResponse(await clientFor(connection).post(`/repos/${template_owner}/${template_repo}/generate`, body)); - }, -); - -server.tool( - 'gitea_repo_search', - 'Search repositories', - { - q: z.string().describe('Search query'), - topic: z.boolean().optional().describe('Search in topics'), - sort: z.enum(['alpha', 'created', 'updated', 'size', 'stars', 'forks']).optional().describe('Sort field'), - order: z.enum(['asc', 'desc']).optional().describe('Sort order'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ q, topic, sort, order, page, limit, connection }) => { - const params: Record = { q, ...pageQuery({ page, limit }) }; - if (topic !== undefined) params['topic'] = String(topic); - if (sort) params['sort'] = sort; - if (order) params['order'] = order; - return formatResponse(await clientFor(connection).get('/repos/search', params)); - }, -); - -server.tool( - 'gitea_org_repos', - 'List repositories in an organization', - { - org: z.string().describe('Organization name'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/repos`, pageQuery({ page, limit }))), -); - -// ── File Contents ─────────────────────────────────────────────────────── - -server.tool( - 'gitea_file_get', - 'Get file contents from a repository', - { - ...OwnerRepo, - filepath: z.string().describe('File path (e.g. "src/index.ts")'), - ref: z.string().optional().describe('Branch/tag/commit (default: default branch)'), - ...ConnectionParam, - }, - async ({ owner, repo, filepath, ref, connection }) => { - const params: Record = {}; - if (ref) params['ref'] = ref; - return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/contents/${filepath}`, params)); - }, -); - -server.tool( - 'gitea_dir_get', - 'Get directory contents (file listing) from a repository', - { - ...OwnerRepo, - dirpath: z.string().optional().describe('Directory path (default: root)'), - ref: z.string().optional().describe('Branch/tag/commit'), - ...ConnectionParam, - }, - async ({ owner, repo, dirpath, ref, connection }) => { - const path = dirpath ? `/repos/${owner}/${repo}/contents/${dirpath}` : `/repos/${owner}/${repo}/contents`; - const params: Record = {}; - if (ref) params['ref'] = ref; - return formatResponse(await clientFor(connection).get(path, params)); - }, -); - -server.tool( - 'gitea_file_create_or_update', - 'Create or update a file in a repository', - { - ...OwnerRepo, - filepath: z.string().describe('File path'), - content: z.string().describe('File content (will be base64-encoded automatically)'), - message: z.string().describe('Commit message'), - branch: z.string().optional().describe('Branch (default: default branch)'), - sha: z.string().optional().describe('SHA of existing file (required for updates)'), - ...ConnectionParam, - }, - async ({ owner, repo, filepath, content, message, branch, sha, connection }) => { - const body: Record = { - content: Buffer.from(content).toString('base64'), - message, - }; - if (branch) body.branch = branch; - if (sha) body.sha = sha; - return formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/contents/${filepath}`, body)); - }, -); - -server.tool( - 'gitea_file_delete', - 'Delete a file from a repository', - { - ...OwnerRepo, - filepath: z.string().describe('File path to delete'), - sha: z.string().describe('SHA of file to delete'), - message: z.string().describe('Commit message'), - branch: z.string().optional().describe('Branch'), - ...ConnectionParam, - }, - async ({ owner, repo, filepath, sha, message, branch, connection }) => { - const client = clientFor(connection); - const body: Record = { sha, message }; - if (branch) body.branch = branch; - // Gitea DELETE with body needs special handling - return formatResponse(await client.post(`/repos/${owner}/${repo}/contents/${filepath}`, { ...body, _method: 'DELETE' })); - }, -); - -server.tool( - 'gitea_tree_get', - 'Get the git tree for a repository (recursive file listing)', - { - ...OwnerRepo, - sha: z.string().describe('Tree SHA or branch name'), - recursive: z.boolean().optional().describe('Recursive listing (default true)'), - ...ConnectionParam, - }, - async ({ owner, repo, sha, recursive, connection }) => { - const params: Record = {}; - if (recursive !== false) params['recursive'] = 'true'; - return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/git/trees/${sha}`, params)); - }, -); - -// ── Branches ──────────────────────────────────────────────────────────── - -server.tool( - 'gitea_branches_list', - 'List branches in a repository', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/branches`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_branch_get', - 'Get a specific branch', - { ...OwnerRepo, branch: z.string().describe('Branch name'), ...ConnectionParam }, - async ({ owner, repo, branch, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/branches/${branch}`)), -); - -server.tool( - 'gitea_branch_create', - 'Create a new branch', - { - ...OwnerRepo, - new_branch_name: z.string().describe('Name for new branch'), - old_branch_name: z.string().optional().describe('Source branch (default: default branch)'), - ...ConnectionParam, - }, - async ({ owner, repo, new_branch_name, old_branch_name, connection }) => { - const body: Record = { new_branch_name }; - if (old_branch_name) body.old_branch_name = old_branch_name; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/branches`, body)); - }, -); - -server.tool( - 'gitea_branch_delete', - 'Delete a branch', - { ...OwnerRepo, branch: z.string().describe('Branch name'), ...ConnectionParam }, - async ({ owner, repo, branch, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/branches/${branch}`)), -); - -// ── Commits ───────────────────────────────────────────────────────────── - -server.tool( - 'gitea_commits_list', - 'List commits in a repository', - { - ...OwnerRepo, - sha: z.string().optional().describe('Branch or commit SHA'), - path: z.string().optional().describe('Filter by file path'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ owner, repo, sha, path, page, limit, connection }) => { - const params: Record = { ...pageQuery({ page, limit }) }; - if (sha) params['sha'] = sha; - if (path) params['path'] = path; - return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/commits`, params)); - }, -); - -server.tool( - 'gitea_commit_get', - 'Get a specific commit', - { ...OwnerRepo, sha: z.string().describe('Commit SHA'), ...ConnectionParam }, - async ({ owner, repo, sha, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/git/commits/${sha}`)), -); - -// ── Issues ────────────────────────────────────────────────────────────── - -server.tool( - 'gitea_issues_list', - 'List issues in a repository', - { - ...OwnerRepo, - state: z.enum(['open', 'closed', 'all']).optional().describe('Issue state filter'), - type: z.enum(['issues', 'pulls']).optional().describe('Filter by type'), - labels: z.string().optional().describe('Comma-separated label names'), - milestones: z.string().optional().describe('Comma-separated milestone names'), - q: z.string().optional().describe('Search query'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ owner, repo, state, type, labels, milestones, q, page, limit, connection }) => { - const params: Record = { ...pageQuery({ page, limit }) }; - if (state) params['state'] = state; - if (type) params['type'] = type; - if (labels) params['labels'] = labels; - if (milestones) params['milestones'] = milestones; - if (q) params['q'] = q; - return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues`, params)); - }, -); - -server.tool( - 'gitea_issue_get', - 'Get a single issue by number', - { ...OwnerRepo, number: z.number().describe('Issue number'), ...ConnectionParam }, - async ({ owner, repo, number, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues/${number}`)), -); - -server.tool( - 'gitea_issue_create', - 'Create or update an issue (searches by title first to prevent duplicates)', - { - ...OwnerRepo, - title: z.string().describe('Issue title'), - body: z.string().optional().describe('Issue body (markdown)'), - labels: z.array(z.number()).optional().describe('Label IDs'), - milestone: z.number().optional().describe('Milestone ID'), - assignees: z.array(z.string()).optional().describe('Usernames to assign'), - status_id: z.number().optional().describe('Custom status definition ID'), - priority_id: z.number().optional().describe('Custom priority definition ID'), - type_id: z.number().optional().describe('Custom type definition ID'), - ...ConnectionParam, - }, - async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, type_id, connection }) => { - const c = clientFor(connection); - - // Search for existing issue with same title to prevent duplicates - const searchRes = await c.get(`/repos/${owner}/${repo}/issues`, { - type: 'issues', state: 'open', limit: '50', - }); - let existing: { number: number } | undefined; - if (searchRes.status < 400 && Array.isArray(searchRes.data)) { - existing = (searchRes.data as Array<{ number: number; title: string }>) - .find((i) => i.title === title); - } - // Also check closed issues if not found in open - if (!existing) { - const closedRes = await c.get(`/repos/${owner}/${repo}/issues`, { - type: 'issues', state: 'closed', limit: '50', - }); - if (closedRes.status < 400 && Array.isArray(closedRes.data)) { - existing = (closedRes.data as Array<{ number: number; title: string }>) - .find((i) => i.title === title); - } - } - - if (existing) { - // Update existing issue instead of creating a duplicate - const body: Record = {}; - if (issueBody !== undefined) body.body = issueBody; - if (labels) body.labels = labels; - if (milestone) body.milestone = milestone; - if (assignees) body.assignees = assignees; - const res = await c.patch(`/repos/${owner}/${repo}/issues/${existing.number}`, body); - // Set status/priority via dedicated endpoints - const issueData = res.data as { id?: number }; - if (issueData?.id) { - if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-status`, { status_id }); - if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-priority`, { priority_id }); - if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-type`, { type_id }); - } - const out = formatResponse(res); - out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`; - return out; - } - - // No duplicate found - create new - const body: Record = { title }; - if (issueBody) body.body = issueBody; - if (labels) body.labels = labels; - if (milestone) body.milestone = milestone; - if (assignees) body.assignees = assignees; - const res = await c.post(`/repos/${owner}/${repo}/issues`, body); - // Set status/priority via dedicated endpoints - const newIssue = res.data as { id?: number }; - if (newIssue?.id) { - if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-status`, { status_id }); - if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-priority`, { priority_id }); - if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-type`, { type_id }); - } - return formatResponse(res); - }, -); - -server.tool( - 'gitea_issue_update', - 'Update an issue', - { - ...OwnerRepo, - number: z.number().describe('Issue number'), - title: z.string().optional().describe('New title'), - body: z.string().optional().describe('New body'), - state: z.enum(['open', 'closed']).optional().describe('State'), - assignees: z.array(z.string()).optional().describe('Assignees'), - milestone: z.number().optional().describe('Milestone ID'), - ...ConnectionParam, - }, - async ({ owner, repo, number, title, body: issueBody, state, assignees, milestone, connection }) => { - const body: Record = {}; - if (title !== undefined) body.title = title; - if (issueBody !== undefined) body.body = issueBody; - if (state) body.state = state; - if (assignees) body.assignees = assignees; - if (milestone !== undefined) body.milestone = milestone; - return formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}/issues/${number}`, body)); - }, -); - -server.tool( - 'gitea_issue_comments_list', - 'List comments on an issue', - { ...OwnerRepo, number: z.number().describe('Issue number'), ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, number, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues/${number}/comments`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_issue_comment_create', - 'Add a comment to an issue', - { - ...OwnerRepo, - number: z.number().describe('Issue number'), - body: z.string().describe('Comment body (markdown)'), - ...ConnectionParam, - }, - async ({ owner, repo, number, body: commentBody, connection }) => { - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/issues/${number}/comments`, { body: commentBody })); - }, -); - -server.tool( - 'gitea_issue_search', - 'Search issues across all repositories', - { - q: z.string().describe('Search query'), - state: z.enum(['open', 'closed', 'all']).optional().describe('State filter'), - labels: z.string().optional().describe('Comma-separated label IDs'), - type: z.enum(['issues', 'pulls']).optional().describe('Filter type'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ q, state, labels, type, page, limit, connection }) => { - const params: Record = { q, ...pageQuery({ page, limit }) }; - if (state) params['state'] = state; - if (labels) params['labels'] = labels; - if (type) params['type'] = type; - return formatResponse(await clientFor(connection).get('/repos/search', params)); - }, -); - -// ── Labels ────────────────────────────────────────────────────────────── - -server.tool( - 'gitea_labels_list', - 'List labels in a repository', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/labels`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_label_create', - 'Create a label', - { - ...OwnerRepo, - name: z.string().describe('Label name'), - color: z.string().describe('Color hex (e.g. "#d73a4a")'), - description: z.string().optional().describe('Label description'), - ...ConnectionParam, - }, - async ({ owner, repo, name, color, description, connection }) => { - const body: Record = { name, color }; - if (description) body.description = description; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/labels`, body)); - }, -); - -// ── Milestones ────────────────────────────────────────────────────────── - -server.tool( - 'gitea_milestones_list', - 'List milestones in a repository', - { - ...OwnerRepo, - state: z.enum(['open', 'closed', 'all']).optional().describe('State filter'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ owner, repo, state, page, limit, connection }) => { - const params: Record = { ...pageQuery({ page, limit }) }; - if (state) params['state'] = state; - return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/milestones`, params)); - }, -); - -server.tool( - 'gitea_milestone_create', - 'Create a milestone', - { - ...OwnerRepo, - title: z.string().describe('Milestone title'), - description: z.string().optional().describe('Description'), - due_on: z.string().optional().describe('Due date (ISO 8601)'), - ...ConnectionParam, - }, - async ({ owner, repo, title, description, due_on, connection }) => { - const body: Record = { title }; - if (description) body.description = description; - if (due_on) body.due_on = due_on; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/milestones`, body)); - }, -); - -// ── Pull Requests ─────────────────────────────────────────────────────── - -server.tool( - 'gitea_pulls_list', - 'List pull requests', - { - ...OwnerRepo, - state: z.enum(['open', 'closed', 'all']).optional().describe('State filter'), - sort: z.enum(['oldest', 'recentupdate', 'leastupdate', 'mostcomment', 'leastcomment', 'priority']).optional(), - labels: z.string().optional().describe('Comma-separated label IDs'), - milestone: z.number().optional().describe('Milestone ID'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ owner, repo, state, sort, labels, milestone, page, limit, connection }) => { - const params: Record = { ...pageQuery({ page, limit }) }; - if (state) params['state'] = state; - if (sort) params['sort'] = sort; - if (labels) params['labels'] = labels; - if (milestone !== undefined) params['milestone'] = String(milestone); - return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/pulls`, params)); - }, -); - -server.tool( - 'gitea_pull_get', - 'Get a single pull request', - { ...OwnerRepo, number: z.number().describe('PR number'), ...ConnectionParam }, - async ({ owner, repo, number, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/pulls/${number}`)), -); - -server.tool( - 'gitea_pull_create', - 'Create a pull request', - { - ...OwnerRepo, - title: z.string().describe('PR title'), - head: z.string().describe('Source branch'), - base: z.string().describe('Target branch'), - body: z.string().optional().describe('PR description (markdown)'), - labels: z.array(z.number()).optional().describe('Label IDs'), - milestone: z.number().optional().describe('Milestone ID'), - assignees: z.array(z.string()).optional().describe('Assignees'), - ...ConnectionParam, - }, - async ({ owner, repo, title, head, base, body: prBody, labels, milestone, assignees, connection }) => { - const body: Record = { title, head, base }; - if (prBody) body.body = prBody; - if (labels) body.labels = labels; - if (milestone) body.milestone = milestone; - if (assignees) body.assignees = assignees; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/pulls`, body)); - }, -); - -server.tool( - 'gitea_pull_merge', - 'Merge a pull request', - { - ...OwnerRepo, - number: z.number().describe('PR number'), - merge_type: z.enum(['merge', 'rebase', 'squash', 'rebase-merge']).optional().describe('Merge method (default: merge)'), - title: z.string().optional().describe('Custom merge commit title'), - message: z.string().optional().describe('Custom merge commit message'), - delete_branch_after_merge: z.boolean().optional().describe('Delete head branch after merge'), - ...ConnectionParam, - }, - async ({ owner, repo, number, merge_type, title, message, delete_branch_after_merge, connection }) => { - const body: Record = { Do: merge_type ?? 'merge' }; - if (title) body.MergeTitleField = title; - if (message) body.MergeMessageField = message; - if (delete_branch_after_merge !== undefined) body.delete_branch_after_merge = delete_branch_after_merge; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/pulls/${number}/merge`, body)); - }, -); - -server.tool( - 'gitea_pull_files', - 'List files changed in a pull request', - { ...OwnerRepo, number: z.number().describe('PR number'), ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, number, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/pulls/${number}/files`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_pull_review_create', - 'Create a pull request review', - { - ...OwnerRepo, - number: z.number().describe('PR number'), - event: z.enum(['APPROVED', 'REQUEST_CHANGES', 'COMMENT']).describe('Review action'), - body: z.string().optional().describe('Review comment'), - ...ConnectionParam, - }, - async ({ owner, repo, number, event, body: reviewBody, connection }) => { - const body: Record = { event }; - if (reviewBody) body.body = reviewBody; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/pulls/${number}/reviews`, body)); - }, -); - -// ── Releases ──────────────────────────────────────────────────────────── - -server.tool( - 'gitea_releases_list', - 'List releases', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/releases`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_release_get', - 'Get a single release by ID', - { ...OwnerRepo, id: z.number().describe('Release ID'), ...ConnectionParam }, - async ({ owner, repo, id, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/releases/${id}`)), -); - -server.tool( - 'gitea_release_latest', - 'Get the latest release', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/releases/latest`)), -); - -server.tool( - 'gitea_release_create', - 'Create a new release', - { - ...OwnerRepo, - tag_name: z.string().describe('Tag name (e.g. "v1.0.0")'), - name: z.string().optional().describe('Release title'), - body: z.string().optional().describe('Release notes (markdown)'), - target_commitish: z.string().optional().describe('Target branch/commit'), - draft: z.boolean().optional().describe('Create as draft'), - prerelease: z.boolean().optional().describe('Mark as prerelease'), - ...ConnectionParam, - }, - async ({ owner, repo, tag_name, name, body: notes, target_commitish, draft, prerelease, connection }) => { - const body: Record = { tag_name }; - if (name) body.name = name; - if (notes) body.body = notes; - if (target_commitish) body.target_commitish = target_commitish; - if (draft !== undefined) body.draft = draft; - if (prerelease !== undefined) body.prerelease = prerelease; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/releases`, body)); - }, -); - -server.tool( - 'gitea_release_delete', - 'Delete a release', - { ...OwnerRepo, id: z.number().describe('Release ID'), ...ConnectionParam }, - async ({ owner, repo, id, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/releases/${id}`)), -); - -// ── Tags ──────────────────────────────────────────────────────────────── - -server.tool( - 'gitea_tags_list', - 'List tags', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/tags`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_tag_create', - 'Create a tag', - { - ...OwnerRepo, - tag_name: z.string().describe('Tag name'), - target: z.string().optional().describe('Target branch/commit SHA'), - message: z.string().optional().describe('Tag message (annotated tag)'), - ...ConnectionParam, - }, - async ({ owner, repo, tag_name, target, message, connection }) => { - const body: Record = { tag_name }; - if (target) body.target = target; - if (message) body.message = message; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/tags`, body)); - }, -); - -server.tool( - 'gitea_tag_delete', - 'Delete a tag', - { ...OwnerRepo, tag: z.string().describe('Tag name'), ...ConnectionParam }, - async ({ owner, repo, tag, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/tags/${tag}`)), -); - -// ── Actions (CI/CD) ───────────────────────────────────────────────────── - -server.tool( - 'gitea_actions_runs_list', - 'List workflow runs for a repository', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_actions_run_get', - 'Get a specific workflow run', - { ...OwnerRepo, run_id: z.number().describe('Run ID'), ...ConnectionParam }, - async ({ owner, repo, run_id, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs/${run_id}`)), -); - -server.tool( - 'gitea_actions_dispatch', - 'Trigger a workflow dispatch (e.g. pre-release, deploy)', - { - ...OwnerRepo, - workflow: z.string().describe('Workflow filename (e.g. pre-release.yml)'), - ref: z.string().describe('Branch or tag to run on (e.g. dev, main)'), - inputs: z.record(z.string()).optional().describe('Workflow input key-value pairs'), - ...ConnectionParam, - }, - async ({ owner, repo, workflow, ref, inputs, connection }) => - formatResponse(await clientFor(connection).post( - `/repos/${owner}/${repo}/actions/workflows/${workflow}/dispatches`, - { ref, inputs: inputs ?? {} }, - )), -); - -server.tool( - 'gitea_actions_jobs_list', - 'List jobs for a workflow run', - { ...OwnerRepo, run_id: z.number().describe('Run ID'), ...ConnectionParam }, - async ({ owner, repo, run_id, connection }) => - formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`)), -); - -server.tool( - 'gitea_actions_job_logs', - 'Get log output for a workflow job', - { ...OwnerRepo, job_id: z.number().describe('Job ID'), ...ConnectionParam }, - async ({ owner, repo, job_id, connection }) => { - const client = clientFor(connection); - const res = await client.get(`/repos/${owner}/${repo}/actions/jobs/${job_id}/logs`); - if (res.status >= 400) return formatResponse(res); - // Logs come as plain text - const text = typeof res.data === 'string' ? res.data : JSON.stringify(res.data); - return { content: [{ type: 'text' as const, text }] }; - }, -); - -server.tool( - 'gitea_release_asset_upload', - 'Upload a file as a release asset (provide base64-encoded content)', - { - ...OwnerRepo, - release_id: z.number().describe('Release ID'), - name: z.string().describe('Asset filename'), - content_base64: z.string().describe('Base64-encoded file content'), - ...ConnectionParam, - }, - async ({ owner, repo, release_id, name, content_base64, connection }) => { - const client = clientFor(connection); - // Gitea expects multipart form data for asset upload - // For now, use the API with the binary content - const res = await client.post( - `/repos/${owner}/${repo}/releases/${release_id}/assets?name=${encodeURIComponent(name)}`, - Buffer.from(content_base64, 'base64'), - ); - return formatResponse(res); - }, -); - -server.tool( - 'gitea_release_asset_delete', - 'Delete a release asset', - { - ...OwnerRepo, - release_id: z.number().describe('Release ID'), - asset_id: z.number().describe('Asset ID'), - ...ConnectionParam, - }, - async ({ owner, repo, release_id, asset_id, connection }) => - formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/releases/${release_id}/assets/${asset_id}`)), -); - -server.tool( - 'gitea_bulk_file_push', - 'Push the same file content to multiple repos (uses Contents API)', - { - owner: z.string().describe('Organization name'), - repos: z.array(z.string()).describe('List of repository names'), - path: z.string().describe('File path in each repo (e.g. .mokogitea/workflows/pre-release.yml)'), - content_base64: z.string().describe('Base64-encoded file content'), - message: z.string().describe('Commit message'), - branch: z.string().optional().describe('Target branch (default: main)'), - ...ConnectionParam, - }, - async ({ owner, repos, path, content_base64, message, branch, connection }) => { - const client = clientFor(connection); - const targetBranch = branch ?? 'main'; - const results: Array<{ repo: string; status: string }> = []; - - for (const repo of repos) { - try { - // Get current file SHA - const existing = await client.get(`/repos/${owner}/${repo}/contents/${path}?ref=${targetBranch}`); - const sha = (existing.data as { sha?: string })?.sha; - - if (sha) { - // Update existing file - await client.put(`/repos/${owner}/${repo}/contents/${path}`, { - content: content_base64, - sha, - message, - branch: targetBranch, - }); - results.push({ repo, status: 'updated' }); - } else { - // Create new file - await client.post(`/repos/${owner}/${repo}/contents/${path}`, { - content: content_base64, - message, - branch: targetBranch, - }); - results.push({ repo, status: 'created' }); - } - } catch (e) { - results.push({ repo, status: `error: ${e}` }); - } - } - - const summary = results.map(r => `${r.repo}: ${r.status}`).join('\n'); - return { content: [{ type: 'text' as const, text: summary }] }; - }, -); - -// ── Organizations ─────────────────────────────────────────────────────── - -server.tool( - 'gitea_org_get', - 'Get organization details', - { org: z.string().describe('Organization name'), ...ConnectionParam }, - async ({ org, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}`)), -); - -server.tool( - 'gitea_org_teams_list', - 'List teams in an organization', - { org: z.string().describe('Organization name'), ...PaginationParams, ...ConnectionParam }, - async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/teams`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_org_members_list', - 'List members of an organization', - { org: z.string().describe('Organization name'), ...PaginationParams, ...ConnectionParam }, - async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/members`, pageQuery({ page, limit }))), -); - -// ── Users ─────────────────────────────────────────────────────────────── - -server.tool( - 'gitea_user_get', - 'Get a user profile', - { username: z.string().describe('Username'), ...ConnectionParam }, - async ({ username, connection }) => formatResponse(await clientFor(connection).get(`/users/${username}`)), -); - -server.tool( - 'gitea_users_search', - 'Search users', - { - q: z.string().describe('Search query'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ q, page, limit, connection }) => formatResponse(await clientFor(connection).get('/users/search', { q, ...pageQuery({ page, limit }) })), -); - -// ── Webhooks ──────────────────────────────────────────────────────────── - -server.tool( - 'gitea_webhooks_list', - 'List webhooks for a repository', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/hooks`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_webhook_create', - 'Create a webhook', - { - ...OwnerRepo, - type: z.enum(['gitea', 'slack', 'discord', 'dingtalk', 'telegram', 'msteams', 'feishu', 'matrix', 'wechatwork', 'packagist']).describe('Hook type'), - url: z.string().describe('Webhook URL'), - events: z.array(z.string()).optional().describe('Events to listen for (e.g. ["push", "pull_request"])'), - active: z.boolean().optional().describe('Active status'), - ...ConnectionParam, - }, - async ({ owner, repo, type, url, events, active, connection }) => { - const body: Record = { - type, - config: { url, content_type: 'json' }, - events: events ?? ['push'], - active: active ?? true, - }; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/hooks`, body)); - }, -); - -// ── Wiki ──────────────────────────────────────────────────────────────── - -server.tool( - 'gitea_wiki_pages_list', - 'List wiki pages', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/wiki/pages`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_wiki_page_get', - 'Get a wiki page', - { ...OwnerRepo, page_name: z.string().describe('Page name/slug'), ...ConnectionParam }, - async ({ owner, repo, page_name, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/wiki/page/${page_name}`)), -); - -// ── Notifications ─────────────────────────────────────────────────────── - -server.tool( - 'gitea_notifications_list', - 'List notifications for the authenticated user', - { - status_types: z.string().optional().describe('Comma-separated: read, unread, pinned'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ status_types, page, limit, connection }) => { - const params: Record = { ...pageQuery({ page, limit }) }; - if (status_types) params['status-types'] = status_types; - return formatResponse(await clientFor(connection).get('/notifications', params)); - }, -); - -server.tool( - 'gitea_notifications_read', - 'Mark all notifications as read', - { ...ConnectionParam }, - async ({ connection }) => formatResponse(await clientFor(connection).put('/notifications', { })), -); - -// ── Topics ────────────────────────────────────────────────────────────── - -server.tool( - 'gitea_repo_topics', - 'Get topics (tags) for a repository', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/topics`)), -); - -server.tool( - 'gitea_repo_topics_set', - 'Set topics for a repository (replaces all existing)', - { - ...OwnerRepo, - topics: z.array(z.string()).describe('Array of topic names'), - ...ConnectionParam, - }, - async ({ owner, repo, topics, connection }) => formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/topics`, { topics })), -); - -server.tool( - 'gitea_topic_search', - 'Search topics across all repositories', - { - q: z.string().describe('Search query'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ q, page, limit, connection }) => formatResponse(await clientFor(connection).get('/topics/search', { q, ...pageQuery({ page, limit }) })), -); - -// ── Collaborators ─────────────────────────────────────────────────────── - -server.tool( - 'gitea_collaborators_list', - 'List collaborators on a repository', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/collaborators`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_collaborator_add', - 'Add a collaborator to a repository', - { - ...OwnerRepo, - collaborator: z.string().describe('Username to add'), - permission: z.enum(['read', 'write', 'admin']).optional().describe('Permission level (default: write)'), - ...ConnectionParam, - }, - async ({ owner, repo, collaborator, permission, connection }) => { - const body: Record = {}; - if (permission) body.permission = permission; - return formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/collaborators/${collaborator}`, body)); - }, -); - -server.tool( - 'gitea_collaborator_remove', - 'Remove a collaborator from a repository', - { - ...OwnerRepo, - collaborator: z.string().describe('Username to remove'), - ...ConnectionParam, - }, - async ({ owner, repo, collaborator, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/collaborators/${collaborator}`)), -); - -// ── Deploy Keys ───────────────────────────────────────────────────────── - -server.tool( - 'gitea_deploy_keys_list', - 'List deploy keys for a repository', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/keys`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_deploy_key_create', - 'Add a deploy key to a repository', - { - ...OwnerRepo, - title: z.string().describe('Key title'), - key: z.string().describe('SSH public key content'), - read_only: z.boolean().optional().describe('Read-only access (default: true)'), - ...ConnectionParam, - }, - async ({ owner, repo, title, key, read_only, connection }) => { - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/keys`, { - title, - key, - read_only: read_only ?? true, - })); - }, -); - -server.tool( - 'gitea_deploy_key_delete', - 'Remove a deploy key from a repository', - { - ...OwnerRepo, - id: z.number().describe('Deploy key ID'), - ...ConnectionParam, - }, - async ({ owner, repo, id, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/keys/${id}`)), -); - -// ── Branch Protection ─────────────────────────────────────────────────── - -server.tool( - 'gitea_branch_protections_list', - 'List branch protection rules for a repository', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/branch_protections`)), -); - -server.tool( - 'gitea_branch_protection_create', - 'Create a branch protection rule', - { - ...OwnerRepo, - branch_name: z.string().describe('Branch name or pattern (e.g. "main", "release/*")'), - enable_push: z.boolean().optional().describe('Allow push (default: true)'), - enable_push_whitelist: z.boolean().optional().describe('Enable push whitelist'), - push_whitelist_usernames: z.array(z.string()).optional().describe('Users allowed to push'), - require_signed_commits: z.boolean().optional().describe('Require signed commits'), - required_approvals: z.number().optional().describe('Required PR approvals (0 = none)'), - enable_status_check: z.boolean().optional().describe('Require status checks'), - status_check_contexts: z.array(z.string()).optional().describe('Required status check names'), - ...ConnectionParam, - }, - async ({ owner, repo, branch_name, enable_push, enable_push_whitelist, push_whitelist_usernames, require_signed_commits, required_approvals, enable_status_check, status_check_contexts, connection }) => { - const body: Record = { branch_name }; - if (enable_push !== undefined) body.enable_push = enable_push; - if (enable_push_whitelist !== undefined) body.enable_push_whitelist = enable_push_whitelist; - if (push_whitelist_usernames) body.push_whitelist_usernames = push_whitelist_usernames; - if (require_signed_commits !== undefined) body.require_signed_commits = require_signed_commits; - if (required_approvals !== undefined) body.required_approvals = required_approvals; - if (enable_status_check !== undefined) body.enable_status_check = enable_status_check; - if (status_check_contexts) body.status_check_contexts = status_check_contexts; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/branch_protections`, body)); - }, -); - -server.tool( - 'gitea_branch_protection_delete', - 'Delete a branch protection rule', - { - ...OwnerRepo, - name: z.string().describe('Branch protection rule name'), - ...ConnectionParam, - }, - async ({ owner, repo, name, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/branch_protections/${name}`)), -); - -// ── Organization Labels ───────────────────────────────────────────────── - -server.tool( - 'gitea_org_labels_list', - 'List labels for an organization (shared across repos)', - { - org: z.string().describe('Organization name'), - ...PaginationParams, - ...ConnectionParam, - }, - async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/labels`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_org_label_create', - 'Create an organization-level label', - { - org: z.string().describe('Organization name'), - name: z.string().describe('Label name'), - color: z.string().describe('Color hex (e.g. "#d73a4a")'), - description: z.string().optional().describe('Label description'), - ...ConnectionParam, - }, - async ({ org, name, color, description, connection }) => { - const body: Record = { name, color }; - if (description) body.description = description; - return formatResponse(await clientFor(connection).post(`/orgs/${org}/labels`, body)); - }, -); - -// ── Repository Secrets (Actions) ──────────────────────────────────────── - -server.tool( - 'gitea_repo_actions_secrets_list', - 'List Actions secrets for a repository (names only, values hidden)', - { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, - async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/secrets`, pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_repo_actions_secret_create', - 'Create or update an Actions secret', - { - ...OwnerRepo, - name: z.string().describe('Secret name'), - data: z.string().describe('Secret value'), - ...ConnectionParam, - }, - async ({ owner, repo, name, data, connection }) => formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/actions/secrets/${name}`, { data })), -); - -server.tool( - 'gitea_repo_actions_secret_delete', - 'Delete an Actions secret', - { - ...OwnerRepo, - name: z.string().describe('Secret name'), - ...ConnectionParam, - }, - async ({ owner, repo, name, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/actions/secrets/${name}`)), -); - -// ── Repo Transfer / Mirror ────────────────────────────────────────────── - -server.tool( - 'gitea_repo_mirror_sync', - 'Trigger a push mirror sync for a repository', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/mirror-sync`, {})), -); - -server.tool( - 'gitea_repo_mirrors_list', - 'List push mirrors for a repository', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/push_mirrors`)), -); - -server.tool( - 'gitea_repo_mirror_create', - 'Create a push mirror to GitHub (backup). Sets up automatic push mirroring from Gitea to a GitHub repo.', - { - ...OwnerRepo, - github_owner: z.string().optional().describe('GitHub org or user (defaults to config github.org)'), - github_repo: z.string().optional().describe('GitHub repo name (defaults to same as Gitea repo)'), - github_token: z.string().optional().describe('GitHub token (defaults to config github.token)'), - interval: z.string().optional().describe('Sync interval (e.g. "8h0m0s", default "8h0m0s")'), - ...ConnectionParam, - }, - async ({ owner, repo, github_owner, github_repo, github_token, interval, connection }) => { - const ghToken = github_token ?? config.github?.token; - const ghOwner = github_owner ?? config.github?.org; - if (!ghToken) return { content: [{ type: 'text' as const, text: 'Error: No GitHub token. Pass github_token or set github.token in config.' }] }; - if (!ghOwner) return { content: [{ type: 'text' as const, text: 'Error: No GitHub owner. Pass github_owner or set github.org in config.' }] }; - const ghRepo = github_repo ?? repo; - return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/push_mirrors`, { - remote_address: `https://github.com/${ghOwner}/${ghRepo}.git`, - remote_username: ghOwner, - remote_password: ghToken, - interval: interval ?? '8h0m0s', - sync_on_commit: true, - })); - }, -); - -server.tool( - 'gitea_repo_mirror_delete', - 'Delete a push mirror from a repository', - { - ...OwnerRepo, - mirror_name: z.string().describe('Push mirror remote name (from mirrors_list)'), - ...ConnectionParam, - }, - async ({ owner, repo, mirror_name, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/push_mirrors/${mirror_name}`)), -); - -server.tool( - 'gitea_repo_mirror_setup_github_backup', - 'One-step GitHub backup mirror setup: creates the GitHub repo (if needed) and configures push mirror. Requires a GitHub token with repo+org scope.', - { - ...OwnerRepo, - github_org: z.string().optional().describe('GitHub org (defaults to config github.org)'), - github_token: z.string().optional().describe('GitHub token (defaults to config github.token)'), - private: z.boolean().optional().describe('Make GitHub repo private (default true)'), - description: z.string().optional().describe('GitHub repo description'), - interval: z.string().optional().describe('Mirror sync interval (default "8h0m0s")'), - ...ConnectionParam, - }, - async ({ owner, repo, github_org, github_token, private: isPrivate, description, interval, connection }) => { - const ghToken = github_token ?? config.github?.token; - const ghOrg = github_org ?? config.github?.org; - if (!ghToken) return { content: [{ type: 'text' as const, text: 'Error: No GitHub token. Pass github_token or set github.token in config.' }] }; - if (!ghOrg) return { content: [{ type: 'text' as const, text: 'Error: No GitHub org. Pass github_org or set github.org in config.' }] }; - const client = clientFor(connection); - const results: string[] = []; - - // 1. Try to create the GitHub repo via GitHub API - const ghRes = await fetch(`https://api.github.com/orgs/${ghOrg}/repos`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${ghToken}`, - 'Accept': 'application/vnd.github+json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: repo, - private: isPrivate ?? true, - description: description ?? `Backup mirror of ${owner}/${repo} from MokoGitea`, - auto_init: false, - has_issues: false, - has_wiki: false, - has_projects: false, - }), - }); - const ghData = await ghRes.json(); - if (ghRes.status === 201) { - results.push(`GitHub repo created: ${ghOrg}/${repo}`); - } else if (ghRes.status === 422) { - results.push(`GitHub repo already exists: ${ghOrg}/${repo}`); - } else { - return { content: [{ type: 'text' as const, text: `GitHub repo creation failed (${ghRes.status}): ${JSON.stringify(ghData)}` }] }; - } - - // 2. Create push mirror on Gitea - const mirrorRes = await client.post(`/repos/${owner}/${repo}/push_mirrors`, { - remote_address: `https://github.com/${ghOrg}/${repo}.git`, - remote_username: ghOrg, - remote_password: ghToken, - interval: interval ?? '8h0m0s', - sync_on_commit: true, - }); - - if (mirrorRes.status >= 400) { - const err = mirrorRes.data as { message?: string }; - results.push(`Push mirror failed: ${err?.message ?? mirrorRes.status}`); - } else { - results.push(`Push mirror configured: sync every ${interval ?? '8h0m0s'}, sync on commit`); - } - - // 3. Trigger initial sync - await client.post(`/repos/${owner}/${repo}/mirror-sync`, {}); - results.push('Initial sync triggered'); - - return { content: [{ type: 'text' as const, text: results.join('\n') }] }; - }, -); - -server.tool( - 'gitea_repo_mirror_setup_github_backup_full', - 'Full GitHub backup: mirrors code repo + wiki repo to GitHub. Creates GitHub repo, sets up push mirrors for both code and wiki, triggers initial sync.', - { - ...OwnerRepo, - github_org: z.string().optional().describe('GitHub org (defaults to config github.org)'), - github_token: z.string().optional().describe('GitHub token (defaults to config github.token)'), - private: z.boolean().optional().describe('Make GitHub repo private (default true)'), - description: z.string().optional().describe('GitHub repo description'), - interval: z.string().optional().describe('Mirror sync interval (default "8h0m0s")'), - ...ConnectionParam, - }, - async ({ owner, repo, github_org, github_token, private: isPrivate, description, interval, connection }) => { - const ghToken = github_token ?? config.github?.token; - const ghOrg = github_org ?? config.github?.org; - if (!ghToken) return { content: [{ type: 'text' as const, text: 'Error: No GitHub token. Pass github_token or set github.token in config.' }] }; - if (!ghOrg) return { content: [{ type: 'text' as const, text: 'Error: No GitHub org. Pass github_org or set github.org in config.' }] }; - const client = clientFor(connection); - const syncInterval = interval ?? '8h0m0s'; - const results: string[] = []; - - // 1. Create GitHub repo (if needed) - const ghRes = await fetch(`https://api.github.com/orgs/${ghOrg}/repos`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${ghToken}`, - 'Accept': 'application/vnd.github+json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: repo, - private: isPrivate ?? true, - description: description ?? `Backup mirror of ${owner}/${repo} from MokoGitea`, - auto_init: false, - has_issues: false, - has_wiki: true, - has_projects: false, - }), - }); - const ghData = await ghRes.json(); - if (ghRes.status === 201) { - results.push(`GitHub repo created: ${ghOrg}/${repo}`); - } else if (ghRes.status === 422) { - results.push(`GitHub repo already exists: ${ghOrg}/${repo}`); - } else { - return { content: [{ type: 'text' as const, text: `GitHub repo creation failed (${ghRes.status}): ${JSON.stringify(ghData)}` }] }; - } - - // 2. Enable wiki on GitHub repo (in case it was disabled) - await fetch(`https://api.github.com/repos/${ghOrg}/${repo}`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${ghToken}`, - 'Accept': 'application/vnd.github+json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ has_wiki: true }), - }); - - // 3. Code push mirror - const codeRes = await client.post(`/repos/${owner}/${repo}/push_mirrors`, { - remote_address: `https://github.com/${ghOrg}/${repo}.git`, - remote_username: ghOrg, - remote_password: ghToken, - interval: syncInterval, - sync_on_commit: true, - }); - if (codeRes.status >= 400) { - const err = codeRes.data as { message?: string }; - results.push(`Code mirror: ${err?.message ?? `failed (${codeRes.status})`}`); - } else { - results.push(`Code mirror configured: sync every ${syncInterval}, sync on commit`); - } - - // 4. Wiki push mirror — Gitea wiki repos are at {repo}.wiki - // Check if wiki exists first - const wikiCheck = await client.get(`/repos/${owner}/${repo}/wiki/pages`); - if (wikiCheck.status < 400) { - const wikiRes = await client.post(`/repos/${owner}/${repo}/push_mirrors`, { - remote_address: `https://github.com/${ghOrg}/${repo}.wiki.git`, - remote_username: ghOrg, - remote_password: ghToken, - interval: syncInterval, - sync_on_commit: true, - }); - if (wikiRes.status >= 400) { - const err = wikiRes.data as { message?: string }; - results.push(`Wiki mirror: ${err?.message ?? `failed (${wikiRes.status})`}`); - } else { - results.push(`Wiki mirror configured: ${ghOrg}/${repo}.wiki.git`); - } - } else { - results.push('Wiki mirror skipped: no wiki pages found'); - } - - // 5. Trigger initial sync - await client.post(`/repos/${owner}/${repo}/mirror-sync`, {}); - results.push('Initial sync triggered'); - - return { content: [{ type: 'text' as const, text: results.join('\n') }] }; - }, -); - -// ── Repo Statistics ───────────────────────────────────────────────────── - -server.tool( - 'gitea_repo_languages', - 'Get language breakdown for a repository', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/languages`)), -); - -server.tool( - 'gitea_repo_contributors', - 'List contributors with commit counts', - { ...OwnerRepo, ...ConnectionParam }, - async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/contributors`)), -); - -// ── Issue Labels (Bulk) ───────────────────────────────────────────────── - -server.tool( - 'gitea_issue_labels_set', - 'Set labels on an issue (replaces existing)', - { - ...OwnerRepo, - number: z.number().describe('Issue/PR number'), - labels: z.array(z.number()).describe('Label IDs to set'), - ...ConnectionParam, - }, - async ({ owner, repo, number, labels, connection }) => formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/issues/${number}/labels`, { labels })), -); - -// ── Diff / Compare ────────────────────────────────────────────────────── - -server.tool( - 'gitea_compare', - 'Compare two branches, tags, or commits (returns diff stats)', - { - ...OwnerRepo, - base: z.string().describe('Base branch/tag/SHA'), - head: z.string().describe('Head branch/tag/SHA'), - ...ConnectionParam, - }, - async ({ owner, repo, base, head, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/compare/${base}...${head}`)), -); - -// ── Gitea Admin (Instance-Level) ──────────────────────────────────────── - -server.tool( - 'gitea_admin_orgs_list', - 'List all organizations (admin only)', - { ...PaginationParams, ...ConnectionParam }, - async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/admin/orgs', pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_admin_users_list', - 'List all users (admin only)', - { ...PaginationParams, ...ConnectionParam }, - async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/admin/users', pageQuery({ page, limit }))), -); - -server.tool( - 'gitea_admin_cron_list', - 'List cron tasks and their last run time (admin only)', - { ...ConnectionParam }, - async ({ connection }) => formatResponse(await clientFor(connection).get('/admin/cron')), -); - -server.tool( - 'gitea_admin_cron_run', - 'Trigger a cron task (admin only)', - { - task: z.string().describe('Cron task name (e.g. "repo_health_check", "resync_all_hooks", "repo_archive_cleanup")'), - ...ConnectionParam, - }, - async ({ task, connection }) => formatResponse(await clientFor(connection).post(`/admin/cron/${task}`, {})), -); - -// ── Generic API Call ──────────────────────────────────────────────────── - -server.tool( - 'gitea_api_request', - 'Make a raw API request to any Gitea v1 endpoint', - { - method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'), - endpoint: z.string().describe('API endpoint path (e.g. "/repos/owner/repo")'), - body: z.record(z.string(), z.unknown()).optional().describe('Request body'), - params: z.record(z.string(), z.string()).optional().describe('Query parameters'), - ...ConnectionParam, - }, - async ({ method, endpoint, body, params, connection }) => { - const client = clientFor(connection); - switch (method) { - case 'GET': return formatResponse(await client.get(endpoint, params)); - case 'POST': return formatResponse(await client.post(endpoint, body)); - case 'PUT': return formatResponse(await client.put(endpoint, body)); - case 'PATCH': return formatResponse(await client.patch(endpoint, body)); - case 'DELETE': return formatResponse(await client.delete(endpoint)); - } - }, -); - -// ── Custom Fields (MokoGitea extension) ───────────────────────────────── - -server.tool( - 'gitea_org_custom_fields_list', - 'List org-level custom field definitions', - { - ...ConnectionParam, - org: z.string().describe('Organization name'), - scope: z.enum(['issue', 'repo']).optional().describe('Filter by scope'), - }, - async ({ connection, org, scope }) => { - const res = await clientFor(connection).get(`/orgs/${org}/custom-fields`); - if (res.status < 400 && scope && Array.isArray(res.data)) { - res.data = (res.data as Array<{ scope: string }>).filter(f => f.scope === scope); - } - return formatResponse(res); - }, -); - -server.tool( - 'gitea_org_custom_field_create', - 'Create an org-level custom field definition', - { - ...ConnectionParam, - org: z.string().describe('Organization name'), - name: z.string(), - field_type: z.enum(['text', 'number', 'date', 'dropdown', 'checkbox', 'url']), - scope: z.enum(['issue', 'repo']).default('issue'), - description: z.string().optional(), - required: z.boolean().optional(), - sort_order: z.number().optional(), - options: z.string().optional().describe('JSON array for dropdown options'), - }, - async ({ connection, org, ...fields }) => - formatResponse(await clientFor(connection).post(`/orgs/${org}/custom-fields`, fields)), -); - -server.tool( - 'gitea_org_custom_field_delete', - 'Delete an org-level custom field and all its values', - { - ...ConnectionParam, - org: z.string().describe('Organization name'), - field_id: z.number(), - }, - async ({ connection, org, field_id }) => - formatResponse(await clientFor(connection).delete(`/orgs/${org}/custom-fields/${field_id}`)), -); - -server.tool( - 'gitea_issue_custom_fields_get', - 'Get custom field values for an issue', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - number: z.number().describe('Issue number'), - }, - async ({ connection, owner, repo, number }) => - formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues/${number}/custom-fields`)), -); - -server.tool( - 'gitea_issue_custom_fields_set', - 'Set custom field values on an issue (map of field_id to value)', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - number: z.number().describe('Issue number'), - values: z.record(z.string(), z.string()).describe('Map of field_id (string) to value, e.g. {"11": "Pending Testing"}'), - }, - async ({ connection, owner, repo, number, values }) => - formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/issues/${number}/custom-fields`, values)), -); - -server.tool( - 'gitea_issue_bulk_set_status', - 'Set the Status custom field on multiple issues at once', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - issue_numbers: z.array(z.number()).describe('Issue numbers'), - status: z.string().describe('Status value (e.g. "Pending Testing", "In Progress", "Complete")'), - field_id: z.number().default(11).describe('Status field ID (default 11)'), - }, - async ({ connection, owner, repo, issue_numbers, status, field_id }) => { - const client = clientFor(connection); - const results: string[] = []; - for (const num of issue_numbers) { - const res = await client.put(`/repos/${owner}/${repo}/issues/${num}/custom-fields`, { [String(field_id)]: status }); - results.push(`#${num}: ${res.status < 300 ? 'ok' : 'failed'}`); - } - return { content: [{ type: 'text' as const, text: results.join('\n') }] }; - }, -); - -// ── Project Boards (MokoGitea extension) ──────────────────────────────── - -server.tool( - 'gitea_project_list', - 'List all projects for a repo', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - state: z.enum(['open', 'closed', 'all']).optional(), - }, - async ({ connection, owner, repo, state }) => - formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/projects`, { state: state ?? 'open' })), -); - -server.tool( - 'gitea_project_create', - 'Create a new project board', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - title: z.string(), - description: z.string().optional(), - board_type: z.number().optional().describe('0=none, 1=basic kanban, 2=bug triage'), - }, - async ({ connection, owner, repo, title, description, board_type }) => - formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/projects`, { - title, description: description ?? '', board_type: board_type ?? 1, - })), -); - -server.tool( - 'gitea_project_get', - 'Get project details', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - id: z.number().describe('Project ID'), - }, - async ({ connection, owner, repo, id }) => - formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/projects/${id}`)), -); - -server.tool( - 'gitea_project_update', - 'Update project title/description', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - id: z.number(), - title: z.string().optional(), - description: z.string().optional(), - }, - async ({ connection, owner, repo, id, title, description }) => - formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}/projects/${id}`, { - ...(title !== undefined && { title }), ...(description !== undefined && { description }), - })), -); - -server.tool( - 'gitea_project_delete', - 'Delete a project', - { ...ConnectionParam, owner: z.string(), repo: z.string(), id: z.number() }, - async ({ connection, owner, repo, id }) => - formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/projects/${id}`)), -); - -server.tool( - 'gitea_project_columns_list', - 'List columns in a project', - { ...ConnectionParam, owner: z.string(), repo: z.string(), project_id: z.number() }, - async ({ connection, owner, repo, project_id }) => - formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/projects/${project_id}/columns`)), -); - -server.tool( - 'gitea_project_column_create', - 'Add a column to a project', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - project_id: z.number(), - title: z.string(), - color: z.string().optional().describe('Hex color e.g. #ff0000'), - }, - async ({ connection, owner, repo, project_id, title, color }) => - formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/projects/${project_id}/columns`, { title, color: color ?? '' })), -); - -server.tool( - 'gitea_project_column_delete', - 'Delete a column from a project', - { ...ConnectionParam, owner: z.string(), repo: z.string(), project_id: z.number(), column_id: z.number() }, - async ({ connection, owner, repo, project_id, column_id }) => - formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/projects/${project_id}/columns/${column_id}`)), -); - -server.tool( - 'gitea_project_cards_list', - 'List issues in a project column', - { ...ConnectionParam, owner: z.string(), repo: z.string(), project_id: z.number(), column_id: z.number() }, - async ({ connection, owner, repo, project_id, column_id }) => - formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/projects/${project_id}/columns/${column_id}/issues`)), -); - -server.tool( - 'gitea_project_card_add', - 'Add an issue to a project column', - { ...ConnectionParam, owner: z.string(), repo: z.string(), project_id: z.number(), column_id: z.number(), issue_id: z.number() }, - async ({ connection, owner, repo, project_id, column_id, issue_id }) => - formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/projects/${project_id}/columns/${column_id}/issues`, { issue_id })), -); - -server.tool( - 'gitea_project_card_move', - 'Move an issue to a different column', - { - ...ConnectionParam, - owner: z.string(), - repo: z.string(), - project_id: z.number(), - issue_id: z.number(), - column_id: z.number().describe('Target column ID'), - sorting: z.number().optional().describe('Sort position'), - }, - async ({ connection, owner, repo, project_id, issue_id, column_id, sorting }) => - formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}/projects/${project_id}/issues/${issue_id}/move`, { - column_id, sorting: sorting ?? 0, - })), -); - -server.tool( - 'gitea_project_card_remove', - 'Remove an issue from a project', - { ...ConnectionParam, owner: z.string(), repo: z.string(), project_id: z.number(), issue_id: z.number() }, - async ({ connection, owner, repo, project_id, issue_id }) => - formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/projects/${project_id}/issues/${issue_id}`)), -); - -server.tool( - 'gitea_project_overview', - 'Full board overview — all columns with their issues', - { ...ConnectionParam, owner: z.string(), repo: z.string(), project_id: z.number() }, - async ({ connection, owner, repo, project_id }) => { - const client = clientFor(connection); - const proj = await client.get(`/repos/${owner}/${repo}/projects/${project_id}`); - if (proj.status >= 400) return formatResponse(proj); - const cols = await client.get(`/repos/${owner}/${repo}/projects/${project_id}/columns`); - if (cols.status >= 400) return formatResponse(cols); - const colData = Array.isArray(cols.data) ? cols.data as Array<{ id: number; title: string }> : []; - const overview: Array<{ column: string; issues: unknown[] }> = []; - for (const col of colData) { - const cards = await client.get(`/repos/${owner}/${repo}/projects/${project_id}/columns/${col.id}/issues`); - overview.push({ column: col.title, issues: Array.isArray(cards.data) ? cards.data : [] }); - } - return { content: [{ type: 'text' as const, text: JSON.stringify({ project: proj.data, columns: overview }, null, 2) }] }; - }, -); - -// ── Connections ────────────────────────────────────────────────────────── - -server.tool( - 'gitea_list_connections', - 'List configured Gitea connections', - {}, - async () => { - const lines = Object.entries(config.connections).map(([name, conn]) => { - const is_default = name === config.defaultConnection ? ' (default)' : ''; - return ` ${name}${is_default}: ${conn.baseUrl}`; - }); - return { content: [{ type: 'text' as const, text: `Configured connections:\n${lines.join('\n')}` }] }; - }, -); - -// ── Manifest ──────────────────────────────────────────────────────────── - -server.tool( - 'gitea_manifest_get', - 'Get manifest settings for a repository (identity, governance, build metadata)', - { - owner: z.string().describe('Repository owner'), - repo: z.string().describe('Repository name'), - ...ConnectionParam, - }, - async ({ owner, repo, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.get(`/repos/${owner}/${repo}/manifest`)); - }, -); - -server.tool( - 'gitea_manifest_update', - 'Update manifest settings for a repository', - { - owner: z.string().describe('Repository owner'), - repo: z.string().describe('Repository name'), - name: z.string().optional().describe('Project name'), - org: z.string().optional().describe('Organization'), - description: z.string().optional().describe('Project description'), - version: z.string().optional().describe('Version string (e.g. 06.00.00)'), - license_spdx: z.string().optional().describe('SPDX license identifier'), - license_name: z.string().optional().describe('Human-readable license name'), - platform: z.string().optional().describe('Platform (go, php, node, python, etc.)'), - standards_version: z.string().optional().describe('moko-platform standards version'), - standards_source: z.string().optional().describe('URL to standards repo'), - language: z.string().optional().describe('Primary language'), - package_type: z.string().optional().describe('Package type (application, library, module, etc.)'), - entry_point: z.string().optional().describe('Build entry point path'), - ...ConnectionParam, - }, - async ({ owner, repo, connection, ...fields }) => { - const c = clientFor(connection); - const current = await c.get(`/repos/${owner}/${repo}/manifest`); - const merged = { ...(current.data as Record) }; - for (const [k, v] of Object.entries(fields)) { - if (v !== undefined) merged[k] = v; - } - return formatResponse(await c.put(`/repos/${owner}/${repo}/manifest`, merged)); - }, -); - -// ── Issue Statuses (org-level) ────────────────────────────────────────── - -server.tool( - 'gitea_org_issue_statuses_list', - 'List custom issue status definitions for an organization', - { - org: z.string().describe('Organization name'), - ...ConnectionParam, - }, - async ({ org, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.get(`/orgs/${org}/issue-statuses`)); - }, -); - -server.tool( - 'gitea_issue_set_status', - 'Set custom status on an issue', - { - owner: z.string().describe('Repository owner'), - repo: z.string().describe('Repository name'), - issue_id: z.number().describe('Internal issue ID'), - status_id: z.number().describe('Status definition ID (0 to clear)'), - ...ConnectionParam, - }, - async ({ owner, repo, issue_id, status_id, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.post(`/repos/${owner}/${repo}/issues/${issue_id}/custom-status`, { status_id })); - }, -); - -// ── Issue Priorities (org-level) ──────────────────────────────────────── - -server.tool( - 'gitea_org_issue_priorities_list', - 'List custom issue priority definitions for an organization', - { - org: z.string().describe('Organization name'), - ...ConnectionParam, - }, - async ({ org, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.get(`/orgs/${org}/issue-priorities`)); - }, -); - -server.tool( - 'gitea_issue_set_priority', - 'Set custom priority on an issue', - { - owner: z.string().describe('Repository owner'), - repo: z.string().describe('Repository name'), - issue_id: z.number().describe('Internal issue ID'), - priority_id: z.number().describe('Priority definition ID (0 to clear)'), - ...ConnectionParam, - }, - async ({ owner, repo, issue_id, priority_id, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.post(`/repos/${owner}/${repo}/issues/${issue_id}/custom-priority`, { priority_id })); - }, -); - -// ── Issue Types (org-level) ────────────────────────────────────────────── - -server.tool( - 'gitea_org_issue_types_list', - 'List custom issue type definitions for an organization', - { - org: z.string().describe('Organization name'), - ...ConnectionParam, - }, - async ({ org, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.get(`/orgs/${org}/issue-types`)); - }, -); - -server.tool( - 'gitea_issue_set_type', - 'Set custom type on an issue', - { - owner: z.string().describe('Repository owner'), - repo: z.string().describe('Repository name'), - issue_id: z.number().describe('Internal issue ID'), - type_id: z.number().describe('Type definition ID (0 to clear)'), - ...ConnectionParam, - }, - async ({ owner, repo, issue_id, type_id, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.post(`/repos/${owner}/${repo}/issues/${issue_id}/custom-type`, { type_id })); - }, -); - -// ── Security ──────────────────────────────────────────────────────────── - -server.tool( - 'gitea_security_alerts', - 'List security alerts for a repository', - { - ...OwnerRepo, - ...ConnectionParam, - }, - async ({ owner, repo, connection }) => { - const c = clientFor(connection); - return formatResponse(await c.get(`/repos/${owner}/${repo}/security/alerts`)); - }, -); - -// ── Start Server ──────────────────────────────────────────────────────── - -async function main(): Promise { - config = await loadConfig(); - const transport = new StdioServerTransport(); - await server.connect(transport); -} - -main().catch((err) => { - process.stderr.write(`Fatal: ${err}\n`); - process.exit(1); -}); diff --git a/.mokogitea/mcp/src/server.ts b/.mokogitea/mcp/src/server.ts deleted file mode 100644 index 79dac8c7fb..0000000000 --- a/.mokogitea/mcp/src/server.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2026 Moko Consulting -// SPDX-License-Identifier: GPL-3.0-or-later -// -// Creates a configured MCP server instance for use by both stdio and SSE transports. - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { GiteaConfig } from './types.js'; - -// Import index.ts to register all tools on its exported `server` singleton, -// then re-export a factory that initializes config and returns the server. -import { server, initConfig } from './index.js'; - -export function createMcpServer(cfg: GiteaConfig): McpServer { - initConfig(cfg); - return server; -} diff --git a/.mokogitea/mcp/src/sse.ts b/.mokogitea/mcp/src/sse.ts deleted file mode 100644 index f13c0b1309..0000000000 --- a/.mokogitea/mcp/src/sse.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2026 Moko Consulting -// SPDX-License-Identifier: GPL-3.0-or-later -// -// SSE transport entry point for MokoGitea MCP server. -// Run with: node dist/sse.js -// Or: GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx node dist/sse.js -// -// Listens on PORT (default 3100) and serves SSE at /sse with POST at /message. - -import { createServer } from 'node:http'; -import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; -import { createMcpServer } from './server.js'; -import { loadConfig } from './config.js'; - -const PORT = parseInt(process.env.PORT ?? '3100', 10); - -async function main(): Promise { - const config = await loadConfig(); - const transports = new Map(); - - const httpServer = createServer(async (req, res) => { - // CORS headers for browser clients - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - if (req.method === 'OPTIONS') { - res.writeHead(204); - res.end(); - return; - } - - // Health check - if (req.url === '/health') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok', tools: 120 })); - return; - } - - // SSE endpoint - client connects here - if (req.url === '/sse' && req.method === 'GET') { - const transport = new SSEServerTransport('/message', res); - const sessionId = transport.sessionId; - transports.set(sessionId, transport); - - const server = createMcpServer(config); - await server.connect(transport); - - req.on('close', () => { - transports.delete(sessionId); - }); - return; - } - - // Message endpoint - client sends tool calls here - if (req.url?.startsWith('/message') && req.method === 'POST') { - const url = new URL(req.url, `http://${req.headers.host}`); - const sessionId = url.searchParams.get('sessionId'); - if (!sessionId || !transports.has(sessionId)) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Invalid or missing sessionId' })); - return; - } - const transport = transports.get(sessionId)!; - await transport.handlePostMessage(req, res); - return; - } - - // Root - info page - if (req.url === '/' || req.url === '') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - name: '@mokoconsulting/mokogitea-mcp', - version: '1.1.0', - description: 'MCP server for Gitea and MokoGitea - 120+ tools', - endpoints: { - sse: '/sse', - message: '/message', - health: '/health', - }, - docs: 'https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api', - })); - return; - } - - res.writeHead(404); - res.end('Not found'); - }); - - httpServer.listen(PORT, () => { - process.stderr.write(`MokoGitea MCP SSE server listening on port ${PORT}\n`); - process.stderr.write(` SSE: http://localhost:${PORT}/sse\n`); - process.stderr.write(` Health: http://localhost:${PORT}/health\n`); - }); -} - -main().catch((err) => { - process.stderr.write(`Fatal: ${err}\n`); - process.exit(1); -}); diff --git a/.mokogitea/mcp/src/types.ts b/.mokogitea/mcp/src/types.ts deleted file mode 100644 index 7502a58896..0000000000 --- a/.mokogitea/mcp/src/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* Copyright (C) 2026 Moko Consulting - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: gitea-api-mcp.Types - * INGROUP: gitea-api-mcp - * REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp - * PATH: /src/types.ts - * VERSION: 01.00.00 - * BRIEF: TypeScript type definitions for Gitea API MCP server - */ - -export interface GiteaConnection { - baseUrl: string; - token: string; - /** Skip TLS certificate verification (self-signed certs) */ - insecure?: boolean; -} - -export interface GitHubBackupConfig { - token: string; - org: string; -} - -export interface GiteaConfig { - connections: Record; - defaultConnection: string; - github?: GitHubBackupConfig; -} - -export interface ApiResponse { - status: number; - data: unknown; -} diff --git a/.mokogitea/mcp/tsconfig.json b/.mokogitea/mcp/tsconfig.json deleted file mode 100644 index 0dd168c019..0000000000 --- a/.mokogitea/mcp/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/mcp-mokogitea-api b/mcp-mokogitea-api new file mode 160000 index 0000000000..bc8bc0b53a --- /dev/null +++ b/mcp-mokogitea-api @@ -0,0 +1 @@ +Subproject commit bc8bc0b53a8c14316451c914984e7dc8fe6b7e48