Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da08718e24 | |||
| 44ea8969f4 | |||
| 94aa8dc572 | |||
| 05194ffc1c | |||
| 5c329129f8 | |||
| 52ccf92761 | |||
| 061ea86b0b | |||
| f14b60fe56 | |||
| 94782a85b6 | |||
| e94dd26b30 | |||
| da49bdeb96 | |||
| 3f61299f72 | |||
| 5308fbfb2b | |||
| a7061f9b64 | |||
| f25cc0de8c | |||
| 417ef26da0 | |||
| 34ca5d45db | |||
| 796fd4682d | |||
| 95c036bf3a | |||
| 70b9ac5b80 | |||
| 59e699aac7 | |||
| 26c50d53bd | |||
| 7bfc596a58 | |||
| 966d617670 | |||
| af27b685d4 | |||
| fac6e1d8d1 | |||
| f656c92cda | |||
| af0975d93f | |||
| 001383142f | |||
| b35919989f | |||
| d0225c4c24 | |||
| 6993bb2b5d | |||
| f1b4a208a7 | |||
| d76f02a234 | |||
| b2bde61882 | |||
| 7cfa1fa218 | |||
| 1fecc1df30 | |||
| 8dc9ed9299 | |||
| 1965c9830b | |||
| f377f06478 | |||
| 02fd91da86 | |||
| 55f32ef4f5 | |||
| c9cada1a8d | |||
| a784029828 | |||
| f27c4c622d | |||
| df47a0c9eb | |||
| e3307adbdf | |||
| afada4435e | |||
| 6285bd2467 | |||
| 5bbf8e0afb | |||
| 0535f5bab7 | |||
| d892b05048 | |||
| 592cf51c9b | |||
| 2a9504fc5d | |||
| 2f17f37053 | |||
| 5270d2eb08 | |||
| 97b98c3fc2 | |||
| 58328680a6 | |||
| 28947a030e | |||
| d48e233fba | |||
| 0e225f21da | |||
| b9e575ad64 | |||
| 8395957553 | |||
| 0aa33e0d62 | |||
| 7d2a0985a3 | |||
| 6f86512a7d | |||
| 7845a84c4e | |||
| 4f781f2ddb | |||
| d6ca289f03 | |||
| e0d34fe7d1 | |||
| 09e0275952 | |||
| 721889e586 | |||
| f4bbd5a09a | |||
| 065f65ad2a | |||
| 4917cb1328 | |||
| 24310675ba | |||
| c99f1306e3 | |||
| 10ebaa1964 | |||
| f2b1bfdc7f | |||
| 479991ef9a | |||
| 3def51ffd1 | |||
| 72d9b5a8dd | |||
| 18cb96fc80 | |||
| 4978621b9b | |||
| 24764caf99 | |||
| 5b5a9b48e2 | |||
| bd43811a07 | |||
| c4f619a907 | |||
| 482b3167b3 | |||
| 6b7fd53c4d | |||
| c34145ac3d | |||
| 58f4e190cf | |||
| ea394e4693 | |||
| a5504ca7e0 | |||
| 50b24dfb61 | |||
| 51f2cf57d3 | |||
| 59693ffc5d | |||
| 82b02fa9f1 | |||
| c900e6b4c3 | |||
| c52f8649a2 | |||
| 051ce0bab4 | |||
| 2dd4aa94ad |
@@ -0,0 +1,52 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = ["-t", "http"]
|
||||||
|
bin = "./gitea-mcp"
|
||||||
|
cmd = "make build"
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Gitea MCP DevContainer",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm",
|
||||||
|
"features": {},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {},
|
||||||
|
"extensions": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"golang.go",
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
"DavidAnson.vscode-markdownlint",
|
||||||
|
"github.copilot",
|
||||||
|
"eamodio.gitlens"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github/
|
||||||
|
.gitea/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Go specific
|
||||||
|
vendor/
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
*_test.go
|
||||||
|
**/test/
|
||||||
|
**/tests/
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# Development tools
|
||||||
|
.air.toml
|
||||||
|
.golangci.yml
|
||||||
|
.goreleaser.yml
|
||||||
|
|
||||||
|
# Debug files
|
||||||
|
debug
|
||||||
|
__debug_bin
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
GITEA_URL=https://gitea.example.com
|
|
||||||
GITEA_TOKEN=your_access_token
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: release-nightly
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: gitea
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_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
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
# 'latest', 'nightly', or a semver
|
||||||
|
version: "~> v2"
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GORELEASER_FORCE_TOKEN: "gitea"
|
||||||
|
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: gitea
|
||||||
|
DOCKER_LATEST: latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_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
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
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 }}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: check-and-test
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
- name: build
|
||||||
|
run: |
|
||||||
|
make build
|
||||||
|
|
||||||
|
govulncheck_job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Run govulncheck
|
||||||
|
steps:
|
||||||
|
- id: govulncheck
|
||||||
|
uses: golang/govulncheck-action@v1
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
go-package: ./...
|
||||||
+5
-26
@@ -1,26 +1,5 @@
|
|||||||
.env
|
.idea
|
||||||
__pycache__/
|
gitea-mcp
|
||||||
*.py[cod]
|
gitea-mcp.exe
|
||||||
*$py.class
|
*.log
|
||||||
*.so
|
tmp
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
.pytest_cache/
|
|
||||||
.mypy_cache/
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
main: .
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.Version={{.Version}}
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- formats: tar.gz
|
||||||
|
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- title .Os }}_
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else if eq .Arch "386" }}i386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
|
# use zip for windows archives
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats: zip
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
groups:
|
||||||
|
- title: Features
|
||||||
|
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||||
|
order: 0
|
||||||
|
- title: "Bug fixes"
|
||||||
|
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||||
|
order: 1
|
||||||
|
- title: "Enhancements"
|
||||||
|
regexp: "^.*chore[(\\w)]*:+.*$"
|
||||||
|
order: 2
|
||||||
|
- title: "Refactor"
|
||||||
|
regexp: "^.*refactor[(\\w)]*:+.*$"
|
||||||
|
order: 3
|
||||||
|
- title: "Build process updates"
|
||||||
|
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
|
||||||
|
order: 4
|
||||||
|
- title: "Documentation updates"
|
||||||
|
regexp: ^.*?docs?(\(.+\))??!?:.+$
|
||||||
|
order: 4
|
||||||
|
- title: Others
|
||||||
|
order: 999
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
|
||||||
|
release:
|
||||||
|
footer: >-
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: https://gitea.com/api/v1
|
||||||
|
download: https://gitea.com
|
||||||
|
force_token: gitea
|
||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
// 💡 Inputs are prompted on first server start, then stored securely by VS Code.
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea-host",
|
||||||
|
"description": "Gitea Host",
|
||||||
|
"password": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea-token",
|
||||||
|
"description": "Gitea Access Token",
|
||||||
|
"password": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea-insecure",
|
||||||
|
"description": "Allow insecure connections (e.g., self-signed certificates)",
|
||||||
|
"default": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": {
|
||||||
|
"gitea-mcp-stdio": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": ["-t", "stdio"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_HOST": "${input:gitea-host}",
|
||||||
|
"GITEA_ACCESS_TOKEN": "${input:gitea-token}",
|
||||||
|
"GITEA_INSECURE": "${input:gitea-insecure}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitea-mcp-http": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "http://localhost:8080/mcp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-16
@@ -1,25 +1,32 @@
|
|||||||
FROM python:3.12-slim
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
|
# Build stage
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install uv for fast dependency management
|
COPY go.mod go.sum ./
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
go mod download
|
||||||
|
|
||||||
# Copy project files
|
COPY . .
|
||||||
COPY pyproject.toml .
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
COPY server.py .
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
COPY README.md .
|
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \
|
||||||
|
go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o gitea-mcp
|
||||||
|
|
||||||
# Install dependencies
|
# Final stage
|
||||||
RUN uv pip install --system --no-cache .
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
# Install curl for health checks
|
WORKDIR /app
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
|
COPY --from=builder --chown=nonroot:nonroot /app/gitea-mcp .
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
EXPOSE 8000
|
USER nonroot:nonroot
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
LABEL org.opencontainers.image.version="${VERSION}"
|
||||||
CMD curl -f http://localhost:8000/health || exit 1
|
|
||||||
|
|
||||||
CMD ["python", "server.py"]
|
CMD ["/app/gitea-mcp"]
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 The Gitea Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
GO ?= go
|
||||||
|
EXECUTABLE := gitea-mcp
|
||||||
|
VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
|
||||||
|
LDFLAGS := -X "main.Version=$(VERSION)"
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Print this help message.
|
||||||
|
@echo "Usage: make [target]"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo ""
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: build ## Install the application.
|
||||||
|
@echo "Installing $(EXECUTABLE)..."
|
||||||
|
@mkdir -p $(GOPATH)/bin
|
||||||
|
@cp $(EXECUTABLE) $(GOPATH)/bin/$(EXECUTABLE)
|
||||||
|
@echo "Installed $(EXECUTABLE) to $(GOPATH)/bin/$(EXECUTABLE)"
|
||||||
|
@echo "Please add $(GOPATH)/bin to your PATH if it is not already there."
|
||||||
|
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall: ## Uninstall the application.
|
||||||
|
@echo "Uninstalling $(EXECUTABLE)..."
|
||||||
|
@rm -f $(GOPATH)/bin/$(EXECUTABLE)
|
||||||
|
@echo "Uninstalled $(EXECUTABLE) from $(GOPATH)/bin/$(EXECUTABLE)"
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Clean the build artifacts.
|
||||||
|
@echo "Cleaning up build artifacts..."
|
||||||
|
@rm -f $(EXECUTABLE)
|
||||||
|
@echo "Cleaned up $(EXECUTABLE)"
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## Build the application.
|
||||||
|
$(GO) build -v -ldflags '-s -w $(LDFLAGS)' -o $(EXECUTABLE)
|
||||||
|
|
||||||
|
.PHONY: air
|
||||||
|
air: ## Install air for hot reload.
|
||||||
|
@hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
$(GO) install github.com/air-verse/air@latest; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: dev
|
||||||
|
dev: air ## run the application with hot reload
|
||||||
|
air --build.cmd "make build" --build.bin ./gitea-mcp
|
||||||
|
|
||||||
|
.PHONY: vendor
|
||||||
|
vendor: ## tidy and verify module dependencies
|
||||||
|
@echo 'Tidying and verifying module dependencies...'
|
||||||
|
go mod tidy
|
||||||
|
go mod verify
|
||||||
@@ -1,92 +1,233 @@
|
|||||||
# Gitea MCP Server
|
# Gitea MCP Server
|
||||||
|
|
||||||
A lightweight Model Context Protocol (MCP) server for Gitea, following the **Hybrid MCP Light** pattern.
|
[繁體中文](README.zh-tw.md) | [简体中文](README.zh-cn.md)
|
||||||
|
|
||||||
## Features
|
**Gitea MCP Server** is an integration plugin designed to connect Gitea with Model Context Protocol (MCP) systems. This allows for seamless command execution and repository management through an MCP-compatible chat interface.
|
||||||
|
|
||||||
- **5 Curated Tools** for common operations
|
[](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)
|
||||||
- **API Pass-through** for complete API coverage
|
|
||||||
- **Embedded API Reference** for agent self-service
|
|
||||||
- **Health Check Endpoint** for Docker/Kubernetes
|
|
||||||
|
|
||||||
## Tools
|
## Table of Contents
|
||||||
|
|
||||||
| Tool | Description |
|
- [Gitea MCP Server](#gitea-mcp-server)
|
||||||
|:-----|:------------|
|
- [Table of Contents](#table-of-contents)
|
||||||
| `get_my_user_info` | Get authenticated user info |
|
- [What is Gitea?](#what-is-gitea)
|
||||||
| `search_repos` | Search repositories by keyword |
|
- [What is MCP?](#what-is-mcp)
|
||||||
| `list_my_repos` | List user's accessible repositories |
|
- [🚧 Installation](#-installation)
|
||||||
| `get_repo` | Get repository details |
|
- [Usage with VS Code](#usage-with-vs-code)
|
||||||
| `list_repo_issues` | List issues for a repository |
|
- [📥 Download the official binary release](#-download-the-official-binary-release)
|
||||||
| `list_repo_commits` | List commits for a repository |
|
- [🔧 Build from Source](#-build-from-source)
|
||||||
| `gitea_api_call` | Raw API pass-through for any endpoint |
|
- [📁 Add to PATH](#-add-to-path)
|
||||||
|
- [🚀 Usage](#-usage)
|
||||||
|
- [✅ Available Tools](#-available-tools)
|
||||||
|
- [🐛 Debugging](#-debugging)
|
||||||
|
- [🛠 Troubleshooting](#-troubleshooting)
|
||||||
|
|
||||||
## Resources
|
## What is Gitea?
|
||||||
|
|
||||||
| Resource URI | Description |
|
Gitea is a community-managed lightweight code hosting solution written in Go. It is published under the MIT license. Gitea provides Git hosting including a repository viewer, issue tracking, pull requests, and more.
|
||||||
|:-------------|:------------|
|
|
||||||
| `gitea://api-reference` | Quick reference for common API endpoints |
|
|
||||||
|
|
||||||
## Setup
|
## What is MCP?
|
||||||
|
|
||||||
### Environment Variables
|
Model Context Protocol (MCP) is a protocol that allows for the integration of various tools and systems through a chat interface. It enables seamless command execution and management of repositories, users, and other resources.
|
||||||
|
|
||||||
```bash
|
## 🚧 Installation
|
||||||
GITEA_URL=https://gitea.example.com
|
|
||||||
GITEA_TOKEN=your_access_token
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local Development
|
### Usage with VS Code
|
||||||
|
|
||||||
```bash
|
For quick installation, use one of the one-click install buttons at the top of this README.
|
||||||
# Install dependencies
|
|
||||||
uv pip install -e .
|
|
||||||
|
|
||||||
# Run server
|
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||||
python server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||||
|
|
||||||
```bash
|
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||||
# Build and run
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# Check health
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
## MCP Client Configuration
|
|
||||||
|
|
||||||
### Gemini CLI (`~/.gemini/settings.json`)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcp": {
|
||||||
"gitea": {
|
"inputs": [
|
||||||
"url": "http://localhost:8000/mcp"
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea_token",
|
||||||
|
"description": "Gitea Personal Access Token",
|
||||||
|
"password": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": {
|
||||||
|
"gitea-mcp": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"GITEA_ACCESS_TOKEN",
|
||||||
|
"docker.gitea.com/gitea-mcp-server"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Pass-through Examples
|
### 📥 Download the official binary release
|
||||||
|
|
||||||
```python
|
You can download the official release from [official Gitea MCP binary releases](https://gitea.com/gitea/gitea-mcp/releases).
|
||||||
# Create a release
|
|
||||||
gitea_api_call(
|
|
||||||
endpoint="/repos/myorg/myrepo/releases",
|
|
||||||
method="POST",
|
|
||||||
body='{"tag_name": "v1.0.0", "name": "Release 1.0"}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get file contents
|
### 🔧 Build from Source
|
||||||
gitea_api_call(
|
|
||||||
endpoint="/repos/myorg/myrepo/contents/README.md",
|
You can download the source code by cloning the repository using Git:
|
||||||
params='{"ref": "main"}'
|
|
||||||
)
|
```bash
|
||||||
|
git clone https://gitea.com/gitea/gitea-mcp.git
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
Before building, make sure you have the following installed:
|
||||||
|
|
||||||
MIT
|
- make
|
||||||
|
- Golang (Go 1.24 or later recommended)
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📁 Add to PATH
|
||||||
|
|
||||||
|
After installing, copy the binary gitea-mcp to a directory included in your system's PATH. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp gitea-mcp /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
This example is for Cursor, you can also use plugins in VSCode.
|
||||||
|
To configure the MCP server for Gitea, add the following to your MCP configuration file:
|
||||||
|
|
||||||
|
- **stdio mode**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": [
|
||||||
|
"-t",
|
||||||
|
"stdio",
|
||||||
|
"--host",
|
||||||
|
"https://gitea.com"
|
||||||
|
// "--token", "<your personal access token>"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
// "GITEA_HOST": "https://gitea.com",
|
||||||
|
// "GITEA_INSECURE": "true",
|
||||||
|
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **sse mode**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/sse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **http mode**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default log path**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> You can provide your Gitea host and access token either as command-line arguments or environment variables.
|
||||||
|
> Command-line arguments have the highest priority
|
||||||
|
|
||||||
|
Once everything is set up, try typing the following in your MCP-compatible chatbox:
|
||||||
|
|
||||||
|
```text
|
||||||
|
list all my repositories
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Available Tools
|
||||||
|
|
||||||
|
The Gitea MCP Server supports the following tools:
|
||||||
|
|
||||||
|
| Tool | Scope | Description |
|
||||||
|
| :--------------------------: | :----------: | :------------------------------------------------------: |
|
||||||
|
| get_my_user_info | User | Get the information of the authenticated user |
|
||||||
|
| get_user_orgs | User | Get organizations associated with the authenticated user |
|
||||||
|
| create_repo | Repository | Create a new repository |
|
||||||
|
| fork_repo | Repository | Fork a repository |
|
||||||
|
| list_my_repos | Repository | List all repositories owned by the authenticated user |
|
||||||
|
| create_branch | Branch | Create a new branch |
|
||||||
|
| delete_branch | Branch | Delete a branch |
|
||||||
|
| list_branches | Branch | List all branches in a repository |
|
||||||
|
| create_release | Release | Create a new release in a repository |
|
||||||
|
| delete_release | Release | Delete a release from a repository |
|
||||||
|
| get_release | Release | Get a release |
|
||||||
|
| get_latest_release | Release | Get the latest release in a repository |
|
||||||
|
| list_releases | Release | List all releases in a repository |
|
||||||
|
| create_tag | Tag | Create a new tag |
|
||||||
|
| delete_tag | Tag | Delete a tag |
|
||||||
|
| get_tag | Tag | Get a tag |
|
||||||
|
| list_tags | Tag | List all tags in a repository |
|
||||||
|
| list_repo_commits | Commit | List all commits in a repository |
|
||||||
|
| get_file_content | File | Get the content and metadata of a file |
|
||||||
|
| get_dir_content | File | Get a list of entries in a directory |
|
||||||
|
| create_file | File | Create a new file |
|
||||||
|
| update_file | File | Update an existing file |
|
||||||
|
| delete_file | File | Delete a file |
|
||||||
|
| get_issue_by_index | Issue | Get an issue by its index |
|
||||||
|
| list_repo_issues | Issue | List all issues in a repository |
|
||||||
|
| create_issue | Issue | Create a new issue |
|
||||||
|
| create_issue_comment | Issue | Create a comment on an issue |
|
||||||
|
| edit_issue | Issue | Edit a issue |
|
||||||
|
| edit_issue_comment | Issue | Edit a comment on an issue |
|
||||||
|
| get_issue_comments_by_index | Issue | Get comments of an issue by its index |
|
||||||
|
| get_pull_request_by_index | Pull Request | Get a pull request by its index |
|
||||||
|
| list_repo_pull_requests | Pull Request | List all pull requests in a repository |
|
||||||
|
| create_pull_request | Pull Request | Create a new pull request |
|
||||||
|
| search_users | User | Search for users |
|
||||||
|
| search_org_teams | Organization | Search for teams in an organization |
|
||||||
|
| search_repos | Repository | Search for repositories |
|
||||||
|
| get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server |
|
||||||
|
|
||||||
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
To enable debug mode, add the `-d` flag when running the Gitea MCP Server with sse mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 Troubleshooting
|
||||||
|
|
||||||
|
If you encounter any issues, here are some common troubleshooting steps:
|
||||||
|
|
||||||
|
1. **Check your PATH**: Ensure that the `gitea-mcp` binary is in a directory included in your system's PATH.
|
||||||
|
2. **Verify dependencies**: Make sure you have all the required dependencies installed, such as `make` and `Golang`.
|
||||||
|
3. **Review configuration**: Double-check your MCP configuration file for any errors or missing information.
|
||||||
|
4. **Consult logs**: Check the logs for any error messages or warnings that can provide more information about the issue.
|
||||||
|
|
||||||
|
Enjoy exploring and managing your Gitea repositories via chat!
|
||||||
|
|||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
# Gitea MCP 服务器
|
||||||
|
|
||||||
|
[English](README.md) | [繁體中文](README.zh-tw.md)
|
||||||
|
|
||||||
|
**Gitea MCP 服务器** 是一个集成插件,旨在将 Gitea 与 Model Context Protocol (MCP) 系统连接起来。这允许通过 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)
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [Gitea MCP 服务器](#gitea-mcp-服务器)
|
||||||
|
- [目录](#目录)
|
||||||
|
- [什么是 Gitea?](#什么是-gitea)
|
||||||
|
- [什么是 MCP?](#什么是-mcp)
|
||||||
|
- [🚧 安装](#-安装)
|
||||||
|
- [在 VS Code 中使用](#在-vs-code-中使用)
|
||||||
|
- [📥 下载官方 Gitea MCP 二进制版本](#-下载官方-gitea-mcp-二进制版本)
|
||||||
|
- [🔧 从源代码构建](#-从源代码构建)
|
||||||
|
- [📁 添加到 PATH](#-添加到-path)
|
||||||
|
- [🚀 使用](#-使用)
|
||||||
|
- [✅ 可用工具](#-可用工具)
|
||||||
|
- [🐛 调试](#-调试)
|
||||||
|
- [🛠 疑难排解](#-疑难排解)
|
||||||
|
|
||||||
|
## 什么是 Gitea?
|
||||||
|
|
||||||
|
Gitea 是一个由社区管理的轻量级代码托管解决方案,使用 Go 语言编写。它以 MIT 许可证发布。Gitea 提供 Git 托管,包括仓库查看器、问题追踪、拉取请求等功能。
|
||||||
|
|
||||||
|
## 什么是 MCP?
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各种工具和系统。它能够无缝执行命令和管理仓库、用户和其他资源。
|
||||||
|
|
||||||
|
## 🚧 安装
|
||||||
|
|
||||||
|
### 在 VS Code 中使用
|
||||||
|
|
||||||
|
要快速安装,请使用本 README 顶部的单击安装按钮之一。
|
||||||
|
|
||||||
|
要手动安装,请将以下 JSON 块添加到 VS Code 的用户设置 (JSON) 文件中。您可以通过按 `Ctrl + Shift + P` 并输入 `Preferences: Open User Settings (JSON)` 来完成此操作。
|
||||||
|
|
||||||
|
或者,您可以将其添加到工作区中的 `.vscode/mcp.json` 文件中。这将允许您与他人共享配置。
|
||||||
|
|
||||||
|
> 请注意,`.vscode/mcp.json` 文件中不需要 `mcp` 键。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea_token",
|
||||||
|
"description": "Gitea 个人访问令牌",
|
||||||
|
"password": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": {
|
||||||
|
"gitea-mcp": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"GITEA_ACCESS_TOKEN",
|
||||||
|
"docker.gitea.com/gitea-mcp-server"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📥 下载官方 Gitea MCP 二进制版本
|
||||||
|
|
||||||
|
您可以从[官方 Gitea MCP 二进制版本](https://gitea.com/gitea/gitea-mcp/releases)下载官方版本。
|
||||||
|
|
||||||
|
### 🔧 从源代码构建
|
||||||
|
|
||||||
|
您可以使用 Git 克隆仓库来下载源代码:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.com/gitea/gitea-mcp.git
|
||||||
|
```
|
||||||
|
|
||||||
|
在构建之前,请确保您已安装以下内容:
|
||||||
|
|
||||||
|
- make
|
||||||
|
- Golang (建议使用 Go 1.24 或更高版本)
|
||||||
|
|
||||||
|
然后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📁 添加到 PATH
|
||||||
|
|
||||||
|
构建后,将二进制文件 gitea-mcp 复制到系统 PATH 中包含的目录。例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp gitea-mcp /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 使用
|
||||||
|
|
||||||
|
此示例适用于 Cursor,您也可以在 VSCode 中使用插件。
|
||||||
|
要配置 Gitea 的 MCP 服务器,请将以下内容添加到您的 MCP 配置文件中:
|
||||||
|
|
||||||
|
- **stdio 模式**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": [
|
||||||
|
"-t",
|
||||||
|
"stdio",
|
||||||
|
"--host",
|
||||||
|
"https://gitea.com"
|
||||||
|
// "--token", "<your personal access token>"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
// "GITEA_HOST": "https://gitea.com",
|
||||||
|
// "GITEA_INSECURE": "true",
|
||||||
|
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **sse 模式**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/sse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **http 模式**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**默认日志路径**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||||
|
|
||||||
|
> [!注意]
|
||||||
|
> 您可以通过命令行参数或环境变量提供您的 Gitea 主机和访问令牌。
|
||||||
|
> 命令行参数具有最高优先级
|
||||||
|
|
||||||
|
一切设置完成后,请尝试在您的 MCP 兼容聊天框中输入以下内容:
|
||||||
|
|
||||||
|
```text
|
||||||
|
列出我所有的仓库
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 可用工具
|
||||||
|
|
||||||
|
Gitea MCP 服务器支持以下工具:
|
||||||
|
|
||||||
|
| 工具 | 范围 | 描述 |
|
||||||
|
| :--------------------------: | :------: | :--------------------------: |
|
||||||
|
| get_my_user_info | 用户 | 获取已认证用户的信息 |
|
||||||
|
| get_user_orgs | 用户 | 获取已认证用户关联的组织 |
|
||||||
|
| create_repo | 仓库 | 创建一个新仓库 |
|
||||||
|
| fork_repo | 仓库 | 复刻一个仓库 |
|
||||||
|
| list_my_repos | 仓库 | 列出已认证用户拥有的所有仓库 |
|
||||||
|
| create_branch | 分支 | 创建一个新分支 |
|
||||||
|
| delete_branch | 分支 | 删除一个分支 |
|
||||||
|
| list_branches | 分支 | 列出仓库中的所有分支 |
|
||||||
|
| create_release | 版本发布 | 创建一个新版本发布 |
|
||||||
|
| delete_release | 版本发布 | 删除一个版本发布 |
|
||||||
|
| get_release | 版本发布 | 获取一个版本发布 |
|
||||||
|
| get_latest_release | 版本发布 | 获取最新的版本发布 |
|
||||||
|
| list_releases | 版本发布 | 列出所有版本发布 |
|
||||||
|
| create_tag | 标签 | 创建一个新标签 |
|
||||||
|
| delete_tag | 标签 | 删除一个标签 |
|
||||||
|
| get_tag | 标签 | 获取一个标签 |
|
||||||
|
| list_tags | 标签 | 列出所有标签 |
|
||||||
|
| list_repo_commits | 提交 | 列出仓库中的所有提交 |
|
||||||
|
| get_file_content | 文件 | 获取文件的内容和元数据 |
|
||||||
|
| get_dir_content | 文件 | 获取目录的内容列表 |
|
||||||
|
| create_file | 文件 | 创建一个新文件 |
|
||||||
|
| update_file | 文件 | 更新现有文件 |
|
||||||
|
| delete_file | 文件 | 删除一个文件 |
|
||||||
|
| get_issue_by_index | 问题 | 根据索引获取问题 |
|
||||||
|
| list_repo_issues | 问题 | 列出仓库中的所有问题 |
|
||||||
|
| create_issue | 问题 | 创建一个新问题 |
|
||||||
|
| create_issue_comment | 问题 | 在问题上创建评论 |
|
||||||
|
| edit_issue | 问题 | 编辑一个问题 |
|
||||||
|
| edit_issue_comment | 问题 | 在问题上编辑评论 |
|
||||||
|
| get_issue_comments_by_index | 问题 | 根据索引获取问题的评论 |
|
||||||
|
| get_pull_request_by_index | 拉取请求 | 根据索引获取拉取请求 |
|
||||||
|
| list_repo_pull_requests | 拉取请求 | 列出仓库中的所有拉取请求 |
|
||||||
|
| create_pull_request | 拉取请求 | 创建一个新拉取请求 |
|
||||||
|
| search_users | 用户 | 搜索用户 |
|
||||||
|
| search_org_teams | 组织 | 搜索组织中的团队 |
|
||||||
|
| search_repos | 仓库 | 搜索仓库 |
|
||||||
|
| get_gitea_mcp_server_version | 服务器 | 获取 Gitea MCP 服务器的版本 |
|
||||||
|
|
||||||
|
## 🐛 调试
|
||||||
|
|
||||||
|
要启用调试模式,请在使用 sse 模式运行 Gitea MCP 服务器时添加 `-d` 标志:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 疑难排解
|
||||||
|
|
||||||
|
如果您遇到任何问题,以下是一些常见的疑难排解步骤:
|
||||||
|
|
||||||
|
1. **检查您的 PATH**: 确保 `gitea-mcp` 二进制文件位于系统 PATH 中包含的目录中。
|
||||||
|
2. **验证依赖项**: 确保您已安装所有所需的依赖项,例如 `make` 和 `Golang`。
|
||||||
|
3. **检查配置**: 仔细检查您的 MCP 配置文件是否有任何错误或遗漏的信息。
|
||||||
|
4. **查看日志**: 检查日志中是否有任何错误消息或警告,可以提供有关问题的更多信息。
|
||||||
|
|
||||||
|
享受通过聊天探索和管理您的 Gitea 仓库的乐趣!
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
# Gitea MCP 伺服器
|
||||||
|
|
||||||
|
[English](README.md) | [简体中文](README.zh-cn.md)
|
||||||
|
|
||||||
|
**Gitea MCP 伺服器** 是一個整合插件,旨在將 Gitea 與 Model Context Protocol (MCP) 系統連接起來。這允許通過 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)
|
||||||
|
|
||||||
|
## 目錄
|
||||||
|
|
||||||
|
- [Gitea MCP 伺服器](#gitea-mcp-伺服器)
|
||||||
|
- [目錄](#目錄)
|
||||||
|
- [什麼是 Gitea?](#什麼是-gitea)
|
||||||
|
- [什麼是 MCP?](#什麼是-mcp)
|
||||||
|
- [🚧 安裝](#-安裝)
|
||||||
|
- [在 VS Code 中使用](#在-vs-code-中使用)
|
||||||
|
- [📥 下載官方 Gitea MCP 二進位版本](#-下載官方-gitea-mcp-二進位版本)
|
||||||
|
- [🔧 從源代碼構建](#-從源代碼構建)
|
||||||
|
- [📁 添加到 PATH](#-添加到-path)
|
||||||
|
- [🚀 使用](#-使用)
|
||||||
|
- [✅ 可用工具](#-可用工具)
|
||||||
|
- [🐛 調試](#-調試)
|
||||||
|
- [🛠 疑難排解](#-疑難排解)
|
||||||
|
|
||||||
|
## 什麼是 Gitea?
|
||||||
|
|
||||||
|
Gitea 是一個由社群管理的輕量級代碼託管解決方案,使用 Go 語言編寫。它以 MIT 許可證發布。Gitea 提供 Git 託管,包括倉庫查看器、問題追蹤、拉取請求等功能。
|
||||||
|
|
||||||
|
## 什麼是 MCP?
|
||||||
|
|
||||||
|
Model Context Protocol (MCP) 是一種協議,允許通過聊天界面整合各種工具和系統。它能夠無縫執行命令和管理倉庫、用戶和其他資源。
|
||||||
|
|
||||||
|
## 🚧 安裝
|
||||||
|
|
||||||
|
### 在 VS Code 中使用
|
||||||
|
|
||||||
|
要快速安裝,請使用本 README 頂部的單擊安裝按鈕之一。
|
||||||
|
|
||||||
|
要手動安裝,請將以下 JSON 塊添加到 VS Code 的用戶設置 (JSON) 文件中。您可以通過按 `Ctrl + Shift + P` 並輸入 `Preferences: Open User Settings (JSON)` 來完成此操作。
|
||||||
|
|
||||||
|
或者,您可以將其添加到工作區中的 `.vscode/mcp.json` 文件中。這將允許您與他人共享配置。
|
||||||
|
|
||||||
|
> 請注意,`.vscode/mcp.json` 文件中不需要 `mcp` 鍵。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"type": "promptString",
|
||||||
|
"id": "gitea_token",
|
||||||
|
"description": "Gitea 個人訪問令牌",
|
||||||
|
"password": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": {
|
||||||
|
"gitea-mcp": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"GITEA_ACCESS_TOKEN",
|
||||||
|
"docker.gitea.com/gitea-mcp-server"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"GITEA_ACCESS_TOKEN": "${input:gitea_token}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📥 下載官方 Gitea MCP 二進位版本
|
||||||
|
|
||||||
|
您可以從[官方 Gitea MCP 二進位版本](https://gitea.com/gitea/gitea-mcp/releases)下載官方版本。
|
||||||
|
|
||||||
|
### 🔧 從源代碼構建
|
||||||
|
|
||||||
|
您可以使用 Git 克隆倉庫來下載源代碼:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.com/gitea/gitea-mcp.git
|
||||||
|
```
|
||||||
|
|
||||||
|
在構建之前,請確保您已安裝以下內容:
|
||||||
|
|
||||||
|
- make
|
||||||
|
- Golang (建議使用 Go 1.24 或更高版本)
|
||||||
|
|
||||||
|
然後運行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📁 添加到 PATH
|
||||||
|
|
||||||
|
安裝後,將二進制文件 gitea-mcp 複製到系統 PATH 中包含的目錄。例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp gitea-mcp /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 使用
|
||||||
|
|
||||||
|
此示例適用於 Cursor,您也可以在 VSCode 中使用插件。
|
||||||
|
要配置 Gitea 的 MCP 伺服器,請將以下內容添加到您的 MCP 配置文件中:
|
||||||
|
|
||||||
|
- **stdio 模式**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": [
|
||||||
|
"-t",
|
||||||
|
"stdio",
|
||||||
|
"--host",
|
||||||
|
"https://gitea.com"
|
||||||
|
// "--token", "<your personal access token>"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
// "GITEA_HOST": "https://gitea.com",
|
||||||
|
// "GITEA_INSECURE": "true",
|
||||||
|
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **sse 模式**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/sse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **http 模式**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"url": "http://localhost:8080/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**預設日誌路徑**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||||
|
|
||||||
|
> [!注意]
|
||||||
|
> 您可以通過命令列參數或環境變數提供您的 Gitea 主機和訪問令牌。
|
||||||
|
> 命令列參數具有最高優先權
|
||||||
|
|
||||||
|
一切設置完成後,請嘗試在您的 MCP 兼容聊天框中輸入以下內容:
|
||||||
|
|
||||||
|
```text
|
||||||
|
列出我所有的倉庫
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 可用工具
|
||||||
|
|
||||||
|
Gitea MCP 伺服器支持以下工具:
|
||||||
|
|
||||||
|
| 工具 | 範圍 | 描述 |
|
||||||
|
| :--------------------------: | :------: | :--------------------------: |
|
||||||
|
| get_my_user_info | 用戶 | 獲取已認證用戶的信息 |
|
||||||
|
| get_user_orgs | 用戶 | 取得已認證用戶所屬組織 |
|
||||||
|
| create_repo | 倉庫 | 創建一個新倉庫 |
|
||||||
|
| fork_repo | 倉庫 | 復刻一個倉庫 |
|
||||||
|
| list_my_repos | 倉庫 | 列出已認證用戶擁有的所有倉庫 |
|
||||||
|
| create_branch | 分支 | 創建一個新分支 |
|
||||||
|
| delete_branch | 分支 | 刪除一個分支 |
|
||||||
|
| list_branches | 分支 | 列出倉庫中的所有分支 |
|
||||||
|
| create_release | 版本發布 | 創建一個新版本發布 |
|
||||||
|
| delete_release | 版本發布 | 刪除一個版本發布 |
|
||||||
|
| get_release | 版本發布 | 獲取一個版本發布 |
|
||||||
|
| get_latest_release | 版本發布 | 獲取最新的版本發布 |
|
||||||
|
| list_releases | 版本發布 | 列出所有版本發布 |
|
||||||
|
| create_tag | 標籤 | 創建一個新標籤 |
|
||||||
|
| delete_tag | 標籤 | 刪除一個標籤 |
|
||||||
|
| get_tag | 標籤 | 獲取一個標籤 |
|
||||||
|
| list_tags | 標籤 | 列出所有標籤 |
|
||||||
|
| list_repo_commits | 提交 | 列出倉庫中的所有提交 |
|
||||||
|
| get_file_content | 文件 | 獲取文件的內容和元數據 |
|
||||||
|
| get_dir_content | 文件 | 獲取目錄的內容列表 |
|
||||||
|
| create_file | 文件 | 創建一個新文件 |
|
||||||
|
| update_file | 文件 | 更新現有文件 |
|
||||||
|
| delete_file | 文件 | 刪除一個文件 |
|
||||||
|
| get_issue_by_index | 問題 | 根據索引獲取問題 |
|
||||||
|
| list_repo_issues | 問題 | 列出倉庫中的所有問題 |
|
||||||
|
| create_issue | 問題 | 創建一個新問題 |
|
||||||
|
| create_issue_comment | 問題 | 在問題上創建評論 |
|
||||||
|
| edit_issue | 問題 | 編輯一個問題 |
|
||||||
|
| edit_issue_comment | 問題 | 在問題上編輯評論 |
|
||||||
|
| get_issue_comments_by_index | 问题 | 根據索引獲取問題的評論 |
|
||||||
|
| get_pull_request_by_index | 拉取請求 | 根據索引獲取拉取請求 |
|
||||||
|
| list_repo_pull_requests | 拉取請求 | 列出倉庫中的所有拉取請求 |
|
||||||
|
| create_pull_request | 拉取請求 | 創建一個新拉取請求 |
|
||||||
|
| search_users | 用戶 | 搜索用戶 |
|
||||||
|
| search_org_teams | 組織 | 搜索組織中的團隊 |
|
||||||
|
| search_repos | 倉庫 | 搜索倉庫 |
|
||||||
|
| get_gitea_mcp_server_version | 伺服器 | 獲取 Gitea MCP 伺服器的版本 |
|
||||||
|
|
||||||
|
## 🐛 調試
|
||||||
|
|
||||||
|
要啟用調試模式,請在使用 sse 模式運行 Gitea MCP 伺服器時添加 `-d` 旗標:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gitea-mcp -t sse [--port 8080] --token <your personal access token> -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 疑難排解
|
||||||
|
|
||||||
|
如果您遇到任何問題,以下是一些常見的疑難排解步驟:
|
||||||
|
|
||||||
|
1. **檢查您的 PATH**: 確保 `gitea-mcp` 二進制文件位於系統 PATH 中包含的目錄中。
|
||||||
|
2. **驗證依賴項**: 確保您已安裝所有所需的依賴項,例如 `make` 和 `Golang`。
|
||||||
|
3. **檢查配置**: 仔細檢查您的 MCP 配置文件是否有任何錯誤或遺漏的信息。
|
||||||
|
4. **查看日誌**: 檢查日誌中是否有任何錯誤消息或警告,可以提供有關問題的更多信息。
|
||||||
|
|
||||||
|
享受通過聊天探索和管理您的 Gitea 倉庫的樂趣!
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation"
|
||||||
|
flagPkg "gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
token string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(
|
||||||
|
&flagPkg.Mode,
|
||||||
|
"t",
|
||||||
|
"stdio",
|
||||||
|
"Transport type (stdio, sse or http)",
|
||||||
|
)
|
||||||
|
flag.StringVar(
|
||||||
|
&flagPkg.Mode,
|
||||||
|
"transport",
|
||||||
|
"stdio",
|
||||||
|
"Transport type (stdio, sse or http)",
|
||||||
|
)
|
||||||
|
flag.StringVar(
|
||||||
|
&host,
|
||||||
|
"host",
|
||||||
|
os.Getenv("GITEA_HOST"),
|
||||||
|
"Gitea host",
|
||||||
|
)
|
||||||
|
flag.IntVar(
|
||||||
|
&port,
|
||||||
|
"port",
|
||||||
|
8080,
|
||||||
|
"see or http port",
|
||||||
|
)
|
||||||
|
flag.StringVar(
|
||||||
|
&token,
|
||||||
|
"token",
|
||||||
|
"",
|
||||||
|
"Your personal access token",
|
||||||
|
)
|
||||||
|
flag.BoolVar(
|
||||||
|
&flagPkg.ReadOnly,
|
||||||
|
"read-only",
|
||||||
|
false,
|
||||||
|
"Read-only mode",
|
||||||
|
)
|
||||||
|
flag.BoolVar(
|
||||||
|
&flagPkg.Debug,
|
||||||
|
"d",
|
||||||
|
false,
|
||||||
|
"debug mode (If -d flag is provided, debug mode will be enabled by default)",
|
||||||
|
)
|
||||||
|
flag.BoolVar(
|
||||||
|
&flagPkg.Insecure,
|
||||||
|
"insecure",
|
||||||
|
false,
|
||||||
|
"ignore TLS certificate errors",
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
flagPkg.Host = host
|
||||||
|
if flagPkg.Host == "" {
|
||||||
|
flagPkg.Host = "https://gitea.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
flagPkg.Port = port
|
||||||
|
|
||||||
|
flagPkg.Token = token
|
||||||
|
if flagPkg.Token == "" {
|
||||||
|
flagPkg.Token = os.Getenv("GITEA_ACCESS_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("MCP_MODE") != "" {
|
||||||
|
flagPkg.Mode = os.Getenv("MCP_MODE")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("GITEA_READONLY") == "true" {
|
||||||
|
flagPkg.ReadOnly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("GITEA_DEBUG") == "true" {
|
||||||
|
flagPkg.Debug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set insecure mode based on environment variable
|
||||||
|
if os.Getenv("GITEA_INSECURE") == "true" {
|
||||||
|
flagPkg.Insecure = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
defer log.Default().Sync()
|
||||||
|
if err := operation.Run(); err != nil {
|
||||||
|
if err == context.Canceled {
|
||||||
|
log.Info("Server shutdown due to context cancellation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Fatalf("Run Gitea MCP Server Error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": {
|
||||||
|
"-t": "stdio",
|
||||||
|
"--host": "https://gitea.com",
|
||||||
|
"--token": "<your personal access token>"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"GITEA_HOST": "https://gitea.com",
|
||||||
|
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Development compose file - builds from local source
|
|
||||||
# Usage: docker compose -f docker-compose.dev.yml up --build
|
|
||||||
services:
|
|
||||||
gitea-mcp:
|
|
||||||
build: .
|
|
||||||
container_name: gitea-mcp-dev
|
|
||||||
restart: "no"
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 5s
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
gitea-mcp:
|
|
||||||
image: gitea.ext.ben.io/b3nw/gitea-mcp:latest
|
|
||||||
container_name: gitea-mcp
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
environment:
|
|
||||||
- GITEA_URL=${GITEA_URL}
|
|
||||||
- GITEA_TOKEN=${GITEA_TOKEN}
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 5s
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
module gitea.com/gitea/gitea-mcp
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
code.gitea.io/sdk/gitea v0.21.0
|
||||||
|
github.com/mark3labs/mcp-go v0.30.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/42wim/httpsig v1.2.3 // indirect
|
||||||
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
|
github.com/spf13/cast v1.8.0 // indirect
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
||||||
|
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||||
|
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||||
|
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||||
|
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mark3labs/mcp-go v0.30.0 h1:Taz7fiefkxY/l8jz1nA90V+WdM2eoMtlvwfWforVYbo=
|
||||||
|
github.com/mark3labs/mcp-go v0.30.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||||
|
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.com/gitea/gitea-mcp/cmd"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Version = Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetIssueByIndexToolName = "get_issue_by_index"
|
||||||
|
ListRepoIssuesToolName = "list_repo_issues"
|
||||||
|
CreateIssueToolName = "create_issue"
|
||||||
|
CreateIssueCommentToolName = "create_issue_comment"
|
||||||
|
EditIssueToolName = "edit_issue"
|
||||||
|
EditIssueCommentToolName = "edit_issue_comment"
|
||||||
|
GetIssueCommentsByIndexToolName = "get_issue_comments_by_index"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GetIssueByIndexTool = mcp.NewTool(
|
||||||
|
GetIssueByIndexToolName,
|
||||||
|
mcp.WithDescription("get issue by index"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListRepoIssuesTool = mcp.NewTool(
|
||||||
|
ListRepoIssuesToolName,
|
||||||
|
mcp.WithDescription("List repository issues"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateIssueTool = mcp.NewTool(
|
||||||
|
CreateIssueToolName,
|
||||||
|
mcp.WithDescription("create issue"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("title", mcp.Required(), mcp.Description("issue title")),
|
||||||
|
mcp.WithString("body", mcp.Required(), mcp.Description("issue body")),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateIssueCommentTool = mcp.NewTool(
|
||||||
|
CreateIssueCommentToolName,
|
||||||
|
mcp.WithDescription("create issue comment"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")),
|
||||||
|
mcp.WithString("body", mcp.Required(), mcp.Description("issue comment body")),
|
||||||
|
)
|
||||||
|
|
||||||
|
EditIssueTool = mcp.NewTool(
|
||||||
|
EditIssueToolName,
|
||||||
|
mcp.WithDescription("edit issue"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")),
|
||||||
|
mcp.WithString("title", mcp.Description("issue title"), mcp.DefaultString("")),
|
||||||
|
mcp.WithString("body", mcp.Description("issue body content")),
|
||||||
|
mcp.WithArray("assignees", mcp.Description("usernames to assign to this issue"), mcp.Items(map[string]interface{}{"type": "string"})),
|
||||||
|
mcp.WithNumber("milestone", mcp.Description("milestone number")),
|
||||||
|
mcp.WithString("state", mcp.Description("issue state, one of open, closed, all")),
|
||||||
|
)
|
||||||
|
|
||||||
|
EditIssueCommentTool = mcp.NewTool(
|
||||||
|
EditIssueCommentToolName,
|
||||||
|
mcp.WithDescription("edit issue comment"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("commentID", mcp.Required(), mcp.Description("id of issue comment")),
|
||||||
|
mcp.WithString("body", mcp.Required(), mcp.Description("issue comment body")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetIssueCommentsByIndexTool = mcp.NewTool(
|
||||||
|
GetIssueCommentsByIndexToolName,
|
||||||
|
mcp.WithDescription("get issue comment by index"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetIssueByIndexTool,
|
||||||
|
Handler: GetIssueByIndexFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListRepoIssuesTool,
|
||||||
|
Handler: ListRepoIssuesFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateIssueTool,
|
||||||
|
Handler: CreateIssueFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateIssueCommentTool,
|
||||||
|
Handler: CreateIssueCommentFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: EditIssueTool,
|
||||||
|
Handler: EditIssueFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: EditIssueCommentTool,
|
||||||
|
Handler: EditIssueCommentFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetIssueCommentsByIndexTool,
|
||||||
|
Handler: GetIssueCommentsByIndexFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetIssueByIndexFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
issue, _, err := gitea.Client().GetIssue(owner, repo, int64(index))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListIssuesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
state, ok := req.GetArguments()["state"].(string)
|
||||||
|
if !ok {
|
||||||
|
state = "all"
|
||||||
|
}
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListIssueOption{
|
||||||
|
State: gitea_sdk.StateType(state),
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
issues, _, err := gitea.Client().ListRepoIssues(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/issues err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateIssueFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
title, ok := req.GetArguments()["title"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("title is required"))
|
||||||
|
}
|
||||||
|
body, ok := req.GetArguments()["body"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("body is required"))
|
||||||
|
}
|
||||||
|
issue, _, err := gitea.Client().CreateIssue(owner, repo, gitea_sdk.CreateIssueOption{
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateIssueCommentFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
body, ok := req.GetArguments()["body"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("body is required"))
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.CreateIssueCommentOption{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
issueComment, _, err := gitea.Client().CreateIssueComment(owner, repo, int64(index), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(issueComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called EditIssueFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea_sdk.EditIssueOption{}
|
||||||
|
|
||||||
|
title, ok := req.GetArguments()["title"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.Title = title
|
||||||
|
}
|
||||||
|
body, ok := req.GetArguments()["body"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.Body = ptr.To(body)
|
||||||
|
}
|
||||||
|
assignees, ok := req.GetArguments()["assignees"].([]string)
|
||||||
|
if ok {
|
||||||
|
opt.Assignees = assignees
|
||||||
|
}
|
||||||
|
milestone, ok := req.GetArguments()["milestone"].(float64)
|
||||||
|
if ok {
|
||||||
|
opt.Milestone = ptr.To(int64(milestone))
|
||||||
|
}
|
||||||
|
state, ok := req.GetArguments()["state"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.State = ptr.To(gitea_sdk.StateType(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, _, err := gitea.Client().EditIssue(owner, repo, int64(index), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called EditIssueCommentFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
commentID, ok := req.GetArguments()["commentID"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("comment ID is required"))
|
||||||
|
}
|
||||||
|
body, ok := req.GetArguments()["body"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("body is required"))
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.EditIssueCommentOption{
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
issueComment, _, err := gitea.Client().EditIssueComment(owner, repo, int64(commentID), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, int64(commentID), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(issueComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetIssueCommentsByIndexFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListIssueCommentOptions{}
|
||||||
|
issue, _, err := gitea.Client().ListIssueComments(owner, repo, int64(index), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(issue)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package operation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/issue"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/pull"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/repo"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/search"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/user"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/version"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mcpServer *server.MCPServer
|
||||||
|
|
||||||
|
func RegisterTool(s *server.MCPServer) {
|
||||||
|
// User Tool
|
||||||
|
s.AddTools(user.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Repo Tool
|
||||||
|
s.AddTools(repo.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Issue Tool
|
||||||
|
s.AddTools(issue.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Pull Tool
|
||||||
|
s.AddTools(pull.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Search Tool
|
||||||
|
s.AddTools(search.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Version Tool
|
||||||
|
s.AddTools(version.Tool.Tools()...)
|
||||||
|
|
||||||
|
s.DeleteTools("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
mcpServer = newMCPServer(flag.Version)
|
||||||
|
RegisterTool(mcpServer)
|
||||||
|
switch flag.Mode {
|
||||||
|
case "stdio":
|
||||||
|
if err := server.ServeStdio(mcpServer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "sse":
|
||||||
|
sseServer := server.NewSSEServer(mcpServer)
|
||||||
|
log.Infof("Gitea MCP SSE server listening on :%d", flag.Port)
|
||||||
|
if err := sseServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "http":
|
||||||
|
httpServer := server.NewStreamableHTTPServer(mcpServer)
|
||||||
|
log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port)
|
||||||
|
if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid transport type: %s. Must be 'stdio', 'sse' or 'http'", flag.Mode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMCPServer(version string) *server.MCPServer {
|
||||||
|
return server.NewMCPServer(
|
||||||
|
"Gitea MCP Server",
|
||||||
|
version,
|
||||||
|
server.WithToolCapabilities(true),
|
||||||
|
server.WithLogging(),
|
||||||
|
server.WithRecovery(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package pull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetPullRequestByIndexToolName = "get_pull_request_by_index"
|
||||||
|
ListRepoPullRequestsToolName = "list_repo_pull_requests"
|
||||||
|
CreatePullRequestToolName = "create_pull_request"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GetPullRequestByIndexTool = mcp.NewTool(
|
||||||
|
GetPullRequestByIndexToolName,
|
||||||
|
mcp.WithDescription("get pull request by index"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListRepoPullRequestsTool = mcp.NewTool(
|
||||||
|
ListRepoPullRequestsToolName,
|
||||||
|
mcp.WithDescription("List repository pull requests"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("state", mcp.Description("state"), mcp.Enum("open", "closed", "all"), mcp.DefaultString("all")),
|
||||||
|
mcp.WithString("sort", mcp.Description("sort"), mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")),
|
||||||
|
mcp.WithNumber("milestone", mcp.Description("milestone")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreatePullRequestTool = mcp.NewTool(
|
||||||
|
CreatePullRequestToolName,
|
||||||
|
mcp.WithDescription("create pull request"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("title", mcp.Required(), mcp.Description("pull request title")),
|
||||||
|
mcp.WithString("body", mcp.Required(), mcp.Description("pull request body")),
|
||||||
|
mcp.WithString("head", mcp.Required(), mcp.Description("pull request head")),
|
||||||
|
mcp.WithString("base", mcp.Required(), mcp.Description("pull request base")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetPullRequestByIndexTool,
|
||||||
|
Handler: GetPullRequestByIndexFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListRepoPullRequestsTool,
|
||||||
|
Handler: ListRepoPullRequestsFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreatePullRequestTool,
|
||||||
|
Handler: CreatePullRequestFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetPullRequestByIndexFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
pr, _, err := gitea.Client().GetPullRequest(owner, repo, int64(index))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoPullRequests")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
state, _ := req.GetArguments()["state"].(string)
|
||||||
|
sort, ok := req.GetArguments()["sort"].(string)
|
||||||
|
if !ok {
|
||||||
|
sort = "recentupdate"
|
||||||
|
}
|
||||||
|
milestone, _ := req.GetArguments()["milestone"].(float64)
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListPullRequestsOptions{
|
||||||
|
State: gitea_sdk.StateType(state),
|
||||||
|
Sort: sort,
|
||||||
|
Milestone: int64(milestone),
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pullRequests, _, err := gitea.Client().ListRepoPullRequests(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(pullRequests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreatePullRequestFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
title, ok := req.GetArguments()["title"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("title is required"))
|
||||||
|
}
|
||||||
|
body, ok := req.GetArguments()["body"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("body is required"))
|
||||||
|
}
|
||||||
|
head, ok := req.GetArguments()["head"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("head is required"))
|
||||||
|
}
|
||||||
|
base, ok := req.GetArguments()["base"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("base is required"))
|
||||||
|
}
|
||||||
|
pr, _, err := gitea.Client().CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
Head: head,
|
||||||
|
Base: base,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(pr)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CreateBranchToolName = "create_branch"
|
||||||
|
DeleteBranchToolName = "delete_branch"
|
||||||
|
ListBranchesToolName = "list_branches"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CreateBranchTool = mcp.NewTool(
|
||||||
|
CreateBranchToolName,
|
||||||
|
mcp.WithDescription("Create branch"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("branch", mcp.Required(), mcp.Description("Name of the branch to create")),
|
||||||
|
mcp.WithString("old_branch", mcp.Required(), mcp.Description("Name of the old branch to create from")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteBranchTool = mcp.NewTool(
|
||||||
|
DeleteBranchToolName,
|
||||||
|
mcp.WithDescription("Delete branch"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("branch", mcp.Required(), mcp.Description("Name of the branch to delete")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListBranchesTool = mcp.NewTool(
|
||||||
|
ListBranchesToolName,
|
||||||
|
mcp.WithDescription("List branches"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateBranchTool,
|
||||||
|
Handler: CreateBranchFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeleteBranchTool,
|
||||||
|
Handler: DeleteBranchFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListBranchesTool,
|
||||||
|
Handler: ListBranchesFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateBranchFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
branch, ok := req.GetArguments()["branch"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("branch is required"))
|
||||||
|
}
|
||||||
|
oldBranch, _ := req.GetArguments()["old_branch"].(string)
|
||||||
|
|
||||||
|
_, _, err := gitea.Client().CreateBranch(owner, repo, gitea_sdk.CreateBranchOption{
|
||||||
|
BranchName: branch,
|
||||||
|
OldBranchName: oldBranch,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create branch error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText("Branch Created"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteBranchFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
branch, ok := req.GetArguments()["branch"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("branch is required"))
|
||||||
|
}
|
||||||
|
_, _, err := gitea.Client().DeleteRepoBranch(owner, repo, branch)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete branch error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult("Branch Deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListBranchesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListRepoBranchesOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
branches, _, err := gitea.Client().ListRepoBranches(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list branches error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(branches)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ListRepoCommitsToolName = "list_repo_commits"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ListRepoCommitsTool = mcp.NewTool(
|
||||||
|
ListRepoCommitsToolName,
|
||||||
|
mcp.WithDescription("List repository commits"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("sha", mcp.Description("SHA or branch to start listing commits from")),
|
||||||
|
mcp.WithString("path", mcp.Description("path indicates that only commits that include the path's file/dir should be returned.")),
|
||||||
|
mcp.WithNumber("page", mcp.Required(), mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("page_size", mcp.Required(), mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListRepoCommitsTool,
|
||||||
|
Handler: ListRepoCommitsFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoCommitsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("page is required"))
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["page_size"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("page_size is required"))
|
||||||
|
}
|
||||||
|
sha, _ := req.GetArguments()["sha"].(string)
|
||||||
|
path, _ := req.GetArguments()["path"].(string)
|
||||||
|
opt := gitea_sdk.ListCommitOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
SHA: sha,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
commits, _, err := gitea.Client().ListRepoCommits(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list repo commits err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(commits)
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetFileToolName = "get_file_content"
|
||||||
|
GetDirToolName = "get_dir_content"
|
||||||
|
CreateFileToolName = "create_file"
|
||||||
|
UpdateFileToolName = "update_file"
|
||||||
|
DeleteFileToolName = "delete_file"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GetFileContentTool = mcp.NewTool(
|
||||||
|
GetFileToolName,
|
||||||
|
mcp.WithDescription("Get file Content and Metadata"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("ref", mcp.Required(), mcp.Description("ref can be branch/tag/commit")),
|
||||||
|
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetDirContentTool = mcp.NewTool(
|
||||||
|
GetDirToolName,
|
||||||
|
mcp.WithDescription("Get a list of entries in a directory"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("ref", mcp.Required(), mcp.Description("ref can be branch/tag/commit")),
|
||||||
|
mcp.WithString("filePath", mcp.Required(), mcp.Description("directory path")),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateFileTool = mcp.NewTool(
|
||||||
|
CreateFileToolName,
|
||||||
|
mcp.WithDescription("Create file"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
|
||||||
|
mcp.WithString("content", mcp.Required(), mcp.Description("file content")),
|
||||||
|
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
|
||||||
|
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")),
|
||||||
|
mcp.WithString("new_branch_name", mcp.Description("new branch name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateFileTool = mcp.NewTool(
|
||||||
|
UpdateFileToolName,
|
||||||
|
mcp.WithDescription("Update file"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
|
||||||
|
mcp.WithString("sha", mcp.Required(), mcp.Description("sha is the SHA for the file that already exists")),
|
||||||
|
mcp.WithString("content", mcp.Required(), mcp.Description("file content, base64 encoded")),
|
||||||
|
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
|
||||||
|
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteFileTool = mcp.NewTool(
|
||||||
|
DeleteFileToolName,
|
||||||
|
mcp.WithDescription("Delete file"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
|
||||||
|
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
|
||||||
|
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")),
|
||||||
|
mcp.WithString("sha", mcp.Description("sha")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetFileContentTool,
|
||||||
|
Handler: GetFileContentFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetDirContentTool,
|
||||||
|
Handler: GetDirContentFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateFileTool,
|
||||||
|
Handler: CreateFileFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: UpdateFileTool,
|
||||||
|
Handler: UpdateFileFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeleteFileTool,
|
||||||
|
Handler: DeleteFileFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetFileFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
ref, _ := req.GetArguments()["ref"].(string)
|
||||||
|
filePath, ok := req.GetArguments()["filePath"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("filePath is required"))
|
||||||
|
}
|
||||||
|
content, _, err := gitea.Client().GetContents(owner, repo, ref, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get file err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetDirContentFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
ref, _ := req.GetArguments()["ref"].(string)
|
||||||
|
filePath, ok := req.GetArguments()["filePath"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("filePath is required"))
|
||||||
|
}
|
||||||
|
content, _, err := gitea.Client().ListContents(owner, repo, ref, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get dir content err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateFileFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
filePath, ok := req.GetArguments()["filePath"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("filePath is required"))
|
||||||
|
}
|
||||||
|
content, _ := req.GetArguments()["content"].(string)
|
||||||
|
message, _ := req.GetArguments()["message"].(string)
|
||||||
|
branchName, _ := req.GetArguments()["branch_name"].(string)
|
||||||
|
opt := gitea_sdk.CreateFileOptions{
|
||||||
|
Content: base64.StdEncoding.EncodeToString([]byte(content)),
|
||||||
|
FileOptions: gitea_sdk.FileOptions{
|
||||||
|
Message: message,
|
||||||
|
BranchName: branchName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := gitea.Client().CreateFile(owner, repo, filePath, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create file err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult("Create file success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called UpdateFileFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
filePath, ok := req.GetArguments()["filePath"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("filePath is required"))
|
||||||
|
}
|
||||||
|
sha, ok := req.GetArguments()["sha"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("sha is required"))
|
||||||
|
}
|
||||||
|
content, _ := req.GetArguments()["content"].(string)
|
||||||
|
message, _ := req.GetArguments()["message"].(string)
|
||||||
|
branchName, _ := req.GetArguments()["branch_name"].(string)
|
||||||
|
|
||||||
|
opt := gitea_sdk.UpdateFileOptions{
|
||||||
|
SHA: sha,
|
||||||
|
Content: base64.StdEncoding.EncodeToString([]byte(content)),
|
||||||
|
FileOptions: gitea_sdk.FileOptions{
|
||||||
|
Message: message,
|
||||||
|
BranchName: branchName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, _, err := gitea.Client().UpdateFile(owner, repo, filePath, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("update file err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult("Update file success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteFileFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
filePath, ok := req.GetArguments()["filePath"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("filePath is required"))
|
||||||
|
}
|
||||||
|
message, _ := req.GetArguments()["message"].(string)
|
||||||
|
branchName, _ := req.GetArguments()["branch_name"].(string)
|
||||||
|
sha, ok := req.GetArguments()["sha"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("sha is required"))
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.DeleteFileOptions{
|
||||||
|
FileOptions: gitea_sdk.FileOptions{
|
||||||
|
Message: message,
|
||||||
|
BranchName: branchName,
|
||||||
|
},
|
||||||
|
SHA: sha,
|
||||||
|
}
|
||||||
|
_, err := gitea.Client().DeleteFile(owner, repo, filePath, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete file err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult("Delete file success")
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CreateReleaseToolName = "create_release"
|
||||||
|
DeleteReleaseToolName = "delete_release"
|
||||||
|
GetReleaseToolName = "get_release"
|
||||||
|
GetLatestReleaseToolName = "get_latest_release"
|
||||||
|
ListReleasesToolName = "list_releases"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CreateReleaseTool = mcp.NewTool(
|
||||||
|
CreateReleaseToolName,
|
||||||
|
mcp.WithDescription("Create release"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")),
|
||||||
|
mcp.WithString("target", mcp.Required(), mcp.Description("target commitish")),
|
||||||
|
mcp.WithString("title", mcp.Required(), mcp.Description("release title")),
|
||||||
|
mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)),
|
||||||
|
mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteReleaseTool = mcp.NewTool(
|
||||||
|
DeleteReleaseToolName,
|
||||||
|
mcp.WithDescription("Delete release"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("release id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetReleaseTool = mcp.NewTool(
|
||||||
|
GetReleaseToolName,
|
||||||
|
mcp.WithDescription("Get release"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("release id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetLatestReleaseTool = mcp.NewTool(
|
||||||
|
GetLatestReleaseToolName,
|
||||||
|
mcp.WithDescription("Get latest release"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListReleasesTool = mcp.NewTool(
|
||||||
|
ListReleasesToolName,
|
||||||
|
mcp.WithDescription("List releases"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)),
|
||||||
|
mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(20), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateReleaseTool,
|
||||||
|
Handler: CreateReleaseFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeleteReleaseTool,
|
||||||
|
Handler: DeleteReleaseFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetReleaseTool,
|
||||||
|
Handler: GetReleaseFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetLatestReleaseTool,
|
||||||
|
Handler: GetLatestReleaseFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListReleasesTool,
|
||||||
|
Handler: ListReleasesFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid return too many tokens, we need to provide at least information as possible
|
||||||
|
// llm can call get release to get more information
|
||||||
|
type ListReleaseResult struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Target string `json:"target_commitish"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
IsDraft bool `json:"draft"`
|
||||||
|
IsPrerelease bool `json:"prerelease"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
PublishedAt time.Time `json:"published_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateReleasesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
tagName, ok := req.GetArguments()["tag_name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("tag_name is required")
|
||||||
|
}
|
||||||
|
target, ok := req.GetArguments()["target"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("target is required")
|
||||||
|
}
|
||||||
|
title, ok := req.GetArguments()["title"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("title is required")
|
||||||
|
}
|
||||||
|
isDraft, _ := req.GetArguments()["is_draft"].(bool)
|
||||||
|
isPreRelease, _ := req.GetArguments()["is_pre_release"].(bool)
|
||||||
|
|
||||||
|
_, _, err := gitea.Client().CreateRelease(owner, repo, gitea_sdk.CreateReleaseOption{
|
||||||
|
TagName: tagName,
|
||||||
|
Target: target,
|
||||||
|
Title: title,
|
||||||
|
IsDraft: isDraft,
|
||||||
|
IsPrerelease: isPreRelease,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create release error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText("Release Created"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteReleaseFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
id, ok := req.GetArguments()["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := gitea.Client().DeleteRelease(owner, repo, int64(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("delete release error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult("Release deleted successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetReleaseFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
id, ok := req.GetArguments()["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
release, _, err := gitea.Client().GetRelease(owner, repo, int64(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get release error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(release)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetLatestReleaseFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
release, _, err := gitea.Client().GetLatestRelease(owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get latest release error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(release)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListReleasesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
var pIsDraft *bool
|
||||||
|
isDraft, ok := req.GetArguments()["is_draft"].(bool)
|
||||||
|
if ok {
|
||||||
|
pIsDraft = ptr.To(isDraft)
|
||||||
|
}
|
||||||
|
var pIsPreRelease *bool
|
||||||
|
isPreRelease, ok := req.GetArguments()["is_pre_release"].(bool)
|
||||||
|
if ok {
|
||||||
|
pIsPreRelease = ptr.To(isPreRelease)
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
|
||||||
|
releases, _, err := gitea.Client().ListReleases(owner, repo, gitea_sdk.ListReleasesOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
IsDraft: pIsDraft,
|
||||||
|
IsPreRelease: pIsPreRelease,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list releases error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]ListReleaseResult, len(releases))
|
||||||
|
for _, release := range releases {
|
||||||
|
results = append(results, ListReleaseResult{
|
||||||
|
ID: release.ID,
|
||||||
|
TagName: release.TagName,
|
||||||
|
Target: release.Target,
|
||||||
|
Title: release.Title,
|
||||||
|
IsDraft: release.IsDraft,
|
||||||
|
IsPrerelease: release.IsPrerelease,
|
||||||
|
CreatedAt: release.CreatedAt,
|
||||||
|
PublishedAt: release.PublishedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return to.TextResult(results)
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
CreateRepoToolName = "create_repo"
|
||||||
|
ForkRepoToolName = "fork_repo"
|
||||||
|
ListMyReposToolName = "list_my_repos"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CreateRepoTool = mcp.NewTool(
|
||||||
|
CreateRepoToolName,
|
||||||
|
mcp.WithDescription("Create repository"),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("Name of the repository to create")),
|
||||||
|
mcp.WithString("description", mcp.Description("Description of the repository to create")),
|
||||||
|
mcp.WithBoolean("private", mcp.Description("Whether the repository is private")),
|
||||||
|
mcp.WithString("issue_labels", mcp.Description("Issue Label set to use")),
|
||||||
|
mcp.WithBoolean("auto_init", mcp.Description("Whether the repository should be auto-intialized?")),
|
||||||
|
mcp.WithBoolean("template", mcp.Description("Whether the repository is template")),
|
||||||
|
mcp.WithString("gitignores", mcp.Description("Gitignores to use")),
|
||||||
|
mcp.WithString("license", mcp.Description("License to use")),
|
||||||
|
mcp.WithString("readme", mcp.Description("Readme of the repository to create")),
|
||||||
|
mcp.WithString("default_branch", mcp.Description("DefaultBranch of the repository (used when initializes and in template)")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ForkRepoTool = mcp.NewTool(
|
||||||
|
ForkRepoToolName,
|
||||||
|
mcp.WithDescription("Fork repository"),
|
||||||
|
mcp.WithString("user", mcp.Required(), mcp.Description("User name of the repository to fork")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name to fork")),
|
||||||
|
mcp.WithString("organization", mcp.Description("Organization name to fork")),
|
||||||
|
mcp.WithString("name", mcp.Description("Name of the forked repository")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListMyReposTool = mcp.NewTool(
|
||||||
|
ListMyReposToolName,
|
||||||
|
mcp.WithDescription("List my repositories"),
|
||||||
|
mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Required(), mcp.Description("Page size number"), mcp.DefaultNumber(100), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateRepoTool,
|
||||||
|
Handler: CreateRepoFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: ForkRepoTool,
|
||||||
|
Handler: ForkRepoFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListMyReposTool,
|
||||||
|
Handler: ListMyReposFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterTool(s *server.MCPServer) {
|
||||||
|
s.AddTool(CreateRepoTool, CreateRepoFn)
|
||||||
|
s.AddTool(ForkRepoTool, ForkRepoFn)
|
||||||
|
s.AddTool(ListMyReposTool, ListMyReposFn)
|
||||||
|
|
||||||
|
// File
|
||||||
|
s.AddTool(GetFileContentTool, GetFileContentFn)
|
||||||
|
s.AddTool(CreateFileTool, CreateFileFn)
|
||||||
|
s.AddTool(UpdateFileTool, UpdateFileFn)
|
||||||
|
s.AddTool(DeleteFileTool, DeleteFileFn)
|
||||||
|
|
||||||
|
// Branch
|
||||||
|
s.AddTool(CreateBranchTool, CreateBranchFn)
|
||||||
|
s.AddTool(DeleteBranchTool, DeleteBranchFn)
|
||||||
|
s.AddTool(ListBranchesTool, ListBranchesFn)
|
||||||
|
|
||||||
|
// Release
|
||||||
|
s.AddTool(CreateReleaseTool, CreateReleaseFn)
|
||||||
|
s.AddTool(DeleteReleaseTool, DeleteReleaseFn)
|
||||||
|
s.AddTool(GetReleaseTool, GetReleaseFn)
|
||||||
|
s.AddTool(GetLatestReleaseTool, GetLatestReleaseFn)
|
||||||
|
s.AddTool(ListReleasesTool, ListReleasesFn)
|
||||||
|
|
||||||
|
// Tag
|
||||||
|
s.AddTool(CreateTagTool, CreateTagFn)
|
||||||
|
s.AddTool(DeleteTagTool, DeleteTagFn)
|
||||||
|
s.AddTool(GetTagTool, GetTagFn)
|
||||||
|
s.AddTool(ListTagsTool, ListTagsFn)
|
||||||
|
|
||||||
|
// Commit
|
||||||
|
s.AddTool(ListRepoCommitsTool, ListRepoCommitsFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateRepoFn")
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(errors.New("repository name is required"))
|
||||||
|
}
|
||||||
|
description, _ := req.GetArguments()["description"].(string)
|
||||||
|
private, _ := req.GetArguments()["private"].(bool)
|
||||||
|
issueLabels, _ := req.GetArguments()["issue_labels"].(string)
|
||||||
|
autoInit, _ := req.GetArguments()["auto_init"].(bool)
|
||||||
|
template, _ := req.GetArguments()["template"].(bool)
|
||||||
|
gitignores, _ := req.GetArguments()["gitignores"].(string)
|
||||||
|
license, _ := req.GetArguments()["license"].(string)
|
||||||
|
readme, _ := req.GetArguments()["readme"].(string)
|
||||||
|
defaultBranch, _ := req.GetArguments()["default_branch"].(string)
|
||||||
|
|
||||||
|
opt := gitea_sdk.CreateRepoOption{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Private: private,
|
||||||
|
IssueLabels: issueLabels,
|
||||||
|
AutoInit: autoInit,
|
||||||
|
Template: template,
|
||||||
|
Gitignores: gitignores,
|
||||||
|
License: license,
|
||||||
|
Readme: readme,
|
||||||
|
DefaultBranch: defaultBranch,
|
||||||
|
}
|
||||||
|
repo, _, err := gitea.Client().CreateRepo(opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create repo err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ForkRepoFn")
|
||||||
|
user, ok := req.GetArguments()["user"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(errors.New("user name is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(errors.New("repository name is required"))
|
||||||
|
}
|
||||||
|
organization, ok := req.GetArguments()["organization"].(string)
|
||||||
|
organizationPtr := ptr.To(organization)
|
||||||
|
if !ok || organization == "" {
|
||||||
|
organizationPtr = nil
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
namePtr := ptr.To(name)
|
||||||
|
if !ok || name == "" {
|
||||||
|
namePtr = nil
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.CreateForkOption{
|
||||||
|
Organization: organizationPtr,
|
||||||
|
Name: namePtr,
|
||||||
|
}
|
||||||
|
_, _, err := gitea.Client().CreateFork(user, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("fork repository error: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult("Fork success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListMyReposFn")
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListReposOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repos, _, err := gitea.Client().ListMyRepos(opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list my repositories error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(repos)
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CreateTagToolName = "create_tag"
|
||||||
|
DeleteTagToolName = "delete_tag"
|
||||||
|
GetTagToolName = "get_tag"
|
||||||
|
ListTagsToolName = "list_tags"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CreateTagTool = mcp.NewTool(
|
||||||
|
CreateTagToolName,
|
||||||
|
mcp.WithDescription("Create tag"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")),
|
||||||
|
mcp.WithString("target", mcp.Description("target commitish"), mcp.DefaultString("")),
|
||||||
|
mcp.WithString("message", mcp.Description("tag message"), mcp.DefaultString("")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteTagTool = mcp.NewTool(
|
||||||
|
DeleteTagToolName,
|
||||||
|
mcp.WithDescription("Delete tag"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetTagTool = mcp.NewTool(
|
||||||
|
GetTagToolName,
|
||||||
|
mcp.WithDescription("Get tag"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListTagsTool = mcp.NewTool(
|
||||||
|
ListTagsToolName,
|
||||||
|
mcp.WithDescription("List tags"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(20), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateTagTool,
|
||||||
|
Handler: CreateTagFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeleteTagTool,
|
||||||
|
Handler: DeleteTagFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetTagTool,
|
||||||
|
Handler: GetTagFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListTagsTool,
|
||||||
|
Handler: ListTagsFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid return too many tokens, we need to provide at least information as possible
|
||||||
|
// llm can call get tag to get more information
|
||||||
|
type ListTagResult struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Commit *gitea_sdk.CommitMeta `json:"commit"`
|
||||||
|
// message may be a long text, so we should not provide it here
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateTagFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
tagName, ok := req.GetArguments()["tag_name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("tag_name is required")
|
||||||
|
}
|
||||||
|
target, _ := req.GetArguments()["target"].(string)
|
||||||
|
message, _ := req.GetArguments()["message"].(string)
|
||||||
|
|
||||||
|
_, _, err := gitea.Client().CreateTag(owner, repo, gitea_sdk.CreateTagOption{
|
||||||
|
TagName: tagName,
|
||||||
|
Target: target,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create tag error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText("Tag Created"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteTagFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
tagName, ok := req.GetArguments()["tag_name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("tag_name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := gitea.Client().DeleteTag(owner, repo, tagName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("delete tag error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult("Tag deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetTagFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
tagName, ok := req.GetArguments()["tag_name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("tag_name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, _, err := gitea.Client().GetTag(owner, repo, tagName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get tag error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListTagsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("owner is required")
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("repo is required")
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
|
||||||
|
tags, _, err := gitea.Client().ListRepoTags(owner, repo, gitea_sdk.ListRepoTagsOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list tags error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]ListTagResult, 0, len(tags))
|
||||||
|
for _, tag := range tags {
|
||||||
|
results = append(results, ListTagResult{
|
||||||
|
ID: tag.ID,
|
||||||
|
Name: tag.Name,
|
||||||
|
Commit: tag.Commit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return to.TextResult(results)
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
SearchUsersToolName = "search_users"
|
||||||
|
SearchOrgTeamsToolName = "search_org_teams"
|
||||||
|
SearchReposToolName = "search_repos"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
SearchUsersTool = mcp.NewTool(
|
||||||
|
SearchUsersToolName,
|
||||||
|
mcp.WithDescription("search users"),
|
||||||
|
mcp.WithString("keyword", mcp.Description("Keyword")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
SearOrgTeamsTool = mcp.NewTool(
|
||||||
|
SearchOrgTeamsToolName,
|
||||||
|
mcp.WithDescription("search organization teams"),
|
||||||
|
mcp.WithString("org", mcp.Description("organization name")),
|
||||||
|
mcp.WithString("query", mcp.Description("search organization teams")),
|
||||||
|
mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
SearchReposTool = mcp.NewTool(
|
||||||
|
SearchReposToolName,
|
||||||
|
mcp.WithDescription("search repos"),
|
||||||
|
mcp.WithString("keyword", mcp.Description("Keyword")),
|
||||||
|
mcp.WithBoolean("keywordIsTopic", mcp.Description("KeywordIsTopic")),
|
||||||
|
mcp.WithBoolean("keywordInDescription", mcp.Description("KeywordInDescription")),
|
||||||
|
mcp.WithNumber("ownerID", mcp.Description("OwnerID")),
|
||||||
|
mcp.WithBoolean("isPrivate", mcp.Description("IsPrivate")),
|
||||||
|
mcp.WithBoolean("isArchived", mcp.Description("IsArchived")),
|
||||||
|
mcp.WithString("sort", mcp.Description("Sort")),
|
||||||
|
mcp.WithString("order", mcp.Description("Order")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: SearchUsersTool,
|
||||||
|
Handler: SearchUsersFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: SearOrgTeamsTool,
|
||||||
|
Handler: SearchOrgTeamsFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: SearchReposTool,
|
||||||
|
Handler: SearchReposFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called SearchUsersFn")
|
||||||
|
keyword, ok := req.GetArguments()["keyword"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("keyword is required"))
|
||||||
|
}
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.SearchUsersOption{
|
||||||
|
KeyWord: keyword,
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
users, _, err := gitea.Client().SearchUsers(opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("search users err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called SearchOrgTeamsFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("organization is required"))
|
||||||
|
}
|
||||||
|
query, ok := req.GetArguments()["query"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("query is required"))
|
||||||
|
}
|
||||||
|
includeDescription, _ := req.GetArguments()["includeDescription"].(bool)
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.SearchTeamsOptions{
|
||||||
|
Query: query,
|
||||||
|
IncludeDescription: includeDescription,
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
teams, _, err := gitea.Client().SearchOrgTeams(org, &opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(teams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called SearchReposFn")
|
||||||
|
keyword, ok := req.GetArguments()["keyword"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("keyword is required"))
|
||||||
|
}
|
||||||
|
keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool)
|
||||||
|
keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool)
|
||||||
|
ownerID, _ := req.GetArguments()["ownerID"].(float64)
|
||||||
|
var pIsPrivate *bool
|
||||||
|
isPrivate, ok := req.GetArguments()["isPrivate"].(bool)
|
||||||
|
if ok {
|
||||||
|
pIsPrivate = ptr.To(isPrivate)
|
||||||
|
}
|
||||||
|
var pIsArchived *bool
|
||||||
|
isArchived, ok := req.GetArguments()["isArchived"].(bool)
|
||||||
|
if ok {
|
||||||
|
pIsArchived = ptr.To(isArchived)
|
||||||
|
}
|
||||||
|
sort, _ := req.GetArguments()["sort"].(string)
|
||||||
|
order, _ := req.GetArguments()["order"].(string)
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.SearchRepoOptions{
|
||||||
|
Keyword: keyword,
|
||||||
|
KeywordIsTopic: keywordIsTopic,
|
||||||
|
KeywordInDescription: keywordInDescription,
|
||||||
|
OwnerID: int64(ownerID),
|
||||||
|
IsPrivate: pIsPrivate,
|
||||||
|
IsArchived: pIsArchived,
|
||||||
|
Sort: sort,
|
||||||
|
Order: order,
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repos, _, err := gitea.Client().SearchRepos(opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("search repos error: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(repos)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetMyUserInfoToolName = "get_my_user_info"
|
||||||
|
GetUserOrgsToolName = "get_user_orgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
var (
|
||||||
|
GetMyUserInfoTool = mcp.NewTool(
|
||||||
|
GetMyUserInfoToolName,
|
||||||
|
mcp.WithDescription("Get my user info"),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetUserOrgsTool = mcp.NewTool(
|
||||||
|
GetUserOrgsToolName,
|
||||||
|
mcp.WithDescription("Get organizations associated with the authenticated user"),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetMyUserInfoTool,
|
||||||
|
Handler: GetUserInfoFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetUserOrgsTool,
|
||||||
|
Handler: GetUserOrgsFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetUserInfoFn")
|
||||||
|
user, _, err := gitea.Client().GetMyUserInfo()
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get user info err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetUserOrgsFn")
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok || page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok || pageSize < 1 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListOrgsOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
orgs, _, err := gitea.Client().ListMyOrgs(opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get user orgs err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(orgs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetGiteaMCPServerVersion = "get_gitea_mcp_server_version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var GetGiteaMCPServerVersionTool = mcp.NewTool(
|
||||||
|
GetGiteaMCPServerVersion,
|
||||||
|
mcp.WithDescription("Get Gitea MCP Server Version"),
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetGiteaMCPServerVersionTool,
|
||||||
|
Handler: GetGiteaMCPServerVersionFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGiteaMCPServerVersionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetGiteaMCPServerVersionFn")
|
||||||
|
version := flag.Version
|
||||||
|
if version == "" {
|
||||||
|
version = "dev"
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Gitea MCP Server version: %v", version))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package flag
|
||||||
|
|
||||||
|
var (
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Token string
|
||||||
|
Version string
|
||||||
|
Mode string
|
||||||
|
|
||||||
|
Insecure bool
|
||||||
|
ReadOnly bool
|
||||||
|
Debug bool
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
client *gitea.Client
|
||||||
|
clientOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func Client() *gitea.Client {
|
||||||
|
clientOnce.Do(func() {
|
||||||
|
var err error
|
||||||
|
if client != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: http.DefaultTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []gitea.ClientOption{
|
||||||
|
gitea.SetToken(flag.Token),
|
||||||
|
}
|
||||||
|
if flag.Insecure {
|
||||||
|
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts = append(opts, gitea.SetHTTPClient(httpClient))
|
||||||
|
if flag.Debug {
|
||||||
|
opts = append(opts, gitea.SetDebugMode())
|
||||||
|
}
|
||||||
|
client, err = gitea.NewClient(flag.Host, opts...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("create gitea client err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return client
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultLoggerOnce sync.Once
|
||||||
|
defaultLogger *zap.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
func Default() *zap.Logger {
|
||||||
|
defaultLoggerOnce.Do(func() {
|
||||||
|
if defaultLogger == nil {
|
||||||
|
ec := zap.NewProductionEncoderConfig()
|
||||||
|
ec.EncodeTime = zapcore.TimeEncoderOfLayout(time.DateTime)
|
||||||
|
ec.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||||
|
|
||||||
|
var ws zapcore.WriteSyncer
|
||||||
|
var wss []zapcore.WriteSyncer
|
||||||
|
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
if home == "" {
|
||||||
|
home = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
logDir := fmt.Sprintf("%s/.gitea-mcp", home)
|
||||||
|
if err := os.MkdirAll(logDir, 0o700); err != nil {
|
||||||
|
// Fallback to temp directory if creation fails
|
||||||
|
logDir = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
wss = append(wss, zapcore.AddSync(&lumberjack.Logger{
|
||||||
|
Filename: fmt.Sprintf("%s/gitea-mcp.log", logDir),
|
||||||
|
MaxSize: 100,
|
||||||
|
MaxBackups: 10,
|
||||||
|
MaxAge: 30,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if flag.Mode == "http" || flag.Mode == "sse" {
|
||||||
|
wss = append(wss, zapcore.AddSync(os.Stdout))
|
||||||
|
}
|
||||||
|
|
||||||
|
ws = zapcore.NewMultiWriteSyncer(wss...)
|
||||||
|
|
||||||
|
enc := zapcore.NewConsoleEncoder(ec)
|
||||||
|
var level zapcore.Level
|
||||||
|
if flag.Debug {
|
||||||
|
level = zapcore.DebugLevel
|
||||||
|
} else {
|
||||||
|
level = zapcore.InfoLevel
|
||||||
|
}
|
||||||
|
core := zapcore.NewCore(enc, ws, level)
|
||||||
|
options := []zap.Option{
|
||||||
|
zap.AddStacktrace(zapcore.DPanicLevel),
|
||||||
|
zap.AddCaller(),
|
||||||
|
zap.AddCallerSkip(1),
|
||||||
|
}
|
||||||
|
defaultLogger = zap.New(core, options...)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return defaultLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDefault(logger *zap.Logger) {
|
||||||
|
if logger != nil {
|
||||||
|
defaultLogger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logger() *zap.Logger {
|
||||||
|
return defaultLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(msg string, fields ...zap.Field) {
|
||||||
|
Default().Debug(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(msg string, fields ...zap.Field) {
|
||||||
|
Default().Info(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(msg string, fields ...zap.Field) {
|
||||||
|
Default().Warn(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(msg string, fields ...zap.Field) {
|
||||||
|
Default().Error(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panic(msg string, fields ...zap.Field) {
|
||||||
|
Default().Panic(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(format string, args ...any) {
|
||||||
|
Default().Sugar().Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(format string, args ...any) {
|
||||||
|
Default().Sugar().Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warnf(format string, args ...any) {
|
||||||
|
Default().Sugar().Warnf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(format string, args ...any) {
|
||||||
|
Default().Sugar().Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatalf(format string, args ...any) {
|
||||||
|
Default().Sugar().Fatalf(format, args...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ptr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllPtrFieldsNil tests whether all pointer fields in a struct are nil. This is useful when,
|
||||||
|
// for example, an API struct is handled by plugins which need to distinguish
|
||||||
|
// "no plugin accepted this spec" from "this spec is empty".
|
||||||
|
//
|
||||||
|
// This function is only valid for structs and pointers to structs. Any other
|
||||||
|
// type will cause a panic. Passing a typed nil pointer will return true.
|
||||||
|
func AllPtrFieldsNil(obj interface{}) bool {
|
||||||
|
v := reflect.ValueOf(obj)
|
||||||
|
if !v.IsValid() {
|
||||||
|
panic(fmt.Sprintf("reflect.ValueOf() produced a non-valid Value for %#v", obj))
|
||||||
|
}
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
if v.Field(i).Kind() == reflect.Ptr && !v.Field(i).IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// To returns a pointer to the given value.
|
||||||
|
func To[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deref dereferences ptr and returns the value it points to if no nil, or else
|
||||||
|
// returns def.
|
||||||
|
func Deref[T any](ptr *T, def T) T {
|
||||||
|
if ptr != nil {
|
||||||
|
return *ptr
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if both arguments are nil or both arguments
|
||||||
|
// dereference to the same value.
|
||||||
|
func Equal[T comparable](a, b *T) bool {
|
||||||
|
if (a == nil) != (b == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *a == *b
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package to
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type textResult struct {
|
||||||
|
Result any
|
||||||
|
}
|
||||||
|
|
||||||
|
func TextResult(v any) (*mcp.CallToolResult, error) {
|
||||||
|
result := textResult{v}
|
||||||
|
resultBytes, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal result err: %v", err)
|
||||||
|
}
|
||||||
|
log.Debugf("Text Result: %s", string(resultBytes))
|
||||||
|
return mcp.NewToolResultText(string(resultBytes)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorResult(err error) (*mcp.CallToolResult, error) {
|
||||||
|
log.Errorf(err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
write []server.ServerTool
|
||||||
|
read []server.ServerTool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Tool {
|
||||||
|
return &Tool{
|
||||||
|
write: make([]server.ServerTool, 100),
|
||||||
|
read: make([]server.ServerTool, 100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tool) RegisterWrite(s server.ServerTool) {
|
||||||
|
t.write = append(t.write, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tool) RegisterRead(s server.ServerTool) {
|
||||||
|
t.read = append(t.read, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tool) Tools() []server.ServerTool {
|
||||||
|
tools := make([]server.ServerTool, 0, len(t.write)+len(t.read))
|
||||||
|
if flag.ReadOnly {
|
||||||
|
tools = append(tools, t.read...)
|
||||||
|
return tools
|
||||||
|
}
|
||||||
|
tools = append(tools, t.write...)
|
||||||
|
tools = append(tools, t.read...)
|
||||||
|
return tools
|
||||||
|
}
|
||||||
-21796
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "gitea-mcp"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "MCP server for Gitea API"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
dependencies = [
|
|
||||||
"fastmcp>=2.0",
|
|
||||||
"httpx>=0.27",
|
|
||||||
"starlette>=0.40",
|
|
||||||
"uvicorn>=0.30",
|
|
||||||
"python-dotenv>=1.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
dev = [
|
|
||||||
"pytest>=8.0",
|
|
||||||
"pytest-asyncio>=0.24",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["hatchling"]
|
|
||||||
build-backend = "hatchling.build"
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
|
||||||
packages = ["."]
|
|
||||||
include = ["server.py"]
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
"""
|
|
||||||
Gitea MCP Server - Hybrid MCP Light implementation for Gitea API.
|
|
||||||
|
|
||||||
Provides 5 curated tools plus an API pass-through for complete API coverage.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from typing import Any
|
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from fastmcp import FastMCP
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
from starlette.routing import Mount, Route
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
GITEA_URL = os.getenv("GITEA_URL", "").rstrip("/")
|
|
||||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
|
|
||||||
|
|
||||||
# Initialize MCP server
|
|
||||||
mcp = FastMCP(
|
|
||||||
"Gitea MCP",
|
|
||||||
instructions="MCP server for Gitea API - provides repository, issue, and user management",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaClient:
|
|
||||||
"""HTTP client for Gitea API with token authentication."""
|
|
||||||
|
|
||||||
def __init__(self, base_url: str, token: str):
|
|
||||||
self.base_url = base_url.rstrip("/")
|
|
||||||
self.api_url = f"{self.base_url}/api/v1"
|
|
||||||
self.token = token
|
|
||||||
self._client: httpx.AsyncClient | None = None
|
|
||||||
|
|
||||||
async def _get_client(self) -> httpx.AsyncClient:
|
|
||||||
if self._client is None:
|
|
||||||
self._client = httpx.AsyncClient(
|
|
||||||
headers={
|
|
||||||
"Authorization": f"token {self.token}",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
timeout=30.0,
|
|
||||||
)
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
if self._client:
|
|
||||||
await self._client.aclose()
|
|
||||||
self._client = None
|
|
||||||
|
|
||||||
async def request(
|
|
||||||
self,
|
|
||||||
method: str,
|
|
||||||
endpoint: str,
|
|
||||||
params: dict[str, Any] | None = None,
|
|
||||||
json_body: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Execute an API request to Gitea."""
|
|
||||||
client = await self._get_client()
|
|
||||||
url = f"{self.api_url}{endpoint}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = await client.request(
|
|
||||||
method=method.upper(),
|
|
||||||
url=url,
|
|
||||||
params=params,
|
|
||||||
json=json_body,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
if response.status_code == 204:
|
|
||||||
return {"status": "success", "message": "No content"}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
return {
|
|
||||||
"error": True,
|
|
||||||
"status_code": e.response.status_code,
|
|
||||||
"message": str(e),
|
|
||||||
"detail": e.response.text[:500] if e.response.text else None,
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {"error": True, "message": str(e)}
|
|
||||||
|
|
||||||
async def health_check(self) -> bool:
|
|
||||||
"""Check if Gitea is accessible."""
|
|
||||||
try:
|
|
||||||
client = await self._get_client()
|
|
||||||
response = await client.get(f"{self.api_url}/version")
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Global client instance
|
|
||||||
client = GiteaClient(GITEA_URL, GITEA_TOKEN)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Curated API Reference
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
API_REFERENCE = """
|
|
||||||
# Gitea API Quick Reference
|
|
||||||
|
|
||||||
For complete API documentation, see: {base_url}/api/swagger
|
|
||||||
|
|
||||||
## Common Endpoints for `gitea_api_call` tool
|
|
||||||
|
|
||||||
### Repository Operations
|
|
||||||
- GET `/repos/search?q={{keyword}}&limit=10` - Search repositories
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}` - Get repository details
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}` - Create repository (body: name, description, private)
|
|
||||||
- PATCH `/repos/{{owner}}/{{repo}}` - Update repository
|
|
||||||
- DELETE `/repos/{{owner}}/{{repo}}` - Delete repository
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/branches` - List branches
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/commits?limit=20` - List commits
|
|
||||||
|
|
||||||
### Issues
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/issues?state=open` - List issues
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}/issues` - Create issue (body: title, body)
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/issues/{{index}}` - Get issue by number
|
|
||||||
- PATCH `/repos/{{owner}}/{{repo}}/issues/{{index}}` - Update issue
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}/issues/{{index}}/comments` - Add comment (body: body)
|
|
||||||
|
|
||||||
### Pull Requests
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/pulls?state=open` - List PRs
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}/pulls` - Create PR (body: title, head, base)
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/pulls/{{index}}` - Get PR by number
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}/pulls/{{index}}/merge` - Merge PR
|
|
||||||
|
|
||||||
### File Operations
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/contents/{{filepath}}?ref={{branch}}` - Get file content
|
|
||||||
- PUT `/repos/{{owner}}/{{repo}}/contents/{{filepath}}` - Create/update file
|
|
||||||
- DELETE `/repos/{{owner}}/{{repo}}/contents/{{filepath}}` - Delete file
|
|
||||||
|
|
||||||
### Releases & Tags
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/releases` - List releases
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/releases/latest` - Get latest release
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}/releases` - Create release
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/tags` - List tags
|
|
||||||
- POST `/repos/{{owner}}/{{repo}}/tags` - Create tag
|
|
||||||
|
|
||||||
### Actions/CI (Gitea Actions)
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/actions/runs` - List workflow runs
|
|
||||||
- GET `/repos/{{owner}}/{{repo}}/actions/jobs?status={{status}}` - List jobs
|
|
||||||
|
|
||||||
### User & Organizations
|
|
||||||
- GET `/user` - Get authenticated user
|
|
||||||
- GET `/users/{{username}}` - Get user by username
|
|
||||||
- GET `/user/repos?limit=50` - List authenticated user's repos
|
|
||||||
- GET `/orgs/{{org}}` - Get organization
|
|
||||||
- GET `/orgs/{{org}}/repos` - List organization repos
|
|
||||||
- GET `/orgs/{{org}}/members` - List organization members
|
|
||||||
|
|
||||||
### Pagination
|
|
||||||
Most list endpoints support `page` and `limit` query parameters.
|
|
||||||
Default limit is usually 20-50 items.
|
|
||||||
""".format(base_url=GITEA_URL)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.resource("gitea://api-reference")
|
|
||||||
def get_api_reference() -> str:
|
|
||||||
"""Returns the Gitea API quick reference for using the gitea_api_call tool."""
|
|
||||||
return API_REFERENCE
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MCP Tools - Curated Operations
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_my_user_info() -> str:
|
|
||||||
"""Get information about the authenticated user.
|
|
||||||
|
|
||||||
Returns the current user's profile including username, email, and permissions.
|
|
||||||
This is useful for determining the authenticated identity before other operations.
|
|
||||||
"""
|
|
||||||
result = await client.request("GET", "/user")
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def search_repos(
|
|
||||||
keyword: str,
|
|
||||||
limit: int = 20,
|
|
||||||
private: bool | None = None,
|
|
||||||
archived: bool | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Search for repositories by keyword.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keyword: Search term to find in repository names/descriptions
|
|
||||||
limit: Maximum number of results (default: 20, max: 50)
|
|
||||||
private: Filter by private status (optional)
|
|
||||||
archived: Filter by archived status (optional)
|
|
||||||
"""
|
|
||||||
params: dict[str, Any] = {"q": keyword, "limit": min(limit, 50)}
|
|
||||||
if private is not None:
|
|
||||||
params["private"] = private
|
|
||||||
if archived is not None:
|
|
||||||
params["archived"] = archived
|
|
||||||
|
|
||||||
result = await client.request("GET", "/repos/search", params=params)
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def list_my_repos(
|
|
||||||
limit: int = 50,
|
|
||||||
page: int = 1,
|
|
||||||
) -> str:
|
|
||||||
"""List repositories owned by or accessible to the authenticated user.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
limit: Maximum number of results per page (default: 50)
|
|
||||||
page: Page number for pagination (default: 1)
|
|
||||||
"""
|
|
||||||
params = {"limit": limit, "page": page}
|
|
||||||
result = await client.request("GET", "/user/repos", params=params)
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_repo(owner: str, repo: str) -> str:
|
|
||||||
"""Get detailed information about a specific repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: Repository owner (username or organization)
|
|
||||||
repo: Repository name
|
|
||||||
"""
|
|
||||||
result = await client.request("GET", f"/repos/{owner}/{repo}")
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def list_repo_issues(
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
state: str = "open",
|
|
||||||
limit: int = 30,
|
|
||||||
page: int = 1,
|
|
||||||
) -> str:
|
|
||||||
"""List issues for a repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: Repository owner (username or organization)
|
|
||||||
repo: Repository name
|
|
||||||
state: Issue state filter: 'open', 'closed', or 'all' (default: 'open')
|
|
||||||
limit: Maximum number of results (default: 30)
|
|
||||||
page: Page number for pagination (default: 1)
|
|
||||||
"""
|
|
||||||
params = {"state": state, "limit": limit, "page": page}
|
|
||||||
result = await client.request("GET", f"/repos/{owner}/{repo}/issues", params=params)
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def list_repo_commits(
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
sha: str | None = None,
|
|
||||||
limit: int = 30,
|
|
||||||
page: int = 1,
|
|
||||||
) -> str:
|
|
||||||
"""List commits for a repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner: Repository owner (username or organization)
|
|
||||||
repo: Repository name
|
|
||||||
sha: SHA or branch to start listing from (default: default branch)
|
|
||||||
limit: Maximum number of results (default: 30)
|
|
||||||
page: Page number for pagination (default: 1)
|
|
||||||
"""
|
|
||||||
params: dict[str, Any] = {"limit": limit, "page": page}
|
|
||||||
if sha:
|
|
||||||
params["sha"] = sha
|
|
||||||
|
|
||||||
result = await client.request("GET", f"/repos/{owner}/{repo}/commits", params=params)
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# API Pass-through Tool
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def gitea_api_call(
|
|
||||||
endpoint: str,
|
|
||||||
method: str = "GET",
|
|
||||||
params: str = "{}",
|
|
||||||
body: str = "{}",
|
|
||||||
) -> str:
|
|
||||||
"""Execute a raw API call to Gitea.
|
|
||||||
|
|
||||||
Use this for any operation not covered by the other tools.
|
|
||||||
Refer to the 'gitea://api-reference' resource for common endpoints,
|
|
||||||
or see the full API docs at {base_url}/api/swagger
|
|
||||||
|
|
||||||
Args:
|
|
||||||
endpoint: API endpoint path (e.g., '/repos/owner/repo/releases')
|
|
||||||
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
||||||
params: JSON string of query parameters (optional)
|
|
||||||
body: JSON string of request body for POST/PUT/PATCH (optional)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
gitea_api_call('/repos/myorg/myrepo/releases', 'POST',
|
|
||||||
body='{{"tag_name": "v1.0.0", "name": "Release 1.0"}}')
|
|
||||||
""".format(base_url=GITEA_URL)
|
|
||||||
try:
|
|
||||||
params_dict = json.loads(params) if params else {}
|
|
||||||
body_dict = json.loads(body) if body else {}
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
return json.dumps({"error": True, "message": f"Invalid JSON: {e}"})
|
|
||||||
|
|
||||||
result = await client.request(
|
|
||||||
method=method,
|
|
||||||
endpoint=endpoint,
|
|
||||||
params=params_dict if params_dict else None,
|
|
||||||
json_body=body_dict if body_dict else None,
|
|
||||||
)
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Starlette Wrapper for Health Checks
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
async def health_check(request):
|
|
||||||
"""Health check endpoint for Docker/Kubernetes."""
|
|
||||||
is_healthy = await client.health_check()
|
|
||||||
if is_healthy:
|
|
||||||
return JSONResponse({"status": "ok", "gitea_url": GITEA_URL})
|
|
||||||
return JSONResponse(
|
|
||||||
{"status": "unhealthy", "message": "Cannot connect to Gitea"},
|
|
||||||
status_code=503,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app):
|
|
||||||
"""Manage client lifecycle."""
|
|
||||||
yield
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> Starlette:
|
|
||||||
"""Create the Starlette application with health check and MCP."""
|
|
||||||
mcp_app = mcp.http_app()
|
|
||||||
|
|
||||||
# Add health check route directly to the MCP app
|
|
||||||
mcp_app.add_route("/health", health_check, methods=["GET"])
|
|
||||||
|
|
||||||
return mcp_app
|
|
||||||
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
port = int(os.getenv("PORT", "8000"))
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
||||||
Reference in New Issue
Block a user