Compare commits

...
Author SHA1 Message Date
gronod 07949c1a63 fix(issue): satisfy embedlit lint in check_blocked
check-and-test / check-and-test (pull_request) Successful in 4m23s
release-nightly / release-image (push) Successful in 3m8s
Use the same ListOptions local as the other list helpers instead of
embedding gitea_sdk.ListOptions in the composite literal.
2026-09-16 08:20:35 +00:00
gronod 3e093c10b9 feat(issue): support cross-repo dependencies and check_blocked
check-and-test / check-and-test (pull_request) Failing after 2m25s
Send blocked_owner/blocked_repo and blocking_owner/blocking_repo in the
IssueMeta body so Gitea can resolve cross-repository links. Default those
fields to the path issue's owner/repo.

Add issue_read method check_blocked to summarize open blockers, and
document that dependency endpoints 404 when the repo has issue
dependencies disabled.
2026-09-16 08:03:23 +00:00
gronod 10f5a345d8 fix(issue): include owner and repo in dependency request bodies
check-and-test / check-and-test (pull_request) Successful in 2m23s
release-nightly / release-image (push) Successful in 2m58s
Gitea resolves the IssueMeta body against owner+repo, not only the
URL. The SDK type only has Index, so block/unblock/add/remove
dependency calls looked up a repo named "". Send the path owner/repo
in the JSON body via DoJSON.

Assisted-by: Devin:SWE-2 Medium
2026-09-15 16:20:56 +01:00
gronod 554b7490e2 Merge pull request 'feat(issue): add issue blocking methods' (#2) from feat/issue-blocks into main
release-nightly / release-image (push) Successful in 2m28s
Reviewed-on: #2
2026-09-15 15:39:53 +01:00
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 175 additions and 183 deletions
+7 -7
View File
@@ -10,8 +10,8 @@ jobs:
release-image: release-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_ORG: gitea REGISTRY: git.i3omb.com
DOCKER_LATEST: nightly IMAGE_NAME: gronod/gitea-mcp-server
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -24,16 +24,16 @@ jobs:
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Login to DockerHub - name: Login to container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with: with:
username: ${{ secrets.DOCKER_USER }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKER_TOKEN }} username: gronod
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Get Meta - name: Get Meta
id: meta id: meta
run: | 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 echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
@@ -46,6 +46,6 @@ jobs:
linux/arm64 linux/arm64
push: true push: true
tags: | tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly
build-args: | build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }} VERSION=${{ steps.meta.outputs.REPO_VERSION }}
+8 -33
View File
@@ -13,16 +13,6 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
fetch-depth: 0 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 - name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
@@ -35,27 +25,12 @@ jobs:
env: env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_FORCE_TOKEN: "gitea" 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: release-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_ORG: gitea REGISTRY: git.i3omb.com
DOCKER_LATEST: latest IMAGE_NAME: gronod/gitea-mcp-server
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -68,16 +43,16 @@ jobs:
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Login to DockerHub - name: Login to container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with: with:
username: ${{ secrets.DOCKER_USER }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKER_TOKEN }} username: gronod
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Get Meta - name: Get Meta
id: meta id: meta
run: | run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
@@ -92,5 +67,5 @@ jobs:
build-args: | build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }} VERSION=${{ steps.meta.outputs.REPO_VERSION }}
tags: | tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ steps.meta.outputs.REPO_VERSION }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+2 -2
View File
@@ -71,6 +71,6 @@ release:
Released by [GoReleaser](https://github.com/goreleaser/goreleaser). Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
gitea_urls: gitea_urls:
api: https://gitea.com/api/v1 api: https://git.i3omb.com/api/v1
download: https://gitea.com download: https://git.i3omb.com
force_token: gitea force_token: gitea
+1 -1
View File
@@ -30,7 +30,7 @@ COPY --from=builder --chown=nonroot:nonroot /app/gitea-mcp .
USER nonroot:nonroot USER nonroot:nonroot
LABEL org.opencontainers.image.version="${VERSION}" 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 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app/gitea-mcp", "-healthcheck"] || exit 1 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. **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 ## 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 ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://git.i3omb.com/gronod/gitea-mcp.git
cd gitea-mcp cd gitea-mcp
make install make install
``` ```
@@ -57,7 +57,7 @@ Use the install buttons at the top of this README, or add the block below to you
"servers": { "servers": {
"gitea-mcp": { "gitea-mcp": {
"command": "docker", "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": { "env": {
"GITEA_ACCESS_TOKEN": "${input:gitea_token}" "GITEA_ACCESS_TOKEN": "${input:gitea_token}"
} }
@@ -93,7 +93,7 @@ Add the following to `~/.vibe/config.toml`:
name = "gitea" name = "gitea"
transport = "stdio" transport = "stdio"
command = "docker" 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] [mcp_servers.env]
GITEA_ACCESS_TOKEN = "TOKEN" 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) | | package_write | packages | Write | Delete a package version (irreversible) |
| list_issues | issue | Read | List repository issues | | list_issues | issue | Read | List repository issues |
| attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content | | 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_read | issue | Read | Read issue: details, comments, labels, or dependency/blocking links |
| issue_write | issue | Write | Write issues: create, update, manage comments, labels, and blocking relationships | | issue_write | issue | Write | Write issues: create, update, manage comments, labels, and dependency/blocking links |
| list_pull_requests | pull_request | Read | List repository pull requests | | 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_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 | | pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers |
+5 -5
View File
@@ -4,14 +4,14 @@
**Gitea MCP 服务器** 将 [Gitea](https://about.gitea.com) 实例接入 [Model Context Protocol](https://modelcontextprotocol.io) 客户端,让仓库、问题、拉取请求等都能在兼容 MCP 的聊天界面中浏览和管理。 **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 ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://git.i3omb.com/gronod/gitea-mcp.git
cd gitea-mcp cd gitea-mcp
make install make install
``` ```
@@ -57,7 +57,7 @@ claude mcp add --transport stdio --scope user gitea \
"servers": { "servers": {
"gitea-mcp": { "gitea-mcp": {
"command": "docker", "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": { "env": {
"GITEA_ACCESS_TOKEN": "${input:gitea_token}" "GITEA_ACCESS_TOKEN": "${input:gitea_token}"
} }
@@ -93,7 +93,7 @@ claude mcp add --transport stdio --scope user gitea \
name = "gitea" name = "gitea"
transport = "stdio" transport = "stdio"
command = "docker" 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] [mcp_servers.env]
GITEA_ACCESS_TOKEN = "TOKEN" GITEA_ACCESS_TOKEN = "TOKEN"
+5 -5
View File
@@ -4,14 +4,14 @@
**Gitea MCP 伺服器** 將 [Gitea](https://about.gitea.com) 實例接入 [Model Context Protocol](https://modelcontextprotocol.io) 客戶端,讓倉庫、問題、拉取請求等都能在相容 MCP 的聊天介面中瀏覽與管理。 **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 ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://git.i3omb.com/gronod/gitea-mcp.git
cd gitea-mcp cd gitea-mcp
make install make install
``` ```
@@ -57,7 +57,7 @@ claude mcp add --transport stdio --scope user gitea \
"servers": { "servers": {
"gitea-mcp": { "gitea-mcp": {
"command": "docker", "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": { "env": {
"GITEA_ACCESS_TOKEN": "${input:gitea_token}" "GITEA_ACCESS_TOKEN": "${input:gitea_token}"
} }
@@ -93,7 +93,7 @@ claude mcp add --transport stdio --scope user gitea \
name = "gitea" name = "gitea"
transport = "stdio" transport = "stdio"
command = "docker" 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] [mcp_servers.env]
GITEA_ACCESS_TOKEN = "TOKEN" GITEA_ACCESS_TOKEN = "TOKEN"
+68 -21
View File
@@ -3,6 +3,8 @@ package issue
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"net/url"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/params"
@@ -12,6 +14,14 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
// issueMeta carries owner/repo in the body; the SDK's IssueMeta only has Index
// and Gitea resolves the body repo from those fields.
type issueMeta struct {
Index int64 `json:"index"`
Owner string `json:"owner"`
Repo string `json:"repo"`
}
func listIssueBlocksFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { func listIssueBlocksFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args) owner, repo, index, err := issueRef(args)
if err != nil { if err != nil {
@@ -49,21 +59,17 @@ func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (*
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) blockedOwner, blockedRepo := relatedIssueRepo(args, "blocked_owner", "blocked_repo", owner, repo)
if err != nil { method := http.MethodPost
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) if !block {
method = http.MethodDelete
} }
meta := gitea_sdk.IssueMeta{Index: blocked} var issue gitea_sdk.Issue
var issue *gitea_sdk.Issue path := fmt.Sprintf("repos/%s/%s/issues/%d/blocks", url.PathEscape(owner), url.PathEscape(repo), index)
if block { if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocked, Owner: blockedOwner, Repo: blockedRepo}, &issue); err != nil {
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.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/blocks err: %v", owner, repo, index, err))
} }
return to.TextResult(slimIssue(issue)) return to.TextResult(slimIssue(&issue))
} }
func listIssueDependenciesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { func listIssueDependenciesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
@@ -103,21 +109,62 @@ func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (*
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
blockingOwner, blockingRepo := relatedIssueRepo(args, "blocking_owner", "blocking_repo", owner, repo)
method := http.MethodPost
if !add {
method = http.MethodDelete
}
var issue gitea_sdk.Issue
path := fmt.Sprintf("repos/%s/%s/issues/%d/dependencies", url.PathEscape(owner), url.PathEscape(repo), index)
if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocking, Owner: blockingOwner, Repo: blockingRepo}, &issue); 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 checkIssueBlockedFn(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) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
meta := gitea_sdk.IssueMeta{Index: blocking} listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize}
var issue *gitea_sdk.Issue issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{
if add { ListOptions: listOpt,
issue, _, err = client.Issues.CreateIssueDependency(ctx, owner, repo, index, meta) })
} else {
issue, _, err = client.Issues.RemoveIssueDependency(ctx, owner, repo, index, meta)
}
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err))
} }
return to.TextResult(slimIssue(issue)) openBlockers := make([]map[string]any, 0)
for _, issue := range issues {
if issue == nil {
continue
}
if issue.State == gitea_sdk.StateOpen || issue.State == "" {
openBlockers = append(openBlockers, slimIssue(issue))
}
}
return to.TextResult(map[string]any{
"blocked": len(openBlockers) > 0,
"total_dependencies": len(issues),
"open_blockers": len(openBlockers),
"blockers": slimIssues(issues),
"open": openBlockers,
})
}
func relatedIssueRepo(args map[string]any, ownerKey, repoKey, owner, repo string) (string, string) {
if o := params.GetOptionalString(args, ownerKey, ""); o != "" {
owner = o
}
if r := params.GetOptionalString(args, repoKey, ""); r != "" {
repo = r
}
return owner, repo
} }
func issueRef(args map[string]any) (owner, repo string, index int64, err error) { func issueRef(args map[string]any) (owner, repo string, index int64, err error) {
+63 -3
View File
@@ -31,6 +31,9 @@ func Test_issueBlocking(t *testing.T) {
wantMethod string wantMethod string
wantQuery string wantQuery string
wantIndex float64 wantIndex float64
wantOwner string
wantRepo string
wantBody string
}{ }{
{ {
name: "list blocks paginates", name: "list blocks paginates",
@@ -47,6 +50,22 @@ func Test_issueBlocking(t *testing.T) {
wantPath: blocksPath, wantPath: blocksPath,
wantMethod: http.MethodPost, wantMethod: http.MethodPost,
wantIndex: 7, wantIndex: 7,
wantOwner: owner,
wantRepo: repo,
},
{
name: "block issue posts cross-repo target",
fn: blockIssueFn,
args: map[string]any{
"blocked_issue": float64(7),
"blocked_owner": "other",
"blocked_repo": "elsewhere",
},
wantPath: blocksPath,
wantMethod: http.MethodPost,
wantIndex: 7,
wantOwner: "other",
wantRepo: "elsewhere",
}, },
{ {
name: "unblock issue deletes with index", name: "unblock issue deletes with index",
@@ -55,6 +74,8 @@ func Test_issueBlocking(t *testing.T) {
wantPath: blocksPath, wantPath: blocksPath,
wantMethod: http.MethodDelete, wantMethod: http.MethodDelete,
wantIndex: 7, wantIndex: 7,
wantOwner: owner,
wantRepo: repo,
}, },
{ {
name: "list dependencies paginates", name: "list dependencies paginates",
@@ -71,6 +92,22 @@ func Test_issueBlocking(t *testing.T) {
wantPath: depsPath, wantPath: depsPath,
wantMethod: http.MethodPost, wantMethod: http.MethodPost,
wantIndex: 9, wantIndex: 9,
wantOwner: owner,
wantRepo: repo,
},
{
name: "add dependency posts cross-repo target",
fn: addIssueDependencyFn,
args: map[string]any{
"blocking_issue": float64(9),
"blocking_owner": "other",
"blocking_repo": "elsewhere",
},
wantPath: depsPath,
wantMethod: http.MethodPost,
wantIndex: 9,
wantOwner: "other",
wantRepo: "elsewhere",
}, },
{ {
name: "remove dependency deletes with index", name: "remove dependency deletes with index",
@@ -79,6 +116,17 @@ func Test_issueBlocking(t *testing.T) {
wantPath: depsPath, wantPath: depsPath,
wantMethod: http.MethodDelete, wantMethod: http.MethodDelete,
wantIndex: 9, wantIndex: 9,
wantOwner: owner,
wantRepo: repo,
},
{
name: "check blocked summarizes open dependencies",
fn: checkIssueBlockedFn,
args: map[string]any{"page": float64(1), "per_page": float64(30)},
wantPath: depsPath,
wantMethod: http.MethodGet,
wantQuery: "limit=30&page=1",
wantBody: `"blocked":true`,
}, },
} }
@@ -135,11 +183,23 @@ func Test_issueBlocking(t *testing.T) {
if tt.wantQuery != "" && gotQuery != tt.wantQuery { if tt.wantQuery != "" && gotQuery != tt.wantQuery {
t.Fatalf("query = %s, want %s", gotQuery, tt.wantQuery) t.Fatalf("query = %s, want %s", gotQuery, tt.wantQuery)
} }
if tt.wantIndex != 0 && gotBody["index"] != tt.wantIndex { if tt.wantIndex != 0 {
t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex) if gotBody["index"] != tt.wantIndex {
t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex)
}
if gotBody["owner"] != tt.wantOwner {
t.Fatalf("body owner = %v, want %s", gotBody["owner"], tt.wantOwner)
}
if gotBody["repo"] != tt.wantRepo {
t.Fatalf("body repo = %v, want %s", gotBody["repo"], tt.wantRepo)
}
} }
body := res.Content[0].(*mcp.TextContent).Text body := res.Content[0].(*mcp.TextContent).Text
if !strings.Contains(body, `"number":7`) && !strings.Contains(body, `"number": 7`) { if tt.wantBody != "" {
if !strings.Contains(body, tt.wantBody) {
t.Fatalf("expected %s in output, got: %s", tt.wantBody, body)
}
} else if !strings.Contains(body, `"number":7`) && !strings.Contains(body, `"number": 7`) {
t.Fatalf("expected slim issue in output, got: %s", body) t.Fatalf("expected slim issue in output, got: %s", body)
} }
}) })
+9 -3
View File
@@ -55,9 +55,9 @@ var (
IssueReadTool = tool.NewDefinition( IssueReadTool = tool.NewDefinition(
IssueReadToolName, IssueReadToolName,
"Read issue: details, comments, labels, or blocking relationships.", "Read issue: details, comments, labels, or dependency/blocking links. list_dependencies is issues that block this one (blocked-by); list_blocks is issues this one blocks. check_blocked summarizes whether any of those dependencies are still open. These methods 404 if the repo has issue dependencies disabled.",
annotation.ReadOnly("Read issue details"), 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", "list_blocks", "list_dependencies", "check_blocked")),
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
tool.Number("issue_number", tool.Required()), tool.Number("issue_number", tool.Required()),
@@ -67,7 +67,7 @@ var (
IssueWriteTool = tool.NewDefinition( IssueWriteTool = tool.NewDefinition(
IssueWriteToolName, IssueWriteToolName,
"Write issues: create, update, manage comments, labels, and blocking relationships.", "Write issues: create, update, manage comments, labels, and dependency/blocking links. block_issue makes issue_number block blocked_issue; add_dependency makes issue_number depend on blocking_issue. Cross-repo links use blocked_owner/blocked_repo or blocking_owner/blocking_repo (instance must allow cross-repository dependencies). Repo must have issue dependencies enabled or Gitea returns 404.",
annotation.Write("Create or update issues, 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", "block_issue", "unblock_issue", "add_dependency", "remove_dependency")),
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
@@ -85,7 +85,11 @@ var (
tool.String("deadline", tool.Description("ISO 8601")), tool.String("deadline", tool.Description("ISO 8601")),
tool.Boolean("remove_deadline"), tool.Boolean("remove_deadline"),
tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")), tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")),
tool.String("blocked_owner", tool.Description("owner of blocked_issue; defaults to owner")),
tool.String("blocked_repo", tool.Description("repo of blocked_issue; defaults to repo")),
tool.Number("blocking_issue", tool.Description("issue number that blocks issue_number (required for 'add_dependency'/'remove_dependency')")), tool.Number("blocking_issue", tool.Description("issue number that blocks issue_number (required for 'add_dependency'/'remove_dependency')")),
tool.String("blocking_owner", tool.Description("owner of blocking_issue; defaults to owner")),
tool.String("blocking_repo", tool.Description("repo of blocking_issue; defaults to repo")),
) )
) )
@@ -120,6 +124,8 @@ func issueReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult,
return listIssueBlocksFn(ctx, args) return listIssueBlocksFn(ctx, args)
case "list_dependencies": case "list_dependencies":
return listIssueDependenciesFn(ctx, args) return listIssueDependenciesFn(ctx, args)
case "check_blocked":
return checkIssueBlockedFn(ctx, args)
default: default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) 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"