diff --git a/.mokogitea/mcp/CHANGELOG.md b/.mokogitea/mcp/CHANGELOG.md new file mode 100644 index 0000000000..394ce04ab8 --- /dev/null +++ b/.mokogitea/mcp/CHANGELOG.md @@ -0,0 +1,129 @@ + + +# 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/README.md b/.mokogitea/mcp/README.md new file mode 100644 index 0000000000..74e7908376 --- /dev/null +++ b/.mokogitea/mcp/README.md @@ -0,0 +1,286 @@ + + +# gitea-api-mcp + +[![License: GPL-3.0-or-later](https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![MCP](https://img.shields.io/badge/MCP-compatible-brightgreen.svg)](https://modelcontextprotocol.io) +[![Node](https://img.shields.io/badge/node-%3E%3D20.0.0-green.svg)](https://nodejs.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org) + +> MCP server for Gitea REST API v1 operations -- 61 tools for complete Gitea instance management from Claude Code and other MCP clients. + +## Table of Contents + +- [Background](#background) +- [Install](#install) +- [Configuration](#configuration) +- [Usage](#usage) +- [Tools](#tools) +- [Contributing](#contributing) +- [License](#license) +- [Revision History](#revision-history) + +## Background + +`gitea-api-mcp` is a Model Context Protocol (MCP) server that exposes 61 tools for interacting with the Gitea REST API v1. It supports multiple named connections, allowing you to manage several Gitea instances from a single server. Authentication uses Gitea's native `Authorization: token` header format. + +## Install + +### Prerequisites + +- Node.js >= 20.0.0 +- A Gitea instance with API access +- A Gitea access token (Settings > Applications > Generate Token) + +### Build from Source + +```bash +git clone https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp.git +cd gitea-api-mcp +npm install +npm run build +``` + +## Configuration + +Create `~/.gitea-api-mcp.json`: + +```json +{ + "defaultConnection": "moko", + "connections": { + "moko": { + "baseUrl": "https://git.mokoconsulting.tech", + "token": "your-gitea-access-token", + "insecure": false + } + } +} +``` + +### Config Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `baseUrl` | string | Yes | Base URL of your Gitea instance | +| `token` | string | Yes | Gitea API access token | +| `insecure` | boolean | No | Skip TLS verification (self-signed certs) | + +Override the config path with the `GITEA_API_MCP_CONFIG` environment variable. + +### Multi-Connection Example + +```json +{ + "defaultConnection": "moko", + "connections": { + "moko": { + "baseUrl": "https://git.mokoconsulting.tech", + "token": "token-for-moko-gitea" + }, + "github-mirror": { + "baseUrl": "https://gitea.example.com", + "token": "token-for-mirror" + } + } +} +``` + +## Usage + +### Claude Code Registration + +Add to your Claude Code MCP config (`~/.claude/claude_desktop_config.json` or project-level `.mcp.json`): + +```json +{ + "mcpServers": { + "gitea-moko": { + "command": "node", + "args": ["/path/to/gitea-api-mcp/dist/index.js"] + } + } +} +``` + +### Multi-Connection Usage in Claude Code + +When using multiple connections, pass the `connection` parameter to any tool: + +``` +Use gitea_repo_get with connection "github-mirror" to get owner/repo details. +``` + +If `connection` is omitted, the `defaultConnection` is used. + +## Tools + +### User / Auth (3 tools) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `gitea_commits_list` | List commits in a repository | +| `gitea_commit_get` | Get a specific commit | + +### Issues (7 tools) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `gitea_labels_list` | List labels in a repository | +| `gitea_label_create` | Create a label | + +### Milestones (2 tools) + +| Tool | Description | +|------|-------------| +| `gitea_milestones_list` | List milestones in a repository | +| `gitea_milestone_create` | Create a milestone | + +### Pull Requests (6 tools) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `gitea_tags_list` | List tags | +| `gitea_tag_create` | Create a tag | +| `gitea_tag_delete` | Delete a tag | + +### Actions (2 tools) + +| Tool | Description | +|------|-------------| +| `gitea_actions_runs_list` | List workflow runs for a repository | +| `gitea_actions_run_get` | Get a specific workflow run | + +### Organizations (3 tools) + +| Tool | Description | +|------|-------------| +| `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) + +| Tool | Description | +|------|-------------| +| `gitea_user_get` | Get a user profile | +| `gitea_users_search` | Search users | + +### Webhooks (2 tools) + +| Tool | Description | +|------|-------------| +| `gitea_webhooks_list` | List webhooks for a repository | +| `gitea_webhook_create` | Create a webhook | + +### Wiki (2 tools) + +| Tool | Description | +|------|-------------| +| `gitea_wiki_pages_list` | List wiki pages | +| `gitea_wiki_page_get` | Get a wiki page | + +### Notifications (2 tools) + +| Tool | Description | +|------|-------------| +| `gitea_notifications_list` | List notifications for the authenticated user | +| `gitea_notifications_read` | Mark all notifications as read | + +### Generic (2 tools) + +| Tool | Description | +|------|-------------| +| `gitea_api_request` | Make a raw API request to any Gitea v1 endpoint | +| `gitea_list_connections` | List configured Gitea connections | + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines. + +## License + +[GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html) -- Copyright (C) 2026 Moko Consulting + +## Revision History + +| Version | Date | Description | +|---------|------|-------------| +| 0.0.1 | 2026-05-07 | Initial release with 61 tools | diff --git a/.mokogitea/mcp/config.example.json b/.mokogitea/mcp/config.example.json new file mode 100644 index 0000000000..763922d4be --- /dev/null +++ b/.mokogitea/mcp/config.example.json @@ -0,0 +1,13 @@ +{ + "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 new file mode 100644 index 0000000000..be4e013711 --- /dev/null +++ b/.mokogitea/mcp/package-lock.json @@ -0,0 +1,1198 @@ +{ + "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 new file mode 100644 index 0000000000..298a848046 --- /dev/null +++ b/.mokogitea/mcp/package.json @@ -0,0 +1,34 @@ +{ + "name": "@mokoconsulting/mcp-mokogitea-api", + "version": "1.0.0", + "description": "MCP server for Gitea REST API v1 operations", + "type": "module", + "main": "dist/index.js", + "bin": { + "mokogitea-api-mcp": "dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js", + "setup": "node scripts/setup.mjs", + "clean": "rm -rf dist/" + }, + "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 ", + "repository": { + "type": "git", + "url": "https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp.git" + } +} diff --git a/.mokogitea/mcp/profile.ps1 b/.mokogitea/mcp/profile.ps1 new file mode 100644 index 0000000000..d1c1b8ee14 --- /dev/null +++ b/.mokogitea/mcp/profile.ps1 @@ -0,0 +1,15 @@ +# 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 new file mode 100644 index 0000000000..125fa068d0 --- /dev/null +++ b/.mokogitea/mcp/scripts/setup.mjs @@ -0,0 +1,40 @@ +#!/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 new file mode 100644 index 0000000000..ec1e8ffbfc --- /dev/null +++ b/.mokogitea/mcp/src/client.ts @@ -0,0 +1,120 @@ +/* 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 new file mode 100644 index 0000000000..7ae522e73a --- /dev/null +++ b/.mokogitea/mcp/src/config.ts @@ -0,0 +1,58 @@ +/* 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.Config + * INGROUP: gitea-api-mcp + * REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp + * PATH: /src/config.ts + * VERSION: 01.00.00 + * BRIEF: Configuration loader for Gitea API MCP connections + */ + +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 { + 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` + + `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 new file mode 100644 index 0000000000..4782d9f0c3 --- /dev/null +++ b/.mokogitea/mcp/src/index.ts @@ -0,0 +1,1965 @@ +#!/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'; + +let config: GiteaConfig; + +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'), +}; + +const server = new McpServer({ + name: 'mokogitea-api-mcp', + version: '1.0.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 a new issue', + { + ...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'), + ...ConnectionParam, + }, + async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, connection }) => { + const body: Record = { title }; + if (issueBody) body.body = issueBody; + if (labels) body.labels = labels; + if (milestone) body.milestone = milestone; + if (assignees) body.assignees = assignees; + return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/issues`, body)); + }, +); + +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 })); + }, +); + +// ── 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/types.ts b/.mokogitea/mcp/src/types.ts new file mode 100644 index 0000000000..7502a58896 --- /dev/null +++ b/.mokogitea/mcp/src/types.ts @@ -0,0 +1,37 @@ +/* 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 new file mode 100644 index 0000000000..0dd168c019 --- /dev/null +++ b/.mokogitea/mcp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "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"] +}