12 Commits
v0.4.0 ... main

Author SHA1 Message Date
appleboy
058d4cd07f feat(PR): add tooling and docs for managing pull request reviewers (#103)
- Add support for creating pull request reviewers through a new tool and handler
- Document the new tool for adding reviewers to a pull request in English, Simplified Chinese, and Traditional Chinese READMEs

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/103
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-10-31 13:49:56 +00:00
appleboy
ba64780c2f chore: upgrade Go dependencies to latest stable versions
- Update version of code.gitea.io/sdk/gitea to v0.22.1
- Upgrade github.com/mark3labs/mcp-go dependency to v0.42.0
- Bump golang.org/x/crypto to v0.43.0 and golang.org/x/sys to v0.37.0

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-10-31 20:56:12 +08:00
Bo-Yi Wu
6930c8ee30 docs: refine and unify Chinese README documentation and localization
- Improve clarity and conciseness of installation and configuration instructions in both Simplified and Traditional Chinese README files
- Standardize terminology and phrasing for build, installation, and usage steps
- Refine and unify tool/function descriptions in the feature tables for greater consistency and accuracy
- Update troubleshooting steps for brevity and clarity
- Enhance overall readability and localization quality throughout both documents

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-10-20 14:08:20 +08:00
Bo-Yi Wu
f08720a625 docs: document organization and Wiki management tools
- Add documentation for organization-level label management tools
- Add documentation for Wiki page management tools
- Fix a translation for the word "issue" in the Traditional Chinese README

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-10-20 14:06:28 +08:00
Daniel
f6b45fdf6e feat(org): add MCP tools for organization-level labels (list/create/edit/delete) (#99) (#102)
This PR adds full support for managing organization-level labels via MCP. It uses the newly added SDK APIs now available on main branch, see https://gitea.com/gitea/go-sdk/issues/732.

Registers following tools under label module and wires them into the MCP server as read/write tools:

- list_org_labels: list labels defined at the organization level (pagination supported)
- create_org_label: create a label in an organization (name, color, description, exclusive)
- edit_org_label: edit an organization label (name, color, description, exclusive)
- delete_org_label: delete an organization label by ID

Dependency note:
go.mod/go.sum updated to use the SDK main branch pseudo-version that includes the org-label APIs.

If you prefer to merge only after a tagged SDK release, I can bump the dependency to the new tag as soon as it’s available.

Thanks for considering!

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/102
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
Co-authored-by: Daniel <danielwichers@gmail.com>
Co-committed-by: Daniel <danielwichers@gmail.com>
2025-10-20 01:43:39 +00:00
johan
98f908d5a1 Resolve ERROR - Invalid configuration: Server 'gitea' 'args' must be a list (#101)
If you assume the template is correctly formatted, you will get this error during startup:

Starting MCP OpenAPI Proxy with config file: /app/config.json
2025-10-17 19:03:07,288 - INFO -   CORS Allowed Origins: ['*']
2025-10-17 19:03:07,288 - INFO -   Path Prefix: /
2025-10-17 19:03:07,289 - INFO -   Root Path:
2025-10-17 19:03:07,289 - INFO - Loading MCP server configurations from: /app/config.json
2025-10-17 19:03:07,289 - ERROR - Invalid configuration: Server 'gitea' 'args' must be a list

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/101
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: johan <johan@noreply.gitea.com>
Co-committed-by: johan <johan@noreply.gitea.com>
2025-10-17 23:18:20 +00:00
appleboy
e4aa29b0f9 docs: document development workflow and MCP tool usage
- Add CLAUDE.md file with guidance for using Claude Code in this repository
- Document development commands, architecture overview, and tool organization
- List available MCP tools with their main categories and functions
- Provide notes on configuration, authentication, and common development practices

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-27 19:17:00 +08:00
appleboy
32eaf86426 feat: implement graceful server shutdown on interrupt or SIGTERM (#98)
- Add graceful shutdown for HTTP server on interrupt or SIGTERM signals
- Wait for server to finish shutting down before exiting

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/98
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-09-27 08:56:41 +00:00
appleboy
8c028ec48b chore: update project and indirect dependencies to latest versions
- Update dependencies to newer versions for multiple modules
- Upgrade code.gitea.io/sdk/gitea to v0.22.0
- Upgrade github.com/mark3labs/mcp-go to v0.40.0
- Update indirect dependencies: github.com/mailru/easyjson, github.com/spf13/cast, golang.org/x/crypto, and golang.org/x/sys

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-27 16:17:14 +08:00
appleboy
88471b5de0 refactor: remove SSE transport support from code and documentation (#97)
- Remove support and documentation for sse mode across all language README files
- Update CLI flags and help text to exclude references to sse mode
- Remove SSE server initialization in operation logic
- Adjust error messages to only mention stdio and http transport types
- Update logging setup to remove sse mode conditional logging

See the latest documentation: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/97
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-09-27 08:01:56 +00:00
appleboy
e9840cf6c0 refactor: improve Bearer token parsing and validation with tests (#96)
- Refactor Bearer token parsing into a dedicated function for improved validation and readability
- Add comprehensive tests for edge cases in Bearer token extraction

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/96
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-09-27 07:31:32 +00:00
Thierry PROST
95ab3a4b73 feat: add wiki management tools (#95)
Fix #94

## Summary
This PR adds wiki management support to gitea-mcp adding new tools: creating, reading, updating, and deleting wiki pages.

## Changes
- Added `operation/wiki/wiki.go` with wiki tools
- Updated `operation/operation.go` to register it
- Updated `README.md`

## New Tools
- `list_wiki_pages` - List all wiki pages in a repository
- `get_wiki_page` - Get wiki page content and metadata
- `get_wiki_revisions` - Get revision history of a wiki page
- `create_wiki_page` - Create a new wiki page
- `update_wiki_page` - Update an existing wiki page
- `delete_wiki_page` - Delete a wiki page

## Implementation Details
- Uses direct HTTP calls to Gitea wiki API endpoints (v1.16.0+)
- Follows existing MCP patterns and error handling
- Includes fallback logic to prevent "unnamed" pages during updates
- Proper base64 content encoding as per Gitea API spec

## Testing
- All 6 tools tested and working correctly
- Error handling validated
- Integration with existing MCP server confirmed
- Made a test repo & simulated a drone construction using Claude Code (in french sorry) at https://git.kernelpanik.fr/Test-Organization/test_wiki_tools/wiki

Ready for review.
Closes #[94]

Co-authored-by: nox <nox@noxen.net>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/95
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
Co-authored-by: Thierry PROST <3kynox@noreply.gitea.com>
Co-committed-by: Thierry PROST <3kynox@noreply.gitea.com>
2025-09-27 07:29:10 +00:00
14 changed files with 1020 additions and 227 deletions

71
CLAUDE.md Normal file
View File

@@ -0,0 +1,71 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
**Build**: `make build` - Build the gitea-mcp binary
**Install**: `make install` - Build and install to GOPATH/bin
**Clean**: `make clean` - Remove build artifacts
**Test**: `go test ./...` - Run all tests
**Hot reload**: `make dev` - Start development server with hot reload (requires air)
**Dependencies**: `make vendor` - Tidy and verify module dependencies
## Architecture Overview
This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provides MCP tools for interacting with Gitea repositories, issues, pull requests, users, and more.
**Core Components**:
- `main.go` + `cmd/cmd.go`: CLI entry point and flag parsing
- `operation/operation.go`: Main server setup and tool registration
- `pkg/tool/tool.go`: Tool registry with read/write categorization
- `operation/*/`: Individual tool modules (user, repo, issue, pull, search, wiki, etc.)
**Transport Modes**:
- **stdio** (default): Standard input/output for MCP clients
- **http**: HTTP server mode on configurable port (default 8080)
**Authentication**:
- Global token via `--token` flag or `GITEA_ACCESS_TOKEN` env var
- HTTP mode supports per-request Bearer token override in Authorization header
- Token precedence: HTTP Authorization header > CLI flag > environment variable
**Tool Organization**:
- Tools are categorized as read-only or write operations
- `--read-only` flag exposes only read tools
- Tool modules register via `Tool.RegisterRead()` and `Tool.RegisterWrite()`
**Key Configuration**:
- Default Gitea host: `https://gitea.com` (override with `--host` or `GITEA_HOST`)
- Environment variables can override CLI flags: `MCP_MODE`, `GITEA_READONLY`, `GITEA_DEBUG`, `GITEA_INSECURE`
- Logs are written to `~/.gitea-mcp/gitea-mcp.log` with rotation
## Available Tools
The server provides 40+ MCP tools covering:
- **User**: get_my_user_info, get_user_orgs, search_users
- **Repository**: create_repo, fork_repo, list_my_repos, search_repos
- **Branches/Tags**: create_branch, delete_branch, list_branches, create_tag, list_tags
- **Files**: get_file_content, create_file, update_file, delete_file, get_dir_content
- **Issues**: create_issue, list_repo_issues, create_issue_comment, edit_issue
- **Pull Requests**: create_pull_request, list_repo_pull_requests, get_pull_request_by_index
- **Releases**: create_release, list_releases, get_latest_release
- **Wiki**: create_wiki_page, update_wiki_page, list_wiki_pages
- **Search**: search_repos, search_users, search_org_teams
- **Version**: get_gitea_mcp_server_version
## Common Development Patterns
**Testing**: Use `go test ./operation -run TestFunctionName` for specific tests
**Token Context**: HTTP requests use `pkg/context.TokenContextKey` for request-scoped token access
**Flag Access**: All packages access configuration via global variables in `pkg/flag/flag.go`
**Graceful Shutdown**: HTTP mode implements graceful shutdown with 10-second timeout on SIGTERM/SIGINT

View File

@@ -133,21 +133,6 @@ To configure the MCP server for Gitea, add the following to your MCP configurati
} }
``` ```
- **sse mode**
```json
{
"mcpServers": {
"gitea": {
"url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
}
}
}
```
- **http mode** - **http mode**
```json ```json
@@ -214,17 +199,28 @@ The Gitea MCP Server supports the following tools:
| get_pull_request_by_index | Pull Request | Get a pull request by its index | | get_pull_request_by_index | Pull Request | Get a pull request by its index |
| list_repo_pull_requests | Pull Request | List all pull requests in a repository | | list_repo_pull_requests | Pull Request | List all pull requests in a repository |
| create_pull_request | Pull Request | Create a new pull request | | create_pull_request | Pull Request | Create a new pull request |
| create_pull_request_reviewer | Pull Request | Add reviewers to a pull request |
| search_users | User | Search for users | | search_users | User | Search for users |
| search_org_teams | Organization | Search for teams in an organization | | search_org_teams | Organization | Search for teams in an organization |
| list_org_labels | Organization | List labels defined at organization level |
| create_org_label | Organization | Create a label in an organization |
| edit_org_label | Organization | Edit a label in an organization |
| delete_org_label | Organization | Delete a label in an organization |
| search_repos | Repository | Search for repositories | | search_repos | Repository | Search for repositories |
| get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server | | get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server |
| list_wiki_pages | Wiki | List all wiki pages in a repository |
| get_wiki_page | Wiki | Get a wiki page content and metadata |
| get_wiki_revisions | Wiki | Get revisions history of a wiki page |
| create_wiki_page | Wiki | Create a new wiki page |
| update_wiki_page | Wiki | Update an existing wiki page |
| delete_wiki_page | Wiki | Delete a wiki page |
## 🐛 Debugging ## 🐛 Debugging
To enable debug mode, add the `-d` flag when running the Gitea MCP Server with sse mode: To enable debug mode, add the `-d` flag when running the Gitea MCP Server with http mode:
```sh ```sh
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d ./gitea-mcp -t http [--port 8080] --token <your personal access token> -d
``` ```
## 🛠 Troubleshooting ## 🛠 Troubleshooting

View File

@@ -14,9 +14,9 @@
- [什么是 MCP](#什么是-mcp) - [什么是 MCP](#什么是-mcp)
- [🚧 安装](#-安装) - [🚧 安装](#-安装)
- [在 VS Code 中使用](#在-vs-code-中使用) - [在 VS Code 中使用](#在-vs-code-中使用)
- [📥 下载官方 Gitea MCP 二进制版本](#-下载官方-gitea-mcp-二进制版本) - [📥 下载官方二进制版本](#-下载官方二进制版本)
- [🔧 从源码构建](#-从源码构建) - [🔧 从源码构建](#-从源码构建)
- [📁 添加到 PATH](#-添加到-path) - [📁 加入 PATH](#-加入-path)
- [🚀 使用](#-使用) - [🚀 使用](#-使用)
- [✅ 可用工具](#-可用工具) - [✅ 可用工具](#-可用工具)
- [🐛 调试](#-调试) - [🐛 调试](#-调试)
@@ -24,23 +24,23 @@
## 什么是 Gitea ## 什么是 Gitea
Gitea 是一个由社区管理的轻量级代码托管解决方案,使用 Go 语言编写。它以 MIT 许可证发布。Gitea 提供 Git 托管,包括仓库查看器、问题追踪、拉取请求等功能。 Gitea 是一个由社区管理的轻量级代码托管解决方案,使用 Go 语言编写,采用 MIT 许可证。Gitea 提供 Git 托管,包括仓库浏览、问题追踪、拉取请求等功能。
## 什么是 MCP ## 什么是 MCP
Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各种工具和系统。它能够无缝执行命令管理仓库、用户其他资源。 Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各种工具和系统。它能够无缝执行命令管理仓库、用户其他资源。
## 🚧 安装 ## 🚧 安装
### 在 VS Code 中使用 ### 在 VS Code 中使用
要快速安装,请使用本 README 顶部的单击安装按钮之一 要快速安装,请使用本 README 顶部的安装按钮。
手动安装,请将以下 JSON 块添加到 VS Code 的用户设置 (JSON) 文件中。您可以通过按 `Ctrl + Shift + P` 并输入 `Preferences: Open User Settings (JSON)` 来完成此操作 如需手动安装,请将以下 JSON 块添加到 VS Code 的用户设置 (JSON) 文件。可通过按 `Ctrl + Shift + P` 并输入 `Preferences: Open User Settings (JSON)`
或者,您可以将其添加到工作区`.vscode/mcp.json` 文件中。这将允许您与他人共享配置。 也可添加到工作区的 `.vscode/mcp.json` 文件,方便与他人共享配置。
> 请注意,`.vscode/mcp.json` 文件不需要 `mcp` 键。 > `.vscode/mcp.json` 文件不需要 `mcp` 键。
```json ```json
{ {
@@ -73,22 +73,22 @@ Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各
} }
``` ```
### 📥 下载官方 Gitea MCP 二进制版本 ### 📥 下载官方二进制版本
您可以从[官方 Gitea MCP 二进制版本](https://gitea.com/gitea/gitea-mcp/releases)下载官方版本 可在 [官方 Gitea MCP 二进制版本](https://gitea.com/gitea/gitea-mcp/releases) 下载。
### 🔧 从源码构建 ### 🔧 从源码构建
您可以使用 Git 克隆仓库来下载源码: 用 Git 下载源码:
```bash ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://gitea.com/gitea/gitea-mcp.git
``` ```
构建之前,请确保您已安装以下内容 构建前请先安装
- make - make
- Golang (建议使用 Go 1.24 或更高版本) - Golang(建议 Go 1.24 及以上)
然后运行: 然后运行:
@@ -96,9 +96,9 @@ git clone https://gitea.com/gitea/gitea-mcp.git
make install make install
``` ```
### 📁 添加到 PATH ### 📁 加入 PATH
构建后,将二进制文件 gitea-mcp 复制到系统 PATH 中包含的目录例如: 安装后,将 gitea-mcp 可执行文件复制到系统 PATH 目录例如:
```bash ```bash
cp gitea-mcp /usr/local/bin/ cp gitea-mcp /usr/local/bin/
@@ -106,8 +106,8 @@ cp gitea-mcp /usr/local/bin/
## 🚀 使用 ## 🚀 使用
此示例适用于 Cursor也可在 VSCode 使用插件。 此示例适用于 Cursor也可在 VSCode 使用插件。
要配置 Gitea MCP 服务器,请将以下内容添加到您的 MCP 配置文件 要配置 Gitea MCP 服务器,请将以下内容添加到 MCP 配置文件:
- **stdio 模式** - **stdio 模式**
@@ -133,21 +133,6 @@ cp gitea-mcp /usr/local/bin/
} }
``` ```
- **sse 模式**
```json
{
"mcpServers": {
"gitea": {
"url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
}
}
}
```
- **http 模式** - **http 模式**
```json ```json
@@ -166,10 +151,10 @@ cp gitea-mcp /usr/local/bin/
**默认日志路径**: `$HOME/.gitea-mcp/gitea-mcp.log` **默认日志路径**: `$HOME/.gitea-mcp/gitea-mcp.log`
> [!注意] > [!注意]
> 您可以通过命令行参数或环境变量提供您的 Gitea 主机和访问令牌。 > 通过命令行参数或环境变量提供 Gitea 主机和访问令牌。
> 命令行参数具有最高优先 > 命令行参数优先
一切设置完成后,请尝试在您的 MCP 兼容聊天框输入以下内容 一切设置完成后,可在 MCP 聊天框输入:
```text ```text
列出我所有的仓库 列出我所有的仓库
@@ -179,61 +164,72 @@ cp gitea-mcp /usr/local/bin/
Gitea MCP 服务器支持以下工具: Gitea MCP 服务器支持以下工具:
| 工具 | 范围 | 描述 | | 工具 | 范围 | 描述 |
| :--------------------------: | :------: | :--------------------------: | | :--------------------------: | :------: | :------------------------: |
| get_my_user_info | 用户 | 获取已认证用户信息 | | get_my_user_info | 用户 | 获取已认证用户信息 |
| get_user_orgs | 用户 | 获取已认证用户关联组织 | | get_user_orgs | 用户 | 获取已认证用户关联组织 |
| create_repo | 仓库 | 创建一个新仓库 | | create_repo | 仓库 | 创建新仓库 |
| fork_repo | 仓库 | 复刻一个仓库 | | fork_repo | 仓库 | 复刻仓库 |
| list_my_repos | 仓库 | 列出已认证用户拥有的所有仓库 | | list_my_repos | 仓库 | 列出用户所有仓库 |
| create_branch | 分支 | 创建一个新分支 | | create_branch | 分支 | 创建新分支 |
| delete_branch | 分支 | 删除一个分支 | | delete_branch | 分支 | 删除分支 |
| list_branches | 分支 | 列出仓库中的所有分支 | | list_branches | 分支 | 列出所有分支 |
| create_release | 版本发布 | 创建一个新版本发布 | | create_release | 版本发布 | 创建新版本发布 |
| delete_release | 版本发布 | 删除一个版本发布 | | delete_release | 版本发布 | 删除版本发布 |
| get_release | 版本发布 | 获取一个版本发布 | | get_release | 版本发布 | 获取版本发布 |
| get_latest_release | 版本发布 | 获取最新版本发布 | | get_latest_release | 版本发布 | 获取最新版本发布 |
| list_releases | 版本发布 | 列出所有版本发布 | | list_releases | 版本发布 | 列出所有版本发布 |
| create_tag | 标签 | 创建一个新标签 | | create_tag | 标签 | 创建新标签 |
| delete_tag | 标签 | 删除一个标签 | | delete_tag | 标签 | 删除标签 |
| get_tag | 标签 | 获取一个标签 | | get_tag | 标签 | 获取标签 |
| list_tags | 标签 | 列出所有标签 | | list_tags | 标签 | 列出所有标签 |
| list_repo_commits | 提交 | 列出仓库中的所有提交 | | list_repo_commits | 提交 | 列出所有提交 |
| get_file_content | 文件 | 获取文件内容和元数据 | | get_file_content | 文件 | 获取文件内容和元数据 |
| get_dir_content | 文件 | 获取目录内容列表 | | get_dir_content | 文件 | 获取目录内容列表 |
| create_file | 文件 | 创建一个新文件 | | create_file | 文件 | 创建新文件 |
| update_file | 文件 | 更新现有文件 | | update_file | 文件 | 更新现有文件 |
| delete_file | 文件 | 删除一个文件 | | delete_file | 文件 | 删除文件 |
| get_issue_by_index | 问题 | 根据索引获取问题 | | get_issue_by_index | 问题 | 索引获取问题 |
| list_repo_issues | 问题 | 列出仓库中的所有问题 | | list_repo_issues | 问题 | 列出所有问题 |
| create_issue | 问题 | 创建一个新问题 | | create_issue | 问题 | 创建新问题 |
| create_issue_comment | 问题 | 在问题上创建评论 | | create_issue_comment | 问题 | 在问题上创建评论 |
| edit_issue | 问题 | 编辑一个问题 | | edit_issue | 问题 | 编辑问题 |
| edit_issue_comment | 问题 | 在问题上编辑评论 | | edit_issue_comment | 问题 | 编辑问题评论 |
| get_issue_comments_by_index | 问题 | 根据索引获取问题评论 | | get_issue_comments_by_index | 问题 | 索引获取问题评论 |
| get_pull_request_by_index | 拉取请求 | 根据索引获取拉取请求 | | get_pull_request_by_index | 拉取请求 | 索引获取拉取请求 |
| list_repo_pull_requests | 拉取请求 | 列出仓库中的所有拉取请求 | | list_repo_pull_requests | 拉取请求 | 列出所有拉取请求 |
| create_pull_request | 拉取请求 | 创建一个新拉取请求 | | create_pull_request | 拉取请求 | 创建新拉取请求 |
| search_users | 用户 | 搜索用户 | | create_pull_request_reviewer | 拉取请求 | 为拉取请求添加审查者 |
| search_org_teams | 组织 | 搜索组织中的团队 | | search_users | 用户 | 搜索用户 |
| search_repos | 仓库 | 搜索仓库 | | search_org_teams | 组织 | 搜索组织团队 |
| get_gitea_mcp_server_version | 服务器 | 获取 Gitea MCP 服务器的版本 | | list_org_labels | 组织 | 列出组织标签 |
| create_org_label | 组织 | 创建组织标签 |
| edit_org_label | 组织 | 编辑组织标签 |
| delete_org_label | 组织 | 删除组织标签 |
| search_repos | 仓库 | 搜索仓库 |
| get_gitea_mcp_server_version | 服务器 | 获取 Gitea MCP 服务器版本 |
| list_wiki_pages | Wiki | 列出所有 Wiki 页面 |
| get_wiki_page | Wiki | 获取 Wiki 页面内容和元数据 |
| get_wiki_revisions | Wiki | 获取 Wiki 修订历史 |
| create_wiki_page | Wiki | 创建新 Wiki 页面 |
| update_wiki_page | Wiki | 更新现有 Wiki 页面 |
| delete_wiki_page | Wiki | 删除 Wiki 页面 |
## 🐛 调试 ## 🐛 调试
启用调试模式,请在使用 sse 模式运行 Gitea MCP 服务器时`-d` 标志: 启用调试模式,请在 http 模式运行 Gitea MCP 服务器时加 `-d` 标志:
```sh ```sh
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d ./gitea-mcp -t http [--port 8080] --token <your personal access token> -d
``` ```
## 🛠 疑难排解 ## 🛠 疑难排解
果您遇到任何问题,以下是一些常见的疑难排解步骤: 遇问题,可参考以下步骤:
1. **检查您的 PATH**: 确保 `gitea-mcp` 二进制文件位于系统 PATH 中包含的目录中。 1. **检查 PATH**确保 `gitea-mcp` 可执行文件已在系统 PATH 目录中。
2. **验证依赖**: 确保您已安装所有所需的依赖项,例如 `make``Golang` 2. **验证依赖**:确认已安装 `make``Golang` 等必要依赖
3. **检查配置**: 仔细检查您的 MCP 配置文件是否有任何错误或遗漏的信息 3. **检查配置**仔细检查 MCP 配置文件是否有错误或遗漏。
4. **查看日志**: 检查日志中是否有任何错误消息或警告,可以提供有关问题的更多信息。 4. **查看日志**检查日志消息或警告以获取更多信息。
享受通过聊天探索和管理您的 Gitea 仓库的乐趣 享受通过聊天探索和管理您的 Gitea 仓库!

View File

@@ -14,9 +14,9 @@
- [什麼是 MCP](#什麼是-mcp) - [什麼是 MCP](#什麼是-mcp)
- [🚧 安裝](#-安裝) - [🚧 安裝](#-安裝)
- [在 VS Code 中使用](#在-vs-code-中使用) - [在 VS Code 中使用](#在-vs-code-中使用)
- [📥 下載官方 Gitea MCP 二進位版本](#-下載官方-gitea-mcp-二進位版本) - [📥 下載官方二進位版本](#-下載官方二進位版本)
- [🔧 從源代碼構建](#-從源代碼構建) - [🔧 從原始碼建置](#-從原始碼建置)
- [📁 添加到 PATH](#-添加到-path) - [📁 加入 PATH](#-加入-path)
- [🚀 使用](#-使用) - [🚀 使用](#-使用)
- [✅ 可用工具](#-可用工具) - [✅ 可用工具](#-可用工具)
- [🐛 調試](#-調試) - [🐛 調試](#-調試)
@@ -24,23 +24,23 @@
## 什麼是 Gitea ## 什麼是 Gitea
Gitea 是一個由社群管理的輕量級碼託管解決方案,使用 Go 語言編寫。它以 MIT 許可證發布。Gitea 提供 Git 託管,包括倉庫查看器、問題追蹤、拉取請求等功能。 Gitea 是一個由社群管理的輕量級程式碼託管解決方案,使用 Go 語言編寫,採用 MIT 授權。Gitea 提供 Git 託管,包括倉庫瀏覽、議題追蹤、拉取請求等功能。
## 什麼是 MCP ## 什麼是 MCP
Model Context Protocol (MCP) 是一種協議,允許過聊天面整合各種工具系統。它能夠無縫執行命令管理倉庫、用戶和其他資源。 Model Context Protocol (MCP) 是一種協議,允許過聊天面整合各種工具系統。它能夠無縫執行命令管理倉庫、使用者及其他資源。
## 🚧 安裝 ## 🚧 安裝
### 在 VS Code 中使用 ### 在 VS Code 中使用
快速安裝,請使用本 README 頂部的單擊安裝按鈕之一 快速安裝,請使用本 README 頂部的安裝按鈕。
手動安裝,請將下 JSON 塊添加到 VS Code 的用戶設置 (JSON) 文件中。您可以通過`Ctrl + Shift + P` 並輸入 `Preferences: Open User Settings (JSON)` 來完成此操作 如需手動安裝,請將下 JSON 區塊加入 VS Code 的使用者設定 (JSON) 檔案。可`Ctrl + Shift + P` 並輸入 `Preferences: Open User Settings (JSON)`
或者,您可以將其添加到工作區`.vscode/mcp.json` 文件中。這將允許您與他人共享配置 也可加入至工作區的 `.vscode/mcp.json` 檔案,方便與他人共享設定
> 請注意,`.vscode/mcp.json` 文件中不需 `mcp` 鍵。 > `.vscode/mcp.json` 檔案不需 `mcp` 鍵。
```json ```json
{ {
@@ -49,7 +49,7 @@ Model Context Protocol (MCP) 是一種協議,允許通過聊天界面整合各
{ {
"type": "promptString", "type": "promptString",
"id": "gitea_token", "id": "gitea_token",
"description": "Gitea 個人訪問令牌", "description": "Gitea 個人存取令牌",
"password": true "password": true
} }
], ],
@@ -73,32 +73,32 @@ Model Context Protocol (MCP) 是一種協議,允許通過聊天界面整合各
} }
``` ```
### 📥 下載官方 Gitea MCP 二進位版本 ### 📥 下載官方二進位版本
您可以從[官方 Gitea MCP 二進位版本](https://gitea.com/gitea/gitea-mcp/releases)下載官方版本 可至 [官方 Gitea MCP 二進位版本](https://gitea.com/gitea/gitea-mcp/releases) 下載。
### 🔧 從源代碼構建 ### 🔧 從原始碼建置
您可以使用 Git 克隆倉庫來下載源代碼: 用 Git 下載原始碼:
```bash ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://gitea.com/gitea/gitea-mcp.git
``` ```
在構建之前,請確保您已安裝以下內容 建置前請先安裝
- make - make
- Golang (建議使用 Go 1.24 或更高版本) - Golang(建議 Go 1.24 以上)
然後行: 然後行:
```bash ```bash
make install make install
``` ```
### 📁 添加到 PATH ### 📁 加入 PATH
安裝後,將二進制文件 gitea-mcp 複製到系統 PATH 中包含的目錄例如: 安裝後,將 gitea-mcp 執行檔複製到系統 PATH 目錄例如:
```bash ```bash
cp gitea-mcp /usr/local/bin/ cp gitea-mcp /usr/local/bin/
@@ -106,8 +106,8 @@ cp gitea-mcp /usr/local/bin/
## 🚀 使用 ## 🚀 使用
例適用於 Cursor也可在 VSCode 使用插件。 例適用於 Cursor也可在 VSCode 使用插件。
要配置 Gitea MCP 伺服器,請將以下內容添加到您的 MCP 配置文件中 欲設定 Gitea MCP 伺服器,請將下列內容加入 MCP 設定檔
- **stdio 模式** - **stdio 模式**
@@ -133,21 +133,6 @@ cp gitea-mcp /usr/local/bin/
} }
``` ```
- **sse 模式**
```json
{
"mcpServers": {
"gitea": {
"url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
}
}
}
```
- **http 模式** - **http 模式**
```json ```json
@@ -166,10 +151,10 @@ cp gitea-mcp /usr/local/bin/
**預設日誌路徑**: `$HOME/.gitea-mcp/gitea-mcp.log` **預設日誌路徑**: `$HOME/.gitea-mcp/gitea-mcp.log`
> [!注意] > [!注意]
> 您可以通過命令列參數或環境變數提供您的 Gitea 主機和訪問令牌。 > 可用命令列參數或環境變數提供 Gitea 主機與存取令牌。
> 命令列參數具有最高優先 > 命令列參數優先
一切設完成後,請嘗試在您的 MCP 兼容聊天框輸入以下內容 一切設完成後,可在 MCP 聊天框輸入:
```text ```text
列出我所有的倉庫 列出我所有的倉庫
@@ -177,63 +162,74 @@ cp gitea-mcp /usr/local/bin/
## ✅ 可用工具 ## ✅ 可用工具
Gitea MCP 伺服器支以下工具: Gitea MCP 伺服器支以下工具:
| 工具 | 範圍 | 描述 | | 工具 | 範圍 | 描述 |
| :--------------------------: | :------: | :--------------------------: | | :--------------------------: | :------: | :--------------------------: |
| get_my_user_info | 用戶 | 獲取已認證用戶的信息 | | get_my_user_info | 用戶 | 取得已認證用戶資訊 |
| get_user_orgs | 用戶 | 取得已認證用戶所屬組織 | | get_user_orgs | 用戶 | 取得已認證用戶所屬組織 |
| create_repo | 倉庫 | 創建一個新倉庫 | | create_repo | 倉庫 | 創建新倉庫 |
| fork_repo | 倉庫 | 復刻一個倉庫 | | fork_repo | 倉庫 | 復刻倉庫 |
| list_my_repos | 倉庫 | 列出已認證用戶擁有的所有倉庫 | | list_my_repos | 倉庫 | 列出用戶所有倉庫 |
| create_branch | 分支 | 創建一個新分支 | | create_branch | 分支 | 創建新分支 |
| delete_branch | 分支 | 刪除一個分支 | | delete_branch | 分支 | 刪除分支 |
| list_branches | 分支 | 列出倉庫中的所有分支 | | list_branches | 分支 | 列出所有分支 |
| create_release | 版本發布 | 創建一個新版本發布 | | create_release | 版本發布 | 創建新版本發布 |
| delete_release | 版本發布 | 刪除一個版本發布 | | delete_release | 版本發布 | 刪除版本發布 |
| get_release | 版本發布 | 獲取一個版本發布 | | get_release | 版本發布 | 取得版本發布 |
| get_latest_release | 版本發布 | 獲取最新版本發布 | | get_latest_release | 版本發布 | 取得最新版本發布 |
| list_releases | 版本發布 | 列出所有版本發布 | | list_releases | 版本發布 | 列出所有版本發布 |
| create_tag | 標籤 | 創建一個新標籤 | | create_tag | 標籤 | 創建新標籤 |
| delete_tag | 標籤 | 刪除一個標籤 | | delete_tag | 標籤 | 刪除標籤 |
| get_tag | 標籤 | 獲取一個標籤 | | get_tag | 標籤 | 取得標籤 |
| list_tags | 標籤 | 列出所有標籤 | | list_tags | 標籤 | 列出所有標籤 |
| list_repo_commits | 提交 | 列出倉庫中的所有提交 | | list_repo_commits | 提交 | 列出所有提交 |
| get_file_content | 文件 | 取文件內容和元數據 | | get_file_content | 文件 | 取文件內容與中繼資料 |
| get_dir_content | 文件 | 獲取目錄內容列表 | | get_dir_content | 文件 | 取得目錄內容列表 |
| create_file | 文件 | 創建一個新文件 | | create_file | 文件 | 創建新文件 |
| update_file | 文件 | 更新現有文件 | | update_file | 文件 | 更新現有文件 |
| delete_file | 文件 | 刪除一個文件 | | delete_file | 文件 | 刪除文件 |
| get_issue_by_index | 問題 | 根據索引獲取問題 | | get_issue_by_index | 問題 | 依索引取得問題 |
| list_repo_issues | 問題 | 列出倉庫中的所有問題 | | list_repo_issues | 問題 | 列出所有問題 |
| create_issue | 問題 | 創建一個新問題 | | create_issue | 問題 | 創建新問題 |
| create_issue_comment | 問題 | 在問題上創建評論 | | create_issue_comment | 問題 | 在問題上創建評論 |
| edit_issue | 問題 | 編輯一個問題 | | edit_issue | 問題 | 編輯問題 |
| edit_issue_comment | 問題 | 在問題上編輯評論 | | edit_issue_comment | 問題 | 編輯問題評論 |
| get_issue_comments_by_index | 问题 | 根據索引獲取問題評論 | | get_issue_comments_by_index | 問題 | 依索引取得問題評論 |
| get_pull_request_by_index | 拉取請求 | 根據索引獲取拉取請求 | | get_pull_request_by_index | 拉取請求 | 依索引取得拉取請求 |
| list_repo_pull_requests | 拉取請求 | 列出倉庫中的所有拉取請求 | | list_repo_pull_requests | 拉取請求 | 列出所有拉取請求 |
| create_pull_request | 拉取請求 | 創建一個新拉取請求 | | create_pull_request | 拉取請求 | 創建新拉取請求 |
| search_users | 用戶 | 搜索用戶 | | create_pull_request_reviewer | 拉取請求 | 為拉取請求添加審查者 |
| search_org_teams | 組織 | 搜索組織中的團隊 | | search_users | 用戶 | 搜尋用戶 |
| search_repos | 倉庫 | 搜索倉庫 | | search_org_teams | 組織 | 搜尋組織團隊 |
| get_gitea_mcp_server_version | 伺服器 | 獲取 Gitea MCP 伺服器的版本 | | list_org_labels | 組織 | 列出組織標籤 |
| create_org_label | 組織 | 創建組織標籤 |
| edit_org_label | 組織 | 編輯組織標籤 |
| delete_org_label | 組織 | 刪除組織標籤 |
| search_repos | 倉庫 | 搜尋倉庫 |
| get_gitea_mcp_server_version | 伺服器 | 取得 Gitea MCP 伺服器版本 |
| list_wiki_pages | Wiki | 列出所有 Wiki 頁面 |
| get_wiki_page | Wiki | 取得 Wiki 頁面內容與中繼資料 |
| get_wiki_revisions | Wiki | 取得 Wiki 修訂歷史 |
| create_wiki_page | Wiki | 創建新 Wiki 頁面 |
| update_wiki_page | Wiki | 更新現有 Wiki 頁面 |
| delete_wiki_page | Wiki | 刪除 Wiki 頁面 |
## 🐛 調試 ## 🐛 調試
啟用調試模式,請在使用 sse 模式行 Gitea MCP 伺服器時`-d` 旗標: 啟用調試模式,請在 http 模式行 Gitea MCP 伺服器時加 `-d` 旗標:
```sh ```sh
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d ./gitea-mcp -t http [--port 8080] --token <your personal access token> -d
``` ```
## 🛠 疑難排解 ## 🛠 疑難排解
果您遇到任何問題,以下是一些常見的疑難排解步驟: 遇問題,可參考以下步驟:
1. **檢查您的 PATH**: 確保 `gitea-mcp` 二進制文件位於系統 PATH 中包含的目錄中。 1. **檢查 PATH**確保 `gitea-mcp` 執行檔已在系統 PATH 目錄中。
2. **驗證依賴**: 確保您已安裝所有所需的依賴項,例如 `make` `Golang` 2. **驗證依賴**:確認已安裝 `make` `Golang` 等必要依賴
3. **檢查配置**: 仔細檢查您的 MCP 配置文件是否有任何錯誤或遺漏的信息 3. **檢查設定**仔細檢查 MCP 設定檔是否有錯誤或遺漏。
4. **查看日誌**: 檢查日誌中是否有任何錯誤消息或警告,可以提供有關問題的更多信息 4. **查看日誌**檢查日誌訊息或警告以獲取更多資訊
享受過聊天探索管理您的 Gitea 倉庫的樂趣 享受過聊天探索管理您的 Gitea 倉庫!

View File

@@ -21,13 +21,13 @@ func init() {
&flagPkg.Mode, &flagPkg.Mode,
"t", "t",
"stdio", "stdio",
"Transport type (stdio, sse or http)", "Transport type (stdio or http)",
) )
flag.StringVar( flag.StringVar(
&flagPkg.Mode, &flagPkg.Mode,
"transport", "transport",
"stdio", "stdio",
"Transport type (stdio, sse or http)", "Transport type (stdio or http)",
) )
flag.StringVar( flag.StringVar(
&host, &host,
@@ -39,7 +39,7 @@ func init() {
&port, &port,
"port", "port",
8080, 8080,
"see or http port", "http port",
) )
flag.StringVar( flag.StringVar(
&token, &token,

View File

@@ -2,11 +2,11 @@
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"command": "gitea-mcp", "command": "gitea-mcp",
"args": { "args": [
"-t": "stdio", "-t", "stdio",
"--host": "https://gitea.com", "--host", "https://gitea.com",
"--token": "<your personal access token>" "--token", "<your personal access token>"
}, ]
"env": { "env": {
"GITEA_HOST": "https://gitea.com", "GITEA_HOST": "https://gitea.com",
"GITEA_ACCESS_TOKEN": "<your personal access token>" "GITEA_ACCESS_TOKEN": "<your personal access token>"

12
go.mod
View File

@@ -3,8 +3,8 @@ module gitea.com/gitea/gitea-mcp
go 1.24.0 go 1.24.0
require ( require (
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.22.1
github.com/mark3labs/mcp-go v0.36.0 github.com/mark3labs/mcp-go v0.42.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
@@ -18,12 +18,12 @@ require (
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.9.1 // indirect
github.com/spf13/cast v1.9.2 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.40.0 // indirect golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

29
go.sum
View File

@@ -1,5 +1,5 @@
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -22,21 +22,20 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis= github.com/mark3labs/mcp-go v0.42.0 h1:gk/8nYJh8t3yroCAOBhNbYsM9TCKvkM13I5t5Hfu6Ls=
github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mark3labs/mcp-go v0.42.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
@@ -52,18 +51,18 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -27,6 +27,10 @@ const (
ReplaceIssueLabelsToolName = "replace_issue_labels" ReplaceIssueLabelsToolName = "replace_issue_labels"
ClearIssueLabelsToolName = "clear_issue_labels" ClearIssueLabelsToolName = "clear_issue_labels"
RemoveIssueLabelToolName = "remove_issue_label" RemoveIssueLabelToolName = "remove_issue_label"
ListOrgLabelsToolName = "list_org_labels"
CreateOrgLabelToolName = "create_org_label"
EditOrgLabelToolName = "edit_org_label"
DeleteOrgLabelToolName = "delete_org_label"
) )
var ( var (
@@ -110,6 +114,43 @@ var (
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")), mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")),
) )
ListOrgLabelsTool = mcp.NewTool(
ListOrgLabelsToolName,
mcp.WithDescription("Lists labels defined at organization level"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CreateOrgLabelTool = mcp.NewTool(
CreateOrgLabelToolName,
mcp.WithDescription("Creates a new label for an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("name", mcp.Required(), mcp.Description("label name")),
mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")),
mcp.WithString("description", mcp.Description("label description")),
mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive"), mcp.DefaultBool(false)),
)
EditOrgLabelTool = mcp.NewTool(
EditOrgLabelToolName,
mcp.WithDescription("Edits an existing organization label"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
mcp.WithString("name", mcp.Description("new label name")),
mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")),
mcp.WithString("description", mcp.Description("new label description")),
mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive")),
)
DeleteOrgLabelTool = mcp.NewTool(
DeleteOrgLabelToolName,
mcp.WithDescription("Deletes an organization label by ID"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
) )
func init() { func init() {
@@ -149,6 +190,22 @@ func init() {
Tool: RemoveIssueLabelTool, Tool: RemoveIssueLabelTool,
Handler: RemoveIssueLabelFn, Handler: RemoveIssueLabelFn,
}) })
Tool.RegisterRead(server.ServerTool{
Tool: ListOrgLabelsTool,
Handler: ListOrgLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: CreateOrgLabelTool,
Handler: CreateOrgLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditOrgLabelTool,
Handler: EditOrgLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteOrgLabelTool,
Handler: DeleteOrgLabelFn,
})
} }
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -452,3 +509,128 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
} }
return to.TextResult("Label removed successfully") return to.TextResult("Label removed successfully")
} }
func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgLabelsFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("org is required"))
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.ListOrgLabelsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
},
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
labels, _, err := client.ListOrgLabels(org, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/labels err: %v", org, err))
}
return to.TextResult(labels)
}
func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgLabelFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("org is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("name is required"))
}
color, ok := req.GetArguments()["color"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("color is required"))
}
description, _ := req.GetArguments()["description"].(string)
exclusive, _ := req.GetArguments()["exclusive"].(bool)
opt := gitea_sdk.CreateOrgLabelOption{
Name: name,
Color: color,
Description: description,
Exclusive: exclusive,
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
label, _, err := client.CreateOrgLabel(org, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/labels err: %v", org, err))
}
return to.TextResult(label)
}
func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditOrgLabelFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("org is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("label ID is required"))
}
opt := gitea_sdk.EditOrgLabelOption{}
if name, ok := req.GetArguments()["name"].(string); ok {
opt.Name = ptr.To(name)
}
if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = ptr.To(color)
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = ptr.To(description)
}
if exclusive, ok := req.GetArguments()["exclusive"].(bool); ok {
opt.Exclusive = ptr.To(exclusive)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
label, _, err := client.EditOrgLabel(org, int64(id), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, int64(id), err))
}
return to.TextResult(label)
}
func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteOrgLabelFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("org is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("label ID is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteOrgLabel(org, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/labels/%v err: %v", org, int64(id), err))
}
return to.TextResult("Label deleted successfully")
}

View File

@@ -4,7 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"os"
"os/signal"
"strings" "strings"
"syscall"
"time" "time"
"gitea.com/gitea/gitea-mcp/operation/issue" "gitea.com/gitea/gitea-mcp/operation/issue"
@@ -14,6 +17,7 @@ import (
"gitea.com/gitea/gitea-mcp/operation/search" "gitea.com/gitea/gitea-mcp/operation/search"
"gitea.com/gitea/gitea-mcp/operation/user" "gitea.com/gitea/gitea-mcp/operation/user"
"gitea.com/gitea/gitea-mcp/operation/version" "gitea.com/gitea/gitea-mcp/operation/version"
"gitea.com/gitea/gitea-mcp/operation/wiki"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
@@ -45,21 +49,40 @@ func RegisterTool(s *server.MCPServer) {
// Version Tool // Version Tool
s.AddTools(version.Tool.Tools()...) s.AddTools(version.Tool.Tools()...)
// Wiki Tool
s.AddTools(wiki.Tool.Tools()...)
s.DeleteTools("") s.DeleteTools("")
} }
// parseBearerToken extracts the Bearer token from an Authorization header.
// Returns the token and true if valid, empty string and false otherwise.
func parseBearerToken(authHeader string) (string, bool) {
const bearerPrefix = "Bearer "
if len(authHeader) < len(bearerPrefix) || !strings.HasPrefix(authHeader, bearerPrefix) {
return "", false
}
token := strings.TrimSpace(authHeader[len(bearerPrefix):])
if token == "" {
return "", false
}
return token, true
}
func getContextWithToken(ctx context.Context, r *http.Request) context.Context { func getContextWithToken(ctx context.Context, r *http.Request) context.Context {
authHeader := r.Header.Get("Authorization") authHeader := r.Header.Get("Authorization")
if authHeader == "" { if authHeader == "" {
return ctx return ctx
} }
parts := strings.Split(authHeader, " ") token, ok := parseBearerToken(authHeader)
if len(parts) != 2 || parts[0] != "Bearer" { if !ok {
return ctx return ctx
} }
return context.WithValue(ctx, mcpContext.TokenContextKey, parts[1]) return context.WithValue(ctx, mcpContext.TokenContextKey, token)
} }
func Run() error { func Run() error {
@@ -72,15 +95,6 @@ func Run() error {
); err != nil { ); err != nil {
return err return err
} }
case "sse":
sseServer := server.NewSSEServer(
mcpServer,
server.WithSSEContextFunc(getContextWithToken),
)
log.Infof("Gitea MCP SSE server listening on :%d", flag.Port)
if err := sseServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
return err
}
case "http": case "http":
httpServer := server.NewStreamableHTTPServer( httpServer := server.NewStreamableHTTPServer(
mcpServer, mcpServer,
@@ -89,11 +103,29 @@ func Run() error {
server.WithHTTPContextFunc(getContextWithToken), server.WithHTTPContextFunc(getContextWithToken),
) )
log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port) log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port)
// Graceful shutdown setup
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
shutdownDone := make(chan struct{})
go func() {
<-sigCh
log.Infof("Shutdown signal received, gracefully stopping HTTP server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Errorf("HTTP server shutdown error: %v", err)
}
close(shutdownDone)
}()
if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil { if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
return err return err
} }
<-shutdownDone // Wait for shutdown to finish
default: default:
return fmt.Errorf("invalid transport type: %s. Must be 'stdio', 'sse' or 'http'", flag.Mode) return fmt.Errorf("invalid transport type: %s. Must be 'stdio' or 'http'", flag.Mode)
} }
return nil return nil
} }

View File

@@ -0,0 +1,81 @@
package operation
import (
"testing"
)
func TestParseBearerToken(t *testing.T) {
tests := []struct {
name string
header string
wantToken string
wantOK bool
}{
{
name: "valid token",
header: "Bearer validtoken",
wantToken: "validtoken",
wantOK: true,
},
{
name: "token with spaces trimmed",
header: "Bearer spacedToken ",
wantToken: "spacedToken",
wantOK: true,
},
{
name: "lowercase bearer should fail",
header: "bearer lowercase",
wantToken: "",
wantOK: false,
},
{
name: "bearer with no token",
header: "Bearer ",
wantToken: "",
wantOK: false,
},
{
name: "bearer with only spaces",
header: "Bearer ",
wantToken: "",
wantOK: false,
},
{
name: "missing space after Bearer",
header: "Bearertoken",
wantToken: "",
wantOK: false,
},
{
name: "different auth type",
header: "Basic dXNlcjpwYXNz",
wantToken: "",
wantOK: false,
},
{
name: "empty header",
header: "",
wantToken: "",
wantOK: false,
},
{
name: "token with internal spaces",
header: "Bearer token with spaces",
wantToken: "token with spaces",
wantOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotToken, gotOK := parseBearerToken(tt.header)
if gotToken != tt.wantToken {
t.Errorf("parseBearerToken() token = %q, want %q", gotToken, tt.wantToken)
}
if gotOK != tt.wantOK {
t.Errorf("parseBearerToken() ok = %v, want %v", gotOK, tt.wantOK)
}
})
}
}

View File

@@ -17,9 +17,10 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
GetPullRequestByIndexToolName = "get_pull_request_by_index" GetPullRequestByIndexToolName = "get_pull_request_by_index"
ListRepoPullRequestsToolName = "list_repo_pull_requests" ListRepoPullRequestsToolName = "list_repo_pull_requests"
CreatePullRequestToolName = "create_pull_request" CreatePullRequestToolName = "create_pull_request"
CreatePullRequestReviewerToolName = "create_pull_request_reviewer"
) )
var ( var (
@@ -53,6 +54,16 @@ var (
mcp.WithString("head", mcp.Required(), mcp.Description("pull request head")), mcp.WithString("head", mcp.Required(), mcp.Description("pull request head")),
mcp.WithString("base", mcp.Required(), mcp.Description("pull request base")), mcp.WithString("base", mcp.Required(), mcp.Description("pull request base")),
) )
CreatePullRequestReviewerTool = mcp.NewTool(
CreatePullRequestReviewerToolName,
mcp.WithDescription("create pull request reviewer"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), mcp.Items(map[string]interface{}{"type": "string"})),
mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]interface{}{"type": "string"})),
)
) )
func init() { func init() {
@@ -68,6 +79,10 @@ func init() {
Tool: CreatePullRequestTool, Tool: CreatePullRequestTool,
Handler: CreatePullRequestFn, Handler: CreatePullRequestFn,
}) })
Tool.RegisterWrite(server.ServerTool{
Tool: CreatePullRequestReviewerTool,
Handler: CreatePullRequestReviewerFn,
})
} }
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -183,3 +198,65 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
return to.TextResult(pr) return to.TextResult(pr)
} }
func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestReviewerFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("index is required"))
}
var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]interface{}); ok {
for _, reviewer := range reviewersSlice {
if reviewerStr, ok := reviewer.(string); ok {
reviewers = append(reviewers, reviewerStr)
}
}
}
}
var teamReviewers []string
if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists {
if teamReviewersSlice, ok := teamReviewersArg.([]interface{}); ok {
for _, teamReviewer := range teamReviewersSlice {
if teamReviewerStr, ok := teamReviewer.(string); ok {
teamReviewers = append(teamReviewers, teamReviewerStr)
}
}
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.CreateReviewRequests(owner, repo, int64(index), gitea_sdk.PullReviewRequestOptions{
Reviewers: reviewers,
TeamReviewers: teamReviewers,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("create review requests for %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
}
// Return a success message instead of the Response object which contains non-serializable functions
successMsg := map[string]interface{}{
"message": "Successfully created review requests",
"reviewers": reviewers,
"team_reviewers": teamReviewers,
"pr_index": int64(index),
"repository": fmt.Sprintf("%s/%s", owner, repo),
}
return to.TextResult(successMsg)
}

363
operation/wiki/wiki.go Normal file
View File

@@ -0,0 +1,363 @@
package wiki
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
ListWikiPagesToolName = "list_wiki_pages"
GetWikiPageToolName = "get_wiki_page"
GetWikiRevisionsToolName = "get_wiki_revisions"
CreateWikiPageToolName = "create_wiki_page"
UpdateWikiPageToolName = "update_wiki_page"
DeleteWikiPageToolName = "delete_wiki_page"
)
var (
ListWikiPagesTool = mcp.NewTool(
ListWikiPagesToolName,
mcp.WithDescription("List all wiki pages in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetWikiPageTool = mcp.NewTool(
GetWikiPageToolName,
mcp.WithDescription("Get a wiki page content and metadata"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")),
)
GetWikiRevisionsTool = mcp.NewTool(
GetWikiRevisionsToolName,
mcp.WithDescription("Get revisions history of a wiki page"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")),
)
CreateWikiPageTool = mcp.NewTool(
CreateWikiPageToolName,
mcp.WithDescription("Create a new wiki page"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("title", mcp.Required(), mcp.Description("wiki page title")),
mcp.WithString("content_base64", mcp.Required(), mcp.Description("page content, base64 encoded")),
mcp.WithString("message", mcp.Description("commit message (optional)")),
)
UpdateWikiPageTool = mcp.NewTool(
UpdateWikiPageToolName,
mcp.WithDescription("Update an existing wiki page"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("pageName", mcp.Required(), mcp.Description("current wiki page name")),
mcp.WithString("title", mcp.Description("new page title (optional)")),
mcp.WithString("content_base64", mcp.Required(), mcp.Description("page content, base64 encoded")),
mcp.WithString("message", mcp.Description("commit message (optional)")),
)
DeleteWikiPageTool = mcp.NewTool(
DeleteWikiPageToolName,
mcp.WithDescription("Delete a wiki page"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name to delete")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: ListWikiPagesTool,
Handler: ListWikiPagesFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetWikiPageTool,
Handler: GetWikiPageFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetWikiRevisionsTool,
Handler: GetWikiRevisionsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: CreateWikiPageTool,
Handler: CreateWikiPageFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: UpdateWikiPageTool,
Handler: UpdateWikiPageFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteWikiPageTool,
Handler: DeleteWikiPageFn,
})
}
func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListWikiPagesFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
// Use direct HTTP request because SDK does not support yet wikis
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/pages", url.QueryEscape(owner), url.QueryEscape(repo)), nil)
if err != nil {
return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err))
}
return to.TextResult(result)
}
func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
pageName, ok := req.GetArguments()["pageName"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
if err != nil {
return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err))
}
return to.TextResult(result)
}
func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWikiRevisionsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
pageName, ok := req.GetArguments()["pageName"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/revisions/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
if err != nil {
return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err))
}
return to.TextResult(result)
}
func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
title, ok := req.GetArguments()["title"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("title is required"))
}
contentBase64, ok := req.GetArguments()["content_base64"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("content_base64 is required"))
}
message, _ := req.GetArguments()["message"].(string)
if message == "" {
message = fmt.Sprintf("Create wiki page '%s'", title)
}
requestBody := map[string]string{
"title": title,
"content_base64": contentBase64,
"message": message,
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
result, err := makeWikiAPIRequest(ctx, client, "POST", fmt.Sprintf("repos/%s/%s/wiki/new", url.QueryEscape(owner), url.QueryEscape(repo)), requestBody)
if err != nil {
return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err))
}
return to.TextResult(result)
}
func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UpdateWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
pageName, ok := req.GetArguments()["pageName"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required"))
}
contentBase64, ok := req.GetArguments()["content_base64"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("content_base64 is required"))
}
requestBody := map[string]string{
"content_base64": contentBase64,
}
// If title is given, use it. Otherwise, keep current page name
if title, ok := req.GetArguments()["title"].(string); ok && title != "" {
requestBody["title"] = title
} else {
// Utiliser pageName comme fallback pour éviter "unnamed"
requestBody["title"] = pageName
}
if message, ok := req.GetArguments()["message"].(string); ok && message != "" {
requestBody["message"] = message
} else {
requestBody["message"] = fmt.Sprintf("Update wiki page '%s'", pageName)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
result, err := makeWikiAPIRequest(ctx, client, "PATCH", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), requestBody)
if err != nil {
return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err))
}
return to.TextResult(result)
}
func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
pageName, ok := req.GetArguments()["pageName"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = makeWikiAPIRequest(ctx, client, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err))
}
return to.TextResult(map[string]string{"message": "Wiki page deleted successfully"})
}
// Helper function to make HTTP requests to Gitea Wiki API
func makeWikiAPIRequest(ctx context.Context, client interface{}, method, path string, body interface{}) (interface{}, error) {
// Use flags to get base URL and token
apiURL := fmt.Sprintf("%s/api/v1/%s", flag.Host, path)
httpClient := &http.Client{}
var reqBody io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = strings.NewReader(string(bodyBytes))
}
req, err := http.NewRequestWithContext(ctx, method, apiURL, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("token %s", flag.Token))
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode)
}
if method == "DELETE" {
return map[string]string{"message": "success"}, nil
}
var result interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return result, nil
}

View File

@@ -48,7 +48,7 @@ func Default() *zap.Logger {
MaxAge: 30, MaxAge: 30,
})) }))
if flag.Mode == "http" || flag.Mode == "sse" { if flag.Mode == "http" {
wss = append(wss, zapcore.AddSync(os.Stdout)) wss = append(wss, zapcore.AddSync(os.Stdout))
} }