Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07949c1a63 | ||
|
|
3e093c10b9 | ||
|
|
10f5a345d8 | ||
|
|
554b7490e2 | ||
|
|
9bfd55f97d | ||
|
|
290ad8da5d |
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](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}}) [](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)
|
||||
[](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}}) [](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, labels, or dependency/blocking links |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
+5
-5
@@ -4,14 +4,14 @@
|
||||
|
||||
**Gitea MCP 服务器** 将 [Gitea](https://about.gitea.com) 实例接入 [Model Context Protocol](https://modelcontextprotocol.io) 客户端,让仓库、问题、拉取请求等都能在兼容 MCP 的聊天界面中浏览和管理。
|
||||
|
||||
[](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}}) [](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)
|
||||
[](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}}) [](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"
|
||||
|
||||
+5
-5
@@ -4,14 +4,14 @@
|
||||
|
||||
**Gitea MCP 伺服器** 將 [Gitea](https://about.gitea.com) 實例接入 [Model Context Protocol](https://modelcontextprotocol.io) 客戶端,讓倉庫、問題、拉取請求等都能在相容 MCP 的聊天介面中瀏覽與管理。
|
||||
|
||||
[](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}}) [](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)
|
||||
[](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}}) [](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"
|
||||
|
||||
+68
-21
@@ -3,6 +3,8 @@ package issue
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/params"
|
||||
@@ -12,6 +14,14 @@ import (
|
||||
"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) {
|
||||
owner, repo, index, err := issueRef(args)
|
||||
if err != nil {
|
||||
@@ -49,21 +59,17 @@ func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (*
|
||||
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))
|
||||
blockedOwner, blockedRepo := relatedIssueRepo(args, "blocked_owner", "blocked_repo", owner, repo)
|
||||
method := http.MethodPost
|
||||
if !block {
|
||||
method = http.MethodDelete
|
||||
}
|
||||
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 {
|
||||
var issue gitea_sdk.Issue
|
||||
path := fmt.Sprintf("repos/%s/%s/issues/%d/blocks", url.PathEscape(owner), url.PathEscape(repo), index)
|
||||
if _, err := gitea.DoJSON(ctx, method, path, nil, issueMeta{Index: blocked, Owner: blockedOwner, Repo: blockedRepo}, &issue); err != nil {
|
||||
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) {
|
||||
@@ -103,21 +109,62 @@ func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (*
|
||||
if err != nil {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
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("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) {
|
||||
|
||||
@@ -31,6 +31,9 @@ func Test_issueBlocking(t *testing.T) {
|
||||
wantMethod string
|
||||
wantQuery string
|
||||
wantIndex float64
|
||||
wantOwner string
|
||||
wantRepo string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "list blocks paginates",
|
||||
@@ -47,6 +50,22 @@ func Test_issueBlocking(t *testing.T) {
|
||||
wantPath: blocksPath,
|
||||
wantMethod: http.MethodPost,
|
||||
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",
|
||||
@@ -55,6 +74,8 @@ func Test_issueBlocking(t *testing.T) {
|
||||
wantPath: blocksPath,
|
||||
wantMethod: http.MethodDelete,
|
||||
wantIndex: 7,
|
||||
wantOwner: owner,
|
||||
wantRepo: repo,
|
||||
},
|
||||
{
|
||||
name: "list dependencies paginates",
|
||||
@@ -71,6 +92,22 @@ func Test_issueBlocking(t *testing.T) {
|
||||
wantPath: depsPath,
|
||||
wantMethod: http.MethodPost,
|
||||
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",
|
||||
@@ -79,6 +116,17 @@ func Test_issueBlocking(t *testing.T) {
|
||||
wantPath: depsPath,
|
||||
wantMethod: http.MethodDelete,
|
||||
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 {
|
||||
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)
|
||||
if tt.wantIndex != 0 {
|
||||
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
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -55,9 +55,9 @@ var (
|
||||
|
||||
IssueReadTool = tool.NewDefinition(
|
||||
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"),
|
||||
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("repo", tool.Required(), tool.Description(params.RepoDesc)),
|
||||
tool.Number("issue_number", tool.Required()),
|
||||
@@ -67,7 +67,7 @@ var (
|
||||
|
||||
IssueWriteTool = tool.NewDefinition(
|
||||
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"),
|
||||
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)),
|
||||
@@ -85,7 +85,11 @@ var (
|
||||
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.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.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)
|
||||
case "list_dependencies":
|
||||
return listIssueDependenciesFn(ctx, args)
|
||||
case "check_blocked":
|
||||
return checkIssueBlockedFn(ctx, args)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user