Compare commits

..
Author SHA1 Message Date
gronod 9bfd55f97d fix(ci): login to registry with PAT secret
release-nightly / release-image (push) Successful in 2m32s
The actions task token is rejected by the container registry
(unauthorized at /v2/); use the REGISTRY_TOKEN repo secret instead.

Assisted-by: Devin:SWE-2-Medium
2026-09-15 15:19:21 +01:00
gronod 290ad8da5d chore(release): publish to self-hosted gitea instance
check-and-test / check-and-test (pull_request) Successful in 4m50s
release-nightly / release-image (push) Failing after 1m1s
Repoint image pushes to git.i3omb.com/gronod/gitea-mcp-server using
GITHUB_TOKEN auth, drop the Cloudflare R2 mirror, and update the docs
and OCI source label to match.

Assisted-by: Devin:SWE-2 High
2026-09-15 13:58:13 +01:00
11 changed files with 43 additions and 461 deletions
+7 -7
View File
@@ -10,8 +10,8 @@ jobs:
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: nightly
REGISTRY: git.i3omb.com
IMAGE_NAME: gronod/gitea-mcp-server
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -24,16 +24,16 @@ jobs:
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Login to DockerHub
- name: Login to container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
registry: ${{ env.REGISTRY }}
username: gronod
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
@@ -46,6 +46,6 @@ jobs:
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly
build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
+8 -33
View File
@@ -13,16 +13,6 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
# The R2 upload only runs after goreleaser has already published
# the Gitea release, so fail early instead if the secrets are
# missing.
- name: Check R2 configuration
run: sh scripts/upload-r2.sh --check-config
env:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
@@ -35,27 +25,12 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_FORCE_TOKEN: "gitea"
# goreleaser `publishers:` is a Pro-only feature, so the release
# artifacts are mirrored to Cloudflare R2 here instead.
- name: Upload binaries to Cloudflare R2
env:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
VERSION: ${{ github.ref_name }}
run: |
for f in dist/*.tar.gz dist/*.zip dist/checksums.txt; do
[ -f "$f" ] || continue
echo "uploading $f"
sh scripts/upload-r2.sh "$f" "gitea-mcp/${VERSION#v}/$(basename "$f")"
done
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: latest
REGISTRY: git.i3omb.com
IMAGE_NAME: gronod/gitea-mcp-server
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -68,16 +43,16 @@ jobs:
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Login to DockerHub
- name: Login to container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
registry: ${{ env.REGISTRY }}
username: gronod
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push
@@ -92,5 +67,5 @@ jobs:
build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+2 -2
View File
@@ -71,6 +71,6 @@ release:
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
gitea_urls:
api: https://gitea.com/api/v1
download: https://gitea.com
api: https://git.i3omb.com/api/v1
download: https://git.i3omb.com
force_token: gitea
+1 -1
View File
@@ -30,7 +30,7 @@ COPY --from=builder --chown=nonroot:nonroot /app/gitea-mcp .
USER nonroot:nonroot
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.source="https://gitea.com/gitea/gitea-mcp"
LABEL org.opencontainers.image.source="https://git.i3omb.com/gronod/gitea-mcp"
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app/gitea-mcp", "-healthcheck"] || exit 1
+7 -7
View File
@@ -4,14 +4,14 @@
**Gitea MCP Server** connects a [Gitea](https://about.gitea.com) instance to [Model Context Protocol](https://modelcontextprotocol.io) clients, so repositories, issues, pull requests and more can be browsed and managed from an MCP-compatible chat interface.
[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22git.i3omb.com/gronod/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22git.i3omb.com/gronod/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
## Installation
Download a binary from the [releases page](https://gitea.com/gitea/gitea-mcp/releases) and put it in your `PATH`, use the `docker.gitea.com/gitea-mcp-server` image, or build from source into `$GOPATH/bin` with `make` and Go 1.27 or later:
Download a binary from the [releases page](https://git.i3omb.com/gronod/gitea-mcp/releases) and put it in your `PATH`, use the `git.i3omb.com/gronod/gitea-mcp-server` image, or build from source into `$GOPATH/bin` with `make` and Go 1.27 or later:
```bash
git clone https://gitea.com/gitea/gitea-mcp.git
git clone https://git.i3omb.com/gronod/gitea-mcp.git
cd gitea-mcp
make install
```
@@ -57,7 +57,7 @@ Use the install buttons at the top of this README, or add the block below to you
"servers": {
"gitea-mcp": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITEA_ACCESS_TOKEN", "docker.gitea.com/gitea-mcp-server"],
"args": ["run", "-i", "--rm", "-e", "GITEA_ACCESS_TOKEN", "git.i3omb.com/gronod/gitea-mcp-server"],
"env": {
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
}
@@ -93,7 +93,7 @@ Add the following to `~/.vibe/config.toml`:
name = "gitea"
transport = "stdio"
command = "docker"
args = ["run", "--rm", "-i", "-e", "GITEA_ACCESS_TOKEN", "-e", "GITEA_HOST", "docker.gitea.com/gitea-mcp-server"]
args = ["run", "--rm", "-i", "-e", "GITEA_ACCESS_TOKEN", "-e", "GITEA_HOST", "git.i3omb.com/gronod/gitea-mcp-server"]
[mcp_servers.env]
GITEA_ACCESS_TOKEN = "TOKEN"
@@ -160,8 +160,8 @@ Once configured, try `list all my repositories` in the chat box.
| package_write | packages | Write | Delete a package version (irreversible) |
| list_issues | issue | Read | List repository issues |
| attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content |
| issue_read | issue | Read | Read issue: details, comments, labels, or blocking relationships |
| issue_write | issue | Write | Write issues: create, update, manage comments, labels, and blocking relationships |
| issue_read | issue | Read | Read issue: details, comments, or labels |
| issue_write | issue | Write | Write issues: create, update, manage comments and labels |
| list_pull_requests | pull_request | Read | List repository pull requests |
| pull_request_read | pull_request | Read | Read pull request: details, diff, files, status, reviews, review comments |
| pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers |
+7 -7
View File
@@ -4,14 +4,14 @@
**Gitea MCP 服务器** 将 [Gitea](https://about.gitea.com) 实例接入 [Model Context Protocol](https://modelcontextprotocol.io) 客户端,让仓库、问题、拉取请求等都能在兼容 MCP 的聊天界面中浏览和管理。
[![在 VS Code 中使用 Docker 安装](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![在 VS Code Insiders 中使用 Docker 安装](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
[![在 VS Code 中使用 Docker 安装](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22git.i3omb.com/gronod/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![在 VS Code Insiders 中使用 Docker 安装](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22git.i3omb.com/gronod/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
## 安装
可从 [发布页面](https://gitea.com/gitea/gitea-mcp/releases) 下载二进制文件并放入 `PATH`,或使用 `docker.gitea.com/gitea-mcp-server` 镜像,也可用 `make` 和 Go 1.27 及以上从源码构建到 `$GOPATH/bin`:
可从 [发布页面](https://git.i3omb.com/gronod/gitea-mcp/releases) 下载二进制文件并放入 `PATH`,或使用 `git.i3omb.com/gronod/gitea-mcp-server` 镜像,也可用 `make` 和 Go 1.27 及以上从源码构建到 `$GOPATH/bin`:
```bash
git clone https://gitea.com/gitea/gitea-mcp.git
git clone https://git.i3omb.com/gronod/gitea-mcp.git
cd gitea-mcp
make install
```
@@ -57,7 +57,7 @@ claude mcp add --transport stdio --scope user gitea \
"servers": {
"gitea-mcp": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITEA_ACCESS_TOKEN", "docker.gitea.com/gitea-mcp-server"],
"args": ["run", "-i", "--rm", "-e", "GITEA_ACCESS_TOKEN", "git.i3omb.com/gronod/gitea-mcp-server"],
"env": {
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
}
@@ -93,7 +93,7 @@ claude mcp add --transport stdio --scope user gitea \
name = "gitea"
transport = "stdio"
command = "docker"
args = ["run", "--rm", "-i", "-e", "GITEA_ACCESS_TOKEN", "-e", "GITEA_HOST", "docker.gitea.com/gitea-mcp-server"]
args = ["run", "--rm", "-i", "-e", "GITEA_ACCESS_TOKEN", "-e", "GITEA_HOST", "git.i3omb.com/gronod/gitea-mcp-server"]
[mcp_servers.env]
GITEA_ACCESS_TOKEN = "TOKEN"
@@ -160,8 +160,8 @@ Cursor 等客户端可使用 stdio 命令:
| package_write | packages | 写入 | 删除软件包版本(不可恢复) |
| list_issues | issue | 读取 | 列出仓库问题 |
| attachment_read | issue | 读取 | 读取问题/评论附件:列出元数据、获取元数据或下载内容 |
| issue_read | issue | 读取 | 读取问题:详情、评论、标签或阻塞关系 |
| issue_write | issue | 写入 | 写入问题:创建、更新、管理评论、标签和阻塞关系 |
| issue_read | issue | 读取 | 读取问题:详情、评论或标签 |
| issue_write | issue | 写入 | 写入问题:创建、更新、管理评论和标签 |
| list_pull_requests | pull_request | 读取 | 列出仓库拉取请求 |
| pull_request_read | pull_request | 读取 | 读取拉取请求:详情、差异、变更文件、头部提交状态、审查、审查评论 |
| pull_request_write | pull_request | 写入 | 写入拉取请求:创建、更新、关闭、重新打开、合并、更新分支、管理审查者 |
+7 -7
View File
@@ -4,14 +4,14 @@
**Gitea MCP 伺服器** 將 [Gitea](https://about.gitea.com) 實例接入 [Model Context Protocol](https://modelcontextprotocol.io) 客戶端,讓倉庫、問題、拉取請求等都能在相容 MCP 的聊天介面中瀏覽與管理。
[![在 VS Code 中使用 Docker 安裝](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![在 VS Code Insiders 中使用 Docker 安裝](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
[![在 VS Code 中使用 Docker 安裝](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22git.i3omb.com/gronod/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![在 VS Code Insiders 中使用 Docker 安裝](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22git.i3omb.com/gronod/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
## 安裝
可從 [發布頁面](https://gitea.com/gitea/gitea-mcp/releases) 下載二進位檔並放入 `PATH`,或使用 `docker.gitea.com/gitea-mcp-server` 映像檔,也可用 `make` 與 Go 1.27 以上從原始碼建置到 `$GOPATH/bin`:
可從 [發布頁面](https://git.i3omb.com/gronod/gitea-mcp/releases) 下載二進位檔並放入 `PATH`,或使用 `git.i3omb.com/gronod/gitea-mcp-server` 映像檔,也可用 `make` 與 Go 1.27 以上從原始碼建置到 `$GOPATH/bin`:
```bash
git clone https://gitea.com/gitea/gitea-mcp.git
git clone https://git.i3omb.com/gronod/gitea-mcp.git
cd gitea-mcp
make install
```
@@ -57,7 +57,7 @@ claude mcp add --transport stdio --scope user gitea \
"servers": {
"gitea-mcp": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITEA_ACCESS_TOKEN", "docker.gitea.com/gitea-mcp-server"],
"args": ["run", "-i", "--rm", "-e", "GITEA_ACCESS_TOKEN", "git.i3omb.com/gronod/gitea-mcp-server"],
"env": {
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
}
@@ -93,7 +93,7 @@ claude mcp add --transport stdio --scope user gitea \
name = "gitea"
transport = "stdio"
command = "docker"
args = ["run", "--rm", "-i", "-e", "GITEA_ACCESS_TOKEN", "-e", "GITEA_HOST", "docker.gitea.com/gitea-mcp-server"]
args = ["run", "--rm", "-i", "-e", "GITEA_ACCESS_TOKEN", "-e", "GITEA_HOST", "git.i3omb.com/gronod/gitea-mcp-server"]
[mcp_servers.env]
GITEA_ACCESS_TOKEN = "TOKEN"
@@ -160,8 +160,8 @@ Cursor 等客戶端可使用 stdio 命令:
| package_write | packages | 寫入 | 刪除軟體套件版本(不可復原) |
| list_issues | issue | 讀取 | 列出倉庫問題 |
| attachment_read | issue | 讀取 | 讀取問題/評論附件:列出中繼資料、取得中繼資料或下載內容 |
| issue_read | issue | 讀取 | 讀取問題:詳情、評論、標籤或阻塞關係 |
| issue_write | issue | 寫入 | 寫入問題:創建、更新、管理評論、標籤和阻塞關係 |
| issue_read | issue | 讀取 | 讀取問題:詳情、評論或標籤 |
| issue_write | issue | 寫入 | 寫入問題:創建、更新、管理評論和標籤 |
| list_pull_requests | pull_request | 讀取 | 列出倉庫拉取請求 |
| pull_request_read | pull_request | 讀取 | 讀取拉取請求:詳情、差異、變更檔案、頭部提交狀態、審查、審查評論 |
| pull_request_write | pull_request | 寫入 | 寫入拉取請求:創建、更新、關閉、重新開啟、合併、更新分支、管理審查者 |
-134
View File
@@ -1,134 +0,0 @@
package issue
import (
"context"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "gitea.dev/sdk"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func listIssueBlocksFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize}
issues, _, err := client.Issues.ListIssueBlocks(ctx, owner, repo, index, gitea_sdk.ListIssueBlocksOptions{
ListOptions: listOpt,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/blocks err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssues(issues))
}
func blockIssueFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueBlockingFn(ctx, args, true)
}
func unblockIssueFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueBlockingFn(ctx, args, false)
}
func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
blocked, err := params.GetIndex(args, "blocked_issue")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
meta := gitea_sdk.IssueMeta{Index: blocked}
var issue *gitea_sdk.Issue
if block {
issue, _, err = client.Issues.CreateIssueBlocking(ctx, owner, repo, index, meta)
} else {
issue, _, err = client.Issues.RemoveIssueBlocking(ctx, owner, repo, index, meta)
}
if err != nil {
return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/blocks err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssue(issue))
}
func listIssueDependenciesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize}
issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{
ListOptions: listOpt,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssues(issues))
}
func addIssueDependencyFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueDependencyFn(ctx, args, true)
}
func removeIssueDependencyFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueDependencyFn(ctx, args, false)
}
func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
blocking, err := params.GetIndex(args, "blocking_issue")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
meta := gitea_sdk.IssueMeta{Index: blocking}
var issue *gitea_sdk.Issue
if add {
issue, _, err = client.Issues.CreateIssueDependency(ctx, owner, repo, index, meta)
} else {
issue, _, err = client.Issues.RemoveIssueDependency(ctx, owner, repo, index, meta)
}
if err != nil {
return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssue(issue))
}
func issueRef(args map[string]any) (owner, repo string, index int64, err error) {
if owner, err = params.GetString(args, "owner"); err != nil {
return "", "", 0, err
}
if repo, err = params.GetString(args, "repo"); err != nil {
return "", "", 0, err
}
if index, err = params.GetIndex(args, "issue_number"); err != nil {
return "", "", 0, err
}
return owner, repo, index, nil
}
-147
View File
@@ -1,147 +0,0 @@
package issue
import (
"context"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func Test_issueBlocking(t *testing.T) {
const (
owner = "octo"
repo = "demo"
)
blocksPath := fmt.Sprintf("/api/v1/repos/%s/%s/issues/42/blocks", owner, repo)
depsPath := fmt.Sprintf("/api/v1/repos/%s/%s/issues/42/dependencies", owner, repo)
tests := []struct {
name string
fn func(context.Context, map[string]any) (*mcp.CallToolResult, error)
args map[string]any
wantPath string
wantMethod string
wantQuery string
wantIndex float64
}{
{
name: "list blocks paginates",
fn: listIssueBlocksFn,
args: map[string]any{"page": float64(2), "per_page": float64(10)},
wantPath: blocksPath,
wantMethod: http.MethodGet,
wantQuery: "limit=10&page=2",
},
{
name: "block issue posts index",
fn: blockIssueFn,
args: map[string]any{"blocked_issue": float64(7)},
wantPath: blocksPath,
wantMethod: http.MethodPost,
wantIndex: 7,
},
{
name: "unblock issue deletes with index",
fn: unblockIssueFn,
args: map[string]any{"blocked_issue": float64(7)},
wantPath: blocksPath,
wantMethod: http.MethodDelete,
wantIndex: 7,
},
{
name: "list dependencies paginates",
fn: listIssueDependenciesFn,
args: map[string]any{"page": float64(3), "per_page": float64(5)},
wantPath: depsPath,
wantMethod: http.MethodGet,
wantQuery: "limit=5&page=3",
},
{
name: "add dependency posts index",
fn: addIssueDependencyFn,
args: map[string]any{"blocking_issue": float64(9)},
wantPath: depsPath,
wantMethod: http.MethodPost,
wantIndex: 9,
},
{
name: "remove dependency deletes with index",
fn: removeIssueDependencyFn,
args: map[string]any{"blocking_issue": float64(9)},
wantPath: depsPath,
wantMethod: http.MethodDelete,
wantIndex: 9,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
gotMethod string
gotPath string
gotQuery string
gotBody map[string]any
)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.27.2"}`))
case blocksPath, depsPath:
gotMethod = r.Method
gotPath = r.URL.Path
gotQuery = r.URL.RawQuery
_ = json.NewDecoder(r.Body).Decode(&gotBody)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
_, _ = w.Write([]byte(`[{"number":7,"title":"blocked","state":"open"}]`))
} else {
_, _ = w.Write([]byte(`{"number":7,"title":"blocked","state":"open"}`))
}
default:
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler)
defer server.Close()
origHost, origToken, origVersion := flag.Host, flag.Token, flag.Version
flag.Host, flag.Token, flag.Version = server.URL, "", "test"
defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }()
args := map[string]any{"owner": owner, "repo": repo, "issue_number": float64(42)}
maps.Copy(args, tt.args)
res, err := tt.fn(context.Background(), args)
if err != nil {
t.Fatalf("handler error = %v", err)
}
if res.IsError {
t.Fatalf("unexpected error result: %v", res.Content)
}
if gotMethod != tt.wantMethod {
t.Fatalf("method = %s, want %s", gotMethod, tt.wantMethod)
}
if gotPath != tt.wantPath {
t.Fatalf("path = %s, want %s", gotPath, tt.wantPath)
}
if tt.wantQuery != "" && gotQuery != tt.wantQuery {
t.Fatalf("query = %s, want %s", gotQuery, tt.wantQuery)
}
if tt.wantIndex != 0 && gotBody["index"] != tt.wantIndex {
t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex)
}
body := res.Content[0].(*mcp.TextContent).Text
if !strings.Contains(body, `"number":7`) && !strings.Contains(body, `"number": 7`) {
t.Fatalf("expected slim issue in output, got: %s", body)
}
})
}
}
+4 -20
View File
@@ -55,21 +55,19 @@ var (
IssueReadTool = tool.NewDefinition(
IssueReadToolName,
"Read issue: details, comments, labels, or blocking relationships.",
"Read issue: details, comments, or labels.",
annotation.ReadOnly("Read issue details"),
tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels", "list_blocks", "list_dependencies")),
tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels")),
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
tool.Number("issue_number", tool.Required()),
tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)),
tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)),
)
IssueWriteTool = tool.NewDefinition(
IssueWriteToolName,
"Write issues: create, update, manage comments, labels, and blocking relationships.",
"Write issues: create, update, manage comments and labels.",
annotation.Write("Create or update issues, comments, and labels"),
tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels", "block_issue", "unblock_issue", "add_dependency", "remove_dependency")),
tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels")),
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
tool.Number("issue_number", tool.Description("required except for 'create'")),
@@ -84,8 +82,6 @@ var (
tool.String("ref", tool.Description("branch to associate")),
tool.String("deadline", tool.Description("ISO 8601")),
tool.Boolean("remove_deadline"),
tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")),
tool.Number("blocking_issue", tool.Description("issue number that blocks issue_number (required for 'add_dependency'/'remove_dependency')")),
)
)
@@ -116,10 +112,6 @@ func issueReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult,
return getIssueCommentsByIndexFn(ctx, args)
case "get_labels":
return getIssueLabelsFn(ctx, args)
case "list_blocks":
return listIssueBlocksFn(ctx, args)
case "list_dependencies":
return listIssueDependenciesFn(ctx, args)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
@@ -147,14 +139,6 @@ func issueWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult
return replaceIssueLabelsFn(ctx, args)
case "clear_labels":
return clearIssueLabelsFn(ctx, args)
case "block_issue":
return blockIssueFn(ctx, args)
case "unblock_issue":
return unblockIssueFn(ctx, args)
case "add_dependency":
return addIssueDependencyFn(ctx, args)
case "remove_dependency":
return removeIssueDependencyFn(ctx, args)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
-96
View File
@@ -1,96 +0,0 @@
#!/bin/sh
# Copyright 2026 The Gitea Authors. All rights reserved.
# SPDX-License-Identifier: MIT
#
# upload-r2.sh uploads a single local file to a single object key in a
# Cloudflare R2 bucket, using curl's built-in AWS SigV4 signer (R2 is
# S3-API compatible).
#
# Usage:
# upload-r2.sh <local-file> <remote-key>
# upload-r2.sh --check-config
#
# The second form only validates that the required environment
# variables below are set (it does not touch the network), and is
# meant to be run as an early preflight step in CI so that a missing
# R2_* secret is reported before anything is built or published.
#
# Required environment variables:
# R2_ENDPOINT Base URL of the R2 endpoint, e.g.
# https://<account>.r2.cloudflarestorage.com
# R2_BUCKET Destination bucket name.
# R2_ACCESS_KEY_ID R2 access key id.
# R2_SECRET_ACCESS_KEY R2 secret access key.
set -eu
# check_env validates that all required R2_* environment variables are
# set and non-empty, so the validation logic only exists in one place
# for both the normal upload mode and --check-config.
check_env() {
missing=""
if [ -z "${R2_ENDPOINT:-}" ]; then
missing="$missing R2_ENDPOINT"
fi
if [ -z "${R2_BUCKET:-}" ]; then
missing="$missing R2_BUCKET"
fi
if [ -z "${R2_ACCESS_KEY_ID:-}" ]; then
missing="$missing R2_ACCESS_KEY_ID"
fi
if [ -z "${R2_SECRET_ACCESS_KEY:-}" ]; then
missing="$missing R2_SECRET_ACCESS_KEY"
fi
if [ -n "$missing" ]; then
echo "upload-r2.sh: missing required environment variable(s):$missing" >&2
exit 1
fi
}
if [ "$#" -eq 1 ] && [ "$1" = "--check-config" ]; then
check_env
echo "upload-r2.sh: R2 configuration OK"
exit 0
fi
if [ "$#" -ne 2 ]; then
echo "usage: upload-r2.sh <local-file> <remote-key>" >&2
echo " upload-r2.sh --check-config" >&2
exit 1
fi
local_file="$1"
remote_key="$2"
if [ ! -f "$local_file" ]; then
echo "upload-r2.sh: local file not found: $local_file" >&2
exit 1
fi
check_env
# Strip a single trailing slash from the endpoint, if present, so that
# building the path-style URL below never produces a double slash.
endpoint="${R2_ENDPOINT%/}"
url="$endpoint/$R2_BUCKET/$remote_key"
# Credentials are passed to curl through a config file read from
# stdin rather than as a command-line argument, so they never show up
# in `ps` output.
#
# --fail-with-body (instead of plain --fail) still exits non-zero on
# HTTP errors, but also prints R2's XML error body, which is where the
# actual error code lives (SignatureDoesNotMatch, NoSuchBucket,
# AccessDenied, ...). --retry 3 (without --retry-all-errors) only
# retries the transient cases (5xx, 408, 429, connection failures).
printf 'user = "%s:%s"\n' "$R2_ACCESS_KEY_ID" "$R2_SECRET_ACCESS_KEY" | curl \
--config - \
--fail-with-body \
--silent \
--show-error \
--retry 3 \
--aws-sigv4 "aws:amz:auto:s3" \
--upload-file "$local_file" \
"$url"