Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a519798e69 | |||
| 12f0e872c1 | |||
| 9d41be6c4f | |||
| 8a314a8a5b | |||
| f9cefeecd8 | |||
| f5837d0569 | |||
| 943bc4a063 | |||
| a2eeab9719 | |||
| 842502ca5a | |||
| 3ac04e69a8 | |||
| 58d2feb54a | |||
| 4b4b5ee247 | |||
| 08128b9471 |
@@ -3,3 +3,4 @@ gitea-mcp
|
||||
gitea-mcp.exe
|
||||
*.log
|
||||
tmp
|
||||
gitea-mcp-new
|
||||
|
||||
@@ -1,287 +1,135 @@
|
||||
# Gitea MCP Server
|
||||
|
||||
[繁體中文](README.zh-tw.md) | [简体中文](README.zh-cn.md)
|
||||
שרת MCP (Model Context Protocol) לגיטאה — מאפשר ניהול מלא של ריפוזיטוריות, issues, pull requests, wikis, actions ועוד דרך Claude Code וכלי MCP אחרים.
|
||||
|
||||
**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.
|
||||
**תומך בחיבור לכמה שרתי Gitea במקביל.**
|
||||
|
||||
[](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)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Gitea MCP Server](#gitea-mcp-server)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What is Gitea?](#what-is-gitea)
|
||||
- [What is MCP?](#what-is-mcp)
|
||||
- [🚧 Installation](#-installation)
|
||||
- [Usage with Claude Code](#usage-with-claude-code)
|
||||
- [Usage with VS Code](#usage-with-vs-code)
|
||||
- [📥 Download the official binary release](#-download-the-official-binary-release)
|
||||
- [🔧 Build from Source](#-build-from-source)
|
||||
- [📁 Add to PATH](#-add-to-path)
|
||||
- [🚀 Usage](#-usage)
|
||||
- [✅ Available Tools](#-available-tools)
|
||||
- [🐛 Debugging](#-debugging)
|
||||
- [🛠 Troubleshooting](#-troubleshooting)
|
||||
|
||||
## What is Gitea?
|
||||
|
||||
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.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
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.
|
||||
|
||||
## 🚧 Installation
|
||||
|
||||
### Usage with Claude Code
|
||||
|
||||
This method uses `go run` and requires [Go](https://go.dev) to be installed.
|
||||
## התקנה מהירה
|
||||
|
||||
```bash
|
||||
claude mcp add --transport stdio --scope user gitea \
|
||||
--env GITEA_ACCESS_TOKEN=token \
|
||||
--env GITEA_HOST=https://gitea.com \
|
||||
-- go run gitea.com/gitea/gitea-mcp@latest -t stdio
|
||||
curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash
|
||||
```
|
||||
|
||||
### Usage with VS Code
|
||||
הסקריפט:
|
||||
- משכפל את הריפו ל-`~/.gitea-mcp`
|
||||
- בונה בינארי ומתקין ל-`$GOPATH/bin`
|
||||
- מגדיר MCP server גלובלי ב-`~/.claude/settings.json`
|
||||
- מאפשר הוספת שרתים נוספים באופן אינטראקטיבי
|
||||
- מוחק הגדרות כפולות מקבצי `.mcp.json` ברמת פרויקט
|
||||
|
||||
For quick installation, use one of the one-click install buttons at the top of this README.
|
||||
### דרישות מוקדמות
|
||||
|
||||
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)`.
|
||||
- [Go](https://go.dev) 1.24+
|
||||
- git
|
||||
- jq
|
||||
|
||||
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.
|
||||
### שדרוג
|
||||
|
||||
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"inputs": [
|
||||
{
|
||||
"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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 📥 Download the official binary release
|
||||
|
||||
You can download the official release from [official Gitea MCP binary releases](https://gitea.com/gitea/gitea-mcp/releases).
|
||||
|
||||
### 🔧 Build from Source
|
||||
|
||||
You can download the source code by cloning the repository using Git:
|
||||
אותה פקודה בדיוק — הסקריפט מזהה התקנה קיימת ומעדכן:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.com/gitea/gitea-mcp.git
|
||||
curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash
|
||||
```
|
||||
|
||||
Before building, make sure you have the following installed:
|
||||
## הגדרה ידנית
|
||||
|
||||
- make
|
||||
- Golang (Go 1.24 or later recommended)
|
||||
> **חשוב:** כדי שההגדרה תהיה **גלובלית** (זמינה בכל פרויקט), יש לכתוב אותה ב-`~/.claude/settings.json` תחת `mcpServers`.
|
||||
> אל תשתמשו ב-`.mcp.json` ברמת פרויקט — זה יגרום לכפילויות ויעבוד רק בתיקייה הספציפית.
|
||||
|
||||
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": {
|
||||
"gitea-dev": {
|
||||
"command": "gitea-mcp",
|
||||
"args": [
|
||||
"-t",
|
||||
"stdio",
|
||||
"--host",
|
||||
"https://gitea.com"
|
||||
// "--token", "<your personal access token>"
|
||||
],
|
||||
"args": ["-t", "stdio", "--host", "https://gitea.example.com"],
|
||||
"env": {
|
||||
// "GITEA_HOST": "https://gitea.com",
|
||||
// "GITEA_INSECURE": "true",
|
||||
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||
"GITEA_ACCESS_TOKEN": "<token>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **http mode**
|
||||
### כמה שרתים
|
||||
|
||||
הוסף את `GITEA_SERVERS` כ-JSON object עם שם → `{host, token, insecure}`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitea": {
|
||||
"url": "http://localhost:8080/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <your personal access token>"
|
||||
"gitea-dev": {
|
||||
"command": "gitea-mcp",
|
||||
"args": ["-t", "stdio", "--host", "https://gitea.example.com"],
|
||||
"env": {
|
||||
"GITEA_ACCESS_TOKEN": "<default-token>",
|
||||
"GITEA_SERVERS": "{\"prod\":{\"host\":\"https://gitea.prod.example.com\",\"token\":\"<prod-token>\"},\"staging\":{\"host\":\"https://staging.example.com\",\"token\":\"<staging-token>\",\"insecure\":true}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default log path**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||
כל כלי מקבל פרמטר `server` אופציונלי:
|
||||
|
||||
> [!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
|
||||
|
||||
> [!NOTE]
|
||||
> Many tools support `page` and `perPage` parameters for pagination. The maximum effective page size is determined by the Gitea server's `[api].MAX_RESPONSE_ITEMS` setting (default: **50**). Requesting a `perPage` value higher than this limit will be silently capped by the server.
|
||||
|
||||
Once everything is set up, try typing the following in your MCP-compatible chatbox:
|
||||
|
||||
```text
|
||||
list all my repositories
|
||||
```
|
||||
list_my_repos # ברירת מחדל
|
||||
list_my_repos(server: "prod") # שרת ספציפי
|
||||
list_servers # הצג שרתים מוגדרים
|
||||
```
|
||||
|
||||
## ✅ Available Tools
|
||||
## משתני סביבה
|
||||
|
||||
The Gitea MCP Server supports the following tools:
|
||||
| משתנה | תיאור |
|
||||
|---|---|
|
||||
| `GITEA_ACCESS_TOKEN` | טוקן גישה לשרת ברירת המחדל |
|
||||
| `GITEA_HOST` | כתובת שרת ברירת המחדל |
|
||||
| `GITEA_SERVERS` | JSON object של שרתים נוספים |
|
||||
| `GITEA_READONLY` | `true` להצגת כלי קריאה בלבד |
|
||||
| `GITEA_DEBUG` | `true` למצב debug |
|
||||
| `GITEA_INSECURE` | `true` לדלג על בדיקת TLS |
|
||||
| `MCP_MODE` | `stdio` (ברירת מחדל) או `http` |
|
||||
|
||||
| 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 |
|
||||
| get_pull_request_diff | Pull Request | Get a pull request diff |
|
||||
| list_repo_pull_requests | Pull Request | List all pull requests in a repository |
|
||||
| create_pull_request | Pull Request | Create a new pull request |
|
||||
| create_pull_request_reviewer | Pull Request | Add reviewers to a pull request |
|
||||
| delete_pull_request_reviewer | Pull Request | Remove reviewers from a pull request |
|
||||
| list_pull_request_reviews | Pull Request | List all reviews for a pull request |
|
||||
| get_pull_request_review | Pull Request | Get a specific review by ID |
|
||||
| list_pull_request_review_comments | Pull Request | List inline comments for a review |
|
||||
| create_pull_request_review | Pull Request | Create a review with optional inline comments |
|
||||
| submit_pull_request_review | Pull Request | Submit a pending review |
|
||||
| delete_pull_request_review | Pull Request | Delete a review |
|
||||
| dismiss_pull_request_review | Pull Request | Dismiss a review with optional message |
|
||||
| merge_pull_request | Pull Request | Merge a pull request |
|
||||
| search_users | User | Search for users |
|
||||
| search_org_teams | Organization | Search for teams in an organization |
|
||||
| list_org_labels | Organization | List labels defined at organization level |
|
||||
| create_org_label | Organization | Create a label in an organization |
|
||||
| edit_org_label | Organization | Edit a label in an organization |
|
||||
| delete_org_label | Organization | Delete a label in an organization |
|
||||
| search_repos | Repository | Search for repositories |
|
||||
| list_repo_action_secrets | Actions | List repository Actions secrets (metadata only) |
|
||||
| upsert_repo_action_secret | Actions | Create/update (upsert) a repository Actions secret |
|
||||
| delete_repo_action_secret | Actions | Delete a repository Actions secret |
|
||||
| list_org_action_secrets | Actions | List organization Actions secrets (metadata only) |
|
||||
| upsert_org_action_secret | Actions | Create/update (upsert) an organization Actions secret |
|
||||
| delete_org_action_secret | Actions | Delete an organization Actions secret |
|
||||
| list_repo_action_variables | Actions | List repository Actions variables |
|
||||
| get_repo_action_variable | Actions | Get a repository Actions variable |
|
||||
| create_repo_action_variable | Actions | Create a repository Actions variable |
|
||||
| update_repo_action_variable | Actions | Update a repository Actions variable |
|
||||
| delete_repo_action_variable | Actions | Delete a repository Actions variable |
|
||||
| list_org_action_variables | Actions | List organization Actions variables |
|
||||
| get_org_action_variable | Actions | Get an organization Actions variable |
|
||||
| create_org_action_variable | Actions | Create an organization Actions variable |
|
||||
| update_org_action_variable | Actions | Update an organization Actions variable |
|
||||
| delete_org_action_variable | Actions | Delete an organization Actions variable |
|
||||
| list_repo_action_workflows | Actions | List repository Actions workflows |
|
||||
| get_repo_action_workflow | Actions | Get a repository Actions workflow |
|
||||
| dispatch_repo_action_workflow | Actions | Trigger (dispatch) a repository Actions workflow |
|
||||
| list_repo_action_runs | Actions | List repository Actions runs |
|
||||
| get_repo_action_run | Actions | Get a repository Actions run |
|
||||
| cancel_repo_action_run | Actions | Cancel a repository Actions run |
|
||||
| rerun_repo_action_run | Actions | Rerun a repository Actions run |
|
||||
| list_repo_action_jobs | Actions | List repository Actions jobs |
|
||||
| list_repo_action_run_jobs | Actions | List Actions jobs for a run |
|
||||
| get_repo_action_job_log_preview | Actions | Get a job log preview (tail/limited) |
|
||||
| download_repo_action_job_log | Actions | Download a job log to a file |
|
||||
| get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server |
|
||||
| list_wiki_pages | Wiki | List all wiki pages in a repository |
|
||||
| get_wiki_page | Wiki | Get a wiki page content and metadata |
|
||||
| get_wiki_revisions | Wiki | Get revisions history of a wiki page |
|
||||
| create_wiki_page | Wiki | Create a new wiki page |
|
||||
| update_wiki_page | Wiki | Update an existing wiki page |
|
||||
| delete_wiki_page | Wiki | Delete a wiki page |
|
||||
## כלים זמינים
|
||||
|
||||
## 🐛 Debugging
|
||||
השרת מספק 60+ כלי MCP:
|
||||
|
||||
To enable debug mode, add the `-d` flag when running the Gitea MCP Server with http mode:
|
||||
| תחום | כלים |
|
||||
|---|---|
|
||||
| משתמש | `get_me`, `get_user_orgs` |
|
||||
| ריפוזיטוריות | `create_repo`, `fork_repo`, `list_my_repos`, `list_org_repos`, `get_repo`, `repo_write`, `list_forks` |
|
||||
| ענפים | `list_branches`, `create_branch`, `delete_branch`, `branch_protection_read/write` |
|
||||
| תגיות | `list_tags`, `get_tag`, `create_tag`, `delete_tag` |
|
||||
| קבצים | `get_file_contents`, `get_dir_contents`, `create_or_update_file`, `delete_file`, `get_repository_tree` |
|
||||
| קומיטים | `list_commits`, `get_commit`, `get_compare` |
|
||||
| Issues | `list_issues`, `issue_read`, `issue_write`, `search_issues` |
|
||||
| Pull Requests | `list_pull_requests`, `pull_request_read`, `pull_request_write`, `pull_request_review_write` |
|
||||
| Labels | `label_read`, `label_write` |
|
||||
| Milestones | `milestone_read`, `milestone_write` |
|
||||
| Releases | `list_releases`, `get_release`, `get_latest_release`, `create_release`, `delete_release` |
|
||||
| Wiki | `wiki_read`, `wiki_write` |
|
||||
| מעקב זמן | `timetracking_read`, `timetracking_write` |
|
||||
| Actions | `actions_run_read`, `actions_run_write`, `actions_config_read`, `actions_config_write` |
|
||||
| התראות | `notification_read`, `notification_write` |
|
||||
| ארגונים | `org_read`, `org_write` |
|
||||
| Webhooks | `webhook_read`, `webhook_write` |
|
||||
| Collaborators | `collaborator_read`, `collaborator_write` |
|
||||
| Deploy Keys | `deploy_key_read`, `deploy_key_write` |
|
||||
| Topics | `topic_read`, `topic_write` |
|
||||
| חיפוש | `search_users`, `search_repos`, `search_org_teams` |
|
||||
| שרת | `get_gitea_mcp_server_version`, `list_servers` |
|
||||
|
||||
```sh
|
||||
./gitea-mcp -t http [--port 8080] --token <your personal access token> -d
|
||||
## פיתוח
|
||||
|
||||
```bash
|
||||
make build # בנייה
|
||||
make install # בנייה + התקנה ל-GOPATH/bin
|
||||
make dev # hot reload (דורש air)
|
||||
make clean # ניקוי
|
||||
go test ./... # טסטים
|
||||
```
|
||||
|
||||
## 🛠 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!
|
||||
MIT
|
||||
|
||||
+13
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -57,6 +58,7 @@ func init() {
|
||||
fmt.Fprintf(w, " GITEA_HOST\tOverride Gitea host URL\n")
|
||||
fmt.Fprintf(w, " GITEA_INSECURE\tSet to 'true' to ignore TLS errors\n")
|
||||
fmt.Fprintf(w, " GITEA_READONLY\tSet to 'true' for read-only mode\n")
|
||||
fmt.Fprintf(w, " GITEA_SERVERS\tJSON object mapping server names to {host, token, insecure} configs\n")
|
||||
fmt.Fprintf(w, " MCP_MODE\tOverride transport mode\n")
|
||||
w.Flush()
|
||||
}
|
||||
@@ -91,6 +93,17 @@ func init() {
|
||||
if os.Getenv("GITEA_INSECURE") == "true" {
|
||||
flagPkg.Insecure = true
|
||||
}
|
||||
|
||||
// Parse multi-server configuration from GITEA_SERVERS env var (JSON object).
|
||||
// Example: {"dev":{"host":"https://gitea.dev.example.com","token":"xxx"},"prod":{...}}
|
||||
if serversJSON := os.Getenv("GITEA_SERVERS"); serversJSON != "" {
|
||||
var servers map[string]flagPkg.ServerConfig
|
||||
if err := json.Unmarshal([]byte(serversJSON), &servers); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to parse GITEA_SERVERS: %v\n", err)
|
||||
} else {
|
||||
flagPkg.Servers = servers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
+4
-3
@@ -6,11 +6,12 @@
|
||||
"-t", "stdio",
|
||||
"--host", "https://gitea.com",
|
||||
"--token", "<your personal access token>"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"GITEA_HOST": "https://gitea.com",
|
||||
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
||||
"GITEA_ACCESS_TOKEN": "<your personal access token>",
|
||||
"GITEA_SERVERS": "{\"dev\":{\"host\":\"https://gitea.dev.example.com\",\"token\":\"<dev-token>\"},\"prod\":{\"host\":\"https://gitea.prod.example.com\",\"token\":\"<prod-token>\"}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+487
@@ -0,0 +1,487 @@
|
||||
#!/bin/bash
|
||||
# install.sh - Install/upgrade gitea-mcp and configure global MCP server
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash
|
||||
# # or locally:
|
||||
# bash install.sh
|
||||
#
|
||||
# Options (via env vars):
|
||||
# GITEA_MCP_DIR=~/custom-dir # Custom install directory (default: ~/.gitea-mcp)
|
||||
# GITEA_MCP_BRANCH=dev # Custom branch (default: main)
|
||||
# GITEA_DEFAULT_HOST=https://... # Default Gitea host
|
||||
# GITEA_DEFAULT_TOKEN=xxx # Default Gitea token
|
||||
# AUTO_YES=1 # Skip all prompts
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp.git"
|
||||
INSTALL_DIR="${GITEA_MCP_DIR:-$HOME/.gitea-mcp}"
|
||||
BRANCH="${GITEA_MCP_BRANCH:-main}"
|
||||
AUTO_YES="${AUTO_YES:-1}"
|
||||
|
||||
MCP_NAME="gitea-dev"
|
||||
BINARY_NAME="gitea-mcp"
|
||||
GOBIN="${GOPATH:-$HOME/go}/bin"
|
||||
BINARY_PATH="$GOBIN/$BINARY_NAME"
|
||||
|
||||
CLAUDE_JSON="$HOME/.claude.json"
|
||||
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
info() { echo -e "${CYAN}>>>${NC} $*"; }
|
||||
ok() { echo -e "${GREEN} ✓${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW} !${NC} $*"; }
|
||||
error() { echo -e "${RED} ✗${NC} $*" >&2; }
|
||||
|
||||
ask_yes_no() {
|
||||
local prompt="$1"
|
||||
if [ "$AUTO_YES" = "1" ]; then return 0; fi
|
||||
if [ -e /dev/tty ]; then
|
||||
printf "%s [Y/n] " "$prompt"
|
||||
local answer=""
|
||||
read -r answer < /dev/tty 2>/dev/null || answer="y"
|
||||
case "$answer" in [nN]*) return 1 ;; *) return 0 ;; esac
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
ask_input() {
|
||||
local prompt="$1" var_name="$2" default="${3:-}"
|
||||
if [ -e /dev/tty ]; then
|
||||
if [ -n "$default" ]; then
|
||||
printf "%s [%s]: " "$prompt" "$default"
|
||||
else
|
||||
printf "%s: " "$prompt"
|
||||
fi
|
||||
local answer=""
|
||||
read -r answer < /dev/tty 2>/dev/null || answer=""
|
||||
if [ -z "$answer" ]; then answer="$default"; fi
|
||||
eval "$var_name=\$answer"
|
||||
else
|
||||
eval "$var_name=\$default"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Auto-install helpers ───────────────────────────────────────────────────
|
||||
|
||||
detect_pkg_manager() {
|
||||
if command -v apt-get &>/dev/null; then echo "apt"
|
||||
elif command -v dnf &>/dev/null; then echo "dnf"
|
||||
elif command -v yum &>/dev/null; then echo "yum"
|
||||
elif command -v pacman &>/dev/null; then echo "pacman"
|
||||
elif command -v apk &>/dev/null; then echo "apk"
|
||||
elif command -v brew &>/dev/null; then echo "brew"
|
||||
else echo ""; fi
|
||||
}
|
||||
|
||||
install_pkg() {
|
||||
local pkg="$1"
|
||||
local pm
|
||||
pm=$(detect_pkg_manager)
|
||||
|
||||
case "$pm" in
|
||||
apt) sudo apt-get update -qq && sudo apt-get install -y -qq "$pkg" ;;
|
||||
dnf) sudo dnf install -y -q "$pkg" ;;
|
||||
yum) sudo yum install -y -q "$pkg" ;;
|
||||
pacman) sudo pacman -S --noconfirm "$pkg" ;;
|
||||
apk) sudo apk add --quiet "$pkg" ;;
|
||||
brew) brew install "$pkg" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_go() {
|
||||
local GO_VERSION_TARGET="1.24.2"
|
||||
local ARCH
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
*) error "Unsupported architecture: $ARCH"; return 1 ;;
|
||||
esac
|
||||
local OS
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
local TARBALL="go${GO_VERSION_TARGET}.${OS}-${ARCH}.tar.gz"
|
||||
local URL="https://go.dev/dl/$TARBALL"
|
||||
|
||||
info "Downloading Go $GO_VERSION_TARGET for $OS/$ARCH ..."
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp)
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL "$URL" -o "$tmpfile"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -q "$URL" -O "$tmpfile"
|
||||
else
|
||||
error "Neither curl nor wget found — cannot download Go"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Installing Go to /usr/local/go ..."
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf "$tmpfile"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# Add to PATH for this session
|
||||
export PATH="/usr/local/go/bin:$PATH"
|
||||
|
||||
# Persist in shell profile if not already there
|
||||
local profile="$HOME/.profile"
|
||||
[ -f "$HOME/.bashrc" ] && profile="$HOME/.bashrc"
|
||||
[ -f "$HOME/.zshrc" ] && profile="$HOME/.zshrc"
|
||||
if ! grep -q '/usr/local/go/bin' "$profile" 2>/dev/null; then
|
||||
echo 'export PATH="/usr/local/go/bin:$PATH"' >> "$profile"
|
||||
info "Added Go to PATH in $profile"
|
||||
fi
|
||||
|
||||
ok "Go $GO_VERSION_TARGET installed"
|
||||
}
|
||||
|
||||
# ── Step 0: Prerequisites ──────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}=== Gitea MCP Server Installer ===${NC}"
|
||||
echo ""
|
||||
|
||||
info "Checking prerequisites..."
|
||||
|
||||
# --- git ---
|
||||
if ! command -v git &>/dev/null; then
|
||||
warn "git not found, installing..."
|
||||
if ! install_pkg git; then
|
||||
error "Failed to install git. Install manually and re-run."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
ok "git ($(git --version | cut -d' ' -f3))"
|
||||
|
||||
# --- jq ---
|
||||
if ! command -v jq &>/dev/null; then
|
||||
warn "jq not found, installing..."
|
||||
if ! install_pkg jq; then
|
||||
error "Failed to install jq. Install manually and re-run."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
ok "jq ($(jq --version 2>/dev/null))"
|
||||
|
||||
# --- Go ---
|
||||
if ! command -v go &>/dev/null; then
|
||||
warn "Go not found, installing..."
|
||||
if ! install_go; then
|
||||
error "Failed to install Go. Install manually from https://go.dev/dl/ and re-run."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
GO_VERSION=$(go version | grep -oP 'go\d+\.\d+')
|
||||
GO_MAJOR=$(echo "$GO_VERSION" | grep -oP '\d+' | head -1)
|
||||
GO_MINOR=$(echo "$GO_VERSION" | grep -oP '\d+' | tail -1)
|
||||
if [ "$GO_MAJOR" -lt 1 ] || { [ "$GO_MAJOR" -eq 1 ] && [ "$GO_MINOR" -lt 24 ]; }; then
|
||||
warn "Go $GO_VERSION is too old (need 1.24+), upgrading..."
|
||||
if ! install_go; then
|
||||
error "Failed to upgrade Go. Install manually from https://go.dev/dl/ and re-run."
|
||||
exit 1
|
||||
fi
|
||||
GO_VERSION=$(go version | grep -oP 'go\d+\.\d+')
|
||||
fi
|
||||
ok "go ($GO_VERSION)"
|
||||
|
||||
# ── Step 1: Clone or update ────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
info "Updating existing installation in $INSTALL_DIR ..."
|
||||
cd "$INSTALL_DIR"
|
||||
git fetch origin 2>&1 | tail -2
|
||||
git reset --hard "origin/$BRANCH" 2>&1 | tail -1
|
||||
ok "Updated to latest $BRANCH"
|
||||
elif [ -d "$INSTALL_DIR" ]; then
|
||||
# Directory exists but is not a git repo — remove and re-clone
|
||||
warn "$INSTALL_DIR exists but is not a git repo, re-cloning..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$INSTALL_DIR" 2>&1 | tail -2
|
||||
cd "$INSTALL_DIR"
|
||||
ok "Cloned"
|
||||
else
|
||||
info "Cloning to $INSTALL_DIR ..."
|
||||
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$INSTALL_DIR" 2>&1 | tail -2
|
||||
cd "$INSTALL_DIR"
|
||||
ok "Cloned"
|
||||
fi
|
||||
|
||||
# ── Step 2: Build ──────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
info "Building $BINARY_NAME ..."
|
||||
mkdir -p "$GOBIN"
|
||||
go build -o "$BINARY_PATH" . 2>&1
|
||||
VERSION=$("$BINARY_PATH" --version 2>/dev/null || echo "unknown")
|
||||
ok "Built $BINARY_PATH ($VERSION)"
|
||||
|
||||
# ── Step 3: Find existing MCP config ───────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
info "Checking existing MCP configuration..."
|
||||
|
||||
# Determine where the MCP entry currently lives
|
||||
MCP_CONFIG_FILE=""
|
||||
EXISTING_ENTRY=""
|
||||
|
||||
# Check ~/.claude.json first (preferred location for global MCP servers)
|
||||
if [ -f "$CLAUDE_JSON" ]; then
|
||||
EXISTING_ENTRY=$(jq -r --arg name "$MCP_NAME" '.mcpServers[$name] // empty' "$CLAUDE_JSON" 2>/dev/null || true)
|
||||
if [ -n "$EXISTING_ENTRY" ]; then
|
||||
MCP_CONFIG_FILE="$CLAUDE_JSON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check ~/.claude/settings.json (legacy location)
|
||||
if [ -z "$EXISTING_ENTRY" ] && [ -f "$CLAUDE_SETTINGS" ]; then
|
||||
EXISTING_ENTRY=$(jq -r --arg name "$MCP_NAME" '.mcpServers[$name] // empty' "$CLAUDE_SETTINGS" 2>/dev/null || true)
|
||||
if [ -n "$EXISTING_ENTRY" ]; then
|
||||
MCP_CONFIG_FILE="$CLAUDE_SETTINGS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for project-level .mcp.json files
|
||||
FOUND_PROJECT_MCP=()
|
||||
while IFS= read -r -d '' mcp_file; do
|
||||
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$mcp_file" &>/dev/null; then
|
||||
FOUND_PROJECT_MCP+=("$mcp_file")
|
||||
fi
|
||||
done < <(find "$HOME" -maxdepth 3 -name ".mcp.json" \
|
||||
-not -path "*/.claude/*" \
|
||||
-not -path "*/.gitea-mcp/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-print0 2>/dev/null || true)
|
||||
|
||||
# ── Step 4: Migrate / deduplicate ──────────────────────────────────────────
|
||||
|
||||
# If found in project-level files, migrate to global
|
||||
if [ ${#FOUND_PROJECT_MCP[@]} -gt 0 ]; then
|
||||
for mcp_file in "${FOUND_PROJECT_MCP[@]}"; do
|
||||
warn "Found '$MCP_NAME' in project-level config: $mcp_file"
|
||||
|
||||
if [ -z "$EXISTING_ENTRY" ]; then
|
||||
# No global config yet - migrate from project file
|
||||
if ask_yes_no " Migrate to global config ($CLAUDE_JSON)?"; then
|
||||
EXISTING_ENTRY=$(jq --arg name "$MCP_NAME" '.mcpServers[$name]' "$mcp_file")
|
||||
MCP_CONFIG_FILE="$CLAUDE_JSON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove from project-level regardless
|
||||
if ask_yes_no " Remove duplicate from $mcp_file?"; then
|
||||
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$mcp_file")
|
||||
echo "$tmp" > "$mcp_file"
|
||||
ok "Removed from $mcp_file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Step 5: Configure default server ───────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
|
||||
# Extract existing values if we have a config
|
||||
CURRENT_HOST=""
|
||||
CURRENT_TOKEN=""
|
||||
CURRENT_SERVERS=""
|
||||
|
||||
if [ -n "$EXISTING_ENTRY" ]; then
|
||||
CURRENT_HOST=$(echo "$EXISTING_ENTRY" | jq -r '
|
||||
(.args // []) | to_entries |
|
||||
map(select(.value == "--host")) | first |
|
||||
if . then (.key + 1) as $k | input | .[$k] else empty end
|
||||
' 2>/dev/null || true)
|
||||
# Simpler extraction
|
||||
CURRENT_HOST=$(echo "$EXISTING_ENTRY" | jq -r '
|
||||
.args as $a | ($a | index("--host")) as $i |
|
||||
if $i then $a[$i + 1] else "" end
|
||||
' 2>/dev/null || true)
|
||||
CURRENT_TOKEN=$(echo "$EXISTING_ENTRY" | jq -r '.env.GITEA_ACCESS_TOKEN // ""' 2>/dev/null || true)
|
||||
CURRENT_SERVERS=$(echo "$EXISTING_ENTRY" | jq -r '.env.GITEA_SERVERS // ""' 2>/dev/null || true)
|
||||
|
||||
ok "Existing config found"
|
||||
echo " Default: ${CURRENT_HOST:-not set}"
|
||||
if [ -n "$CURRENT_SERVERS" ] && [ "$CURRENT_SERVERS" != "" ]; then
|
||||
echo "$CURRENT_SERVERS" | jq -r 'to_entries[] | " \(.key): \(.value.host)"' 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine default host and token (env vars > existing config; no built-in token).
|
||||
# Per Infisical-only secrets policy, tokens MUST come from env vars at install time.
|
||||
# Source values from Infisical /gitea (per environment) before re-running this script.
|
||||
BUILTIN_HOST="https://gitea.dev.marcus-law.co.il"
|
||||
|
||||
DEFAULT_HOST="${GITEA_DEFAULT_HOST:-${CURRENT_HOST:-$BUILTIN_HOST}}"
|
||||
DEFAULT_TOKEN="${GITEA_DEFAULT_TOKEN:-${CURRENT_TOKEN:-}}"
|
||||
|
||||
if [[ -z "$DEFAULT_TOKEN" ]]; then
|
||||
err "No token resolved for default server. Set GITEA_DEFAULT_TOKEN env var (Infisical /gitea dev → API_TOKEN) and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "Default server: $DEFAULT_HOST"
|
||||
|
||||
# Built-in additional server hosts (no tokens — those come from env vars).
|
||||
# Set GITEA_PROD_TOKEN and GITEA_NAUTILUS_TOKEN env vars (Infisical /gitea prod|nautilus → API_TOKEN).
|
||||
BUILTIN_SERVERS_TEMPLATE='{"prod":{"host":"https://gitea.prod.marcus-law.co.il","token":"__PROD_TOKEN__","insecure":false},"nautilus":{"host":"https://gitea.nautilus.marcusgroup.org","token":"__NAUTILUS_TOKEN__","insecure":false}}'
|
||||
|
||||
if [[ -z "${GITEA_PROD_TOKEN:-}" || -z "${GITEA_NAUTILUS_TOKEN:-}" ]]; then
|
||||
err "GITEA_PROD_TOKEN and GITEA_NAUTILUS_TOKEN env vars are required for the additional-servers template."
|
||||
err "Source values from Infisical /gitea prod and /gitea nautilus, then re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILTIN_SERVERS="${BUILTIN_SERVERS_TEMPLATE//__PROD_TOKEN__/$GITEA_PROD_TOKEN}"
|
||||
BUILTIN_SERVERS="${BUILTIN_SERVERS//__NAUTILUS_TOKEN__/$GITEA_NAUTILUS_TOKEN}"
|
||||
|
||||
# ── Step 6: Add additional servers ─────────────────────────────────────────
|
||||
|
||||
SERVERS_JSON="${CURRENT_SERVERS:-$BUILTIN_SERVERS}"
|
||||
|
||||
while true; do
|
||||
echo ""
|
||||
if [ "$AUTO_YES" = "1" ]; then break; fi
|
||||
if ! ask_yes_no "Add an additional Gitea server?"; then
|
||||
break
|
||||
fi
|
||||
|
||||
srv_name="" srv_host="" srv_token="" srv_insecure="false"
|
||||
ask_input " Server name (e.g. prod, staging)" srv_name ""
|
||||
if [ -z "$srv_name" ]; then break; fi
|
||||
|
||||
# Check if already exists
|
||||
if [ -n "$SERVERS_JSON" ] && echo "$SERVERS_JSON" | jq -e --arg n "$srv_name" '.[$n]' &>/dev/null; then
|
||||
existing_host=$(echo "$SERVERS_JSON" | jq -r --arg n "$srv_name" '.[$n].host')
|
||||
warn "Server '$srv_name' already configured: $existing_host"
|
||||
if ! ask_yes_no " Overwrite?"; then continue; fi
|
||||
fi
|
||||
|
||||
ask_input " Host URL" srv_host ""
|
||||
ask_input " Access token" srv_token ""
|
||||
ask_input " Skip TLS verification? (y/n)" srv_insecure "n"
|
||||
[[ "$srv_insecure" =~ ^[Yy] ]] && srv_insecure="true" || srv_insecure="false"
|
||||
|
||||
if [ -z "$SERVERS_JSON" ] || [ "$SERVERS_JSON" = "" ]; then
|
||||
SERVERS_JSON="{}"
|
||||
fi
|
||||
SERVERS_JSON=$(echo "$SERVERS_JSON" | jq \
|
||||
--arg n "$srv_name" --arg h "$srv_host" --arg t "$srv_token" --argjson i "$srv_insecure" \
|
||||
'.[$n] = {host: $h, token: $t, insecure: $i}')
|
||||
|
||||
ok "Added server '$srv_name': $srv_host"
|
||||
done
|
||||
|
||||
# ── Step 7: Write global MCP config ───────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
info "Writing global MCP configuration..."
|
||||
|
||||
# Build the MCP entry
|
||||
if [ -n "$SERVERS_JSON" ] && [ "$SERVERS_JSON" != "{}" ] && [ "$SERVERS_JSON" != "" ]; then
|
||||
MCP_ENTRY=$(jq -n \
|
||||
--arg cmd "$BINARY_PATH" \
|
||||
--arg host "$DEFAULT_HOST" \
|
||||
--arg token "$DEFAULT_TOKEN" \
|
||||
--arg servers "$SERVERS_JSON" \
|
||||
'{
|
||||
command: $cmd,
|
||||
args: ["-t", "stdio", "--host", $host],
|
||||
env: {
|
||||
GITEA_ACCESS_TOKEN: $token,
|
||||
GITEA_SERVERS: $servers
|
||||
}
|
||||
}')
|
||||
else
|
||||
MCP_ENTRY=$(jq -n \
|
||||
--arg cmd "$BINARY_PATH" \
|
||||
--arg host "$DEFAULT_HOST" \
|
||||
--arg token "$DEFAULT_TOKEN" \
|
||||
'{
|
||||
command: $cmd,
|
||||
args: ["-t", "stdio", "--host", $host],
|
||||
env: {
|
||||
GITEA_ACCESS_TOKEN: $token
|
||||
}
|
||||
}')
|
||||
fi
|
||||
|
||||
# Write to ~/.claude.json (global MCP config location)
|
||||
if [ ! -f "$CLAUDE_JSON" ]; then
|
||||
echo '{}' > "$CLAUDE_JSON"
|
||||
fi
|
||||
|
||||
tmp=$(jq --arg name "$MCP_NAME" --argjson entry "$MCP_ENTRY" \
|
||||
'.mcpServers[$name] = $entry' "$CLAUDE_JSON")
|
||||
echo "$tmp" > "$CLAUDE_JSON"
|
||||
ok "Written to $CLAUDE_JSON"
|
||||
|
||||
# Clean up from settings.json if it exists there (legacy location)
|
||||
if [ -f "$CLAUDE_SETTINGS" ]; then
|
||||
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$CLAUDE_SETTINGS" &>/dev/null; then
|
||||
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$CLAUDE_SETTINGS")
|
||||
echo "$tmp" > "$CLAUDE_SETTINGS"
|
||||
ok "Removed legacy entry from $CLAUDE_SETTINGS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Step 8: Clean up duplicates in other config files ──────────────────────
|
||||
|
||||
echo ""
|
||||
info "Checking for duplicate configs..."
|
||||
|
||||
# Clean from project-level .mcp.json files
|
||||
while IFS= read -r -d '' mcp_file; do
|
||||
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$mcp_file" &>/dev/null; then
|
||||
warn "Duplicate found in $mcp_file"
|
||||
if ask_yes_no " Remove '$MCP_NAME' from $mcp_file?"; then
|
||||
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$mcp_file")
|
||||
echo "$tmp" > "$mcp_file"
|
||||
ok "Removed"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$HOME" -maxdepth 3 -name ".mcp.json" \
|
||||
-not -path "*/.claude/*" \
|
||||
-not -path "*/.gitea-mcp/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-print0 2>/dev/null || true)
|
||||
|
||||
# Clean from ~/.claude/settings.json if we wrote to ~/.claude.json
|
||||
if [ -f "$CLAUDE_SETTINGS" ]; then
|
||||
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$CLAUDE_SETTINGS" &>/dev/null; then
|
||||
warn "Duplicate found in $CLAUDE_SETTINGS"
|
||||
if ask_yes_no " Remove from $CLAUDE_SETTINGS? (already in $CLAUDE_JSON)"; then
|
||||
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$CLAUDE_SETTINGS")
|
||||
echo "$tmp" > "$CLAUDE_SETTINGS"
|
||||
ok "Removed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Done ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}=== Configured Servers ===${NC}"
|
||||
echo " default: $DEFAULT_HOST"
|
||||
if [ -n "$SERVERS_JSON" ] && [ "$SERVERS_JSON" != "{}" ] && [ "$SERVERS_JSON" != "" ]; then
|
||||
echo "$SERVERS_JSON" | jq -r 'to_entries[] | " \(.key): \(.value.host)\(if .value.insecure then " (insecure)" else "" end)"'
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}=== Installation complete! ===${NC}"
|
||||
echo ""
|
||||
echo " Binary: $BINARY_PATH ($VERSION)"
|
||||
echo " Config: $CLAUDE_JSON"
|
||||
echo " Source: $INSTALL_DIR"
|
||||
echo ""
|
||||
echo "Restart Claude Code to load the new MCP server."
|
||||
echo ""
|
||||
echo "To upgrade later:"
|
||||
echo " curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash"
|
||||
echo ""
|
||||
@@ -0,0 +1,215 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/params"
|
||||
"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 (
|
||||
NotificationReadToolName = "notification_read"
|
||||
NotificationWriteToolName = "notification_write"
|
||||
)
|
||||
|
||||
var (
|
||||
NotificationReadTool = mcp.NewTool(
|
||||
NotificationReadToolName,
|
||||
mcp.WithDescription("Get notifications. Use method 'list' to list notifications (optionally scoped to a repo), 'get' to get a single notification thread by ID."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
|
||||
mcp.WithString("owner", mcp.Description("repository owner (for 'list' to scope to a repo)")),
|
||||
mcp.WithString("repo", mcp.Description("repository name (for 'list' to scope to a repo)")),
|
||||
mcp.WithNumber("id", mcp.Description("notification thread ID (required for 'get')")),
|
||||
mcp.WithString("status", mcp.Description("filter by status (for 'list')"), mcp.Enum("unread", "read", "pinned")),
|
||||
mcp.WithString("subject_type", mcp.Description("filter by subject type (for 'list')"), mcp.Enum("Issue", "Pull", "Commit", "Repository")),
|
||||
mcp.WithString("since", mcp.Description("filter notifications updated after this ISO 8601 timestamp (for 'list')")),
|
||||
mcp.WithString("before", mcp.Description("filter notifications updated before this ISO 8601 timestamp (for 'list')")),
|
||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
NotificationWriteTool = mcp.NewTool(
|
||||
NotificationWriteToolName,
|
||||
mcp.WithDescription("Manage notifications. Use method 'mark_read' to mark a single notification as read, 'mark_all_read' to mark all notifications as read (optionally scoped to a repo)."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("mark_read", "mark_all_read")),
|
||||
mcp.WithNumber("id", mcp.Description("notification thread ID (required for 'mark_read')")),
|
||||
mcp.WithString("owner", mcp.Description("repository owner (for 'mark_all_read' to scope to a repo)")),
|
||||
mcp.WithString("repo", mcp.Description("repository name (for 'mark_all_read' to scope to a repo)")),
|
||||
mcp.WithString("last_read_at", mcp.Description("ISO 8601 timestamp, marks notifications before this time as read (for 'mark_all_read', defaults to now)")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: NotificationReadTool,
|
||||
Handler: notificationReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: NotificationWriteTool,
|
||||
Handler: notificationWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func notificationReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "list":
|
||||
return listNotificationsFn(ctx, req)
|
||||
case "get":
|
||||
return getNotificationFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func notificationWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "mark_read":
|
||||
return markNotificationReadFn(ctx, req)
|
||||
case "mark_all_read":
|
||||
return markAllNotificationsReadFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func listNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listNotificationsFn")
|
||||
args := req.GetArguments()
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
opt := gitea_sdk.ListNotificationOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
}
|
||||
if status, ok := args["status"].(string); ok {
|
||||
opt.Status = []gitea_sdk.NotifyStatus{gitea_sdk.NotifyStatus(status)}
|
||||
}
|
||||
if subjectType, ok := args["subject_type"].(string); ok {
|
||||
opt.SubjectTypes = []gitea_sdk.NotifySubjectType{gitea_sdk.NotifySubjectType(subjectType)}
|
||||
}
|
||||
if t := params.GetOptionalTime(args, "since"); t != nil {
|
||||
opt.Since = *t
|
||||
}
|
||||
if t := params.GetOptionalTime(args, "before"); t != nil {
|
||||
opt.Before = *t
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
owner := params.GetOptionalString(args, "owner", "")
|
||||
repo := params.GetOptionalString(args, "repo", "")
|
||||
if owner != "" && repo != "" {
|
||||
threads, _, err := client.ListRepoNotifications(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list %v/%v/notifications err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimThreads(threads))
|
||||
}
|
||||
|
||||
threads, _, err := client.ListNotifications(opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list notifications err: %v", err))
|
||||
}
|
||||
return to.TextResult(slimThreads(threads))
|
||||
}
|
||||
|
||||
func getNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getNotificationFn")
|
||||
id, err := params.GetIndex(req.GetArguments(), "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
thread, _, err := client.GetNotification(id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get notification/%v err: %v", id, err))
|
||||
}
|
||||
return to.TextResult(slimThread(thread))
|
||||
}
|
||||
|
||||
func markNotificationReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called markNotificationReadFn")
|
||||
id, err := params.GetIndex(req.GetArguments(), "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
thread, _, err := client.ReadNotification(id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("mark notification/%v read err: %v", id, err))
|
||||
}
|
||||
if thread != nil {
|
||||
return to.TextResult(slimThread(thread))
|
||||
}
|
||||
return to.TextResult("Notification marked as read")
|
||||
}
|
||||
|
||||
func markAllNotificationsReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called markAllNotificationsReadFn")
|
||||
args := req.GetArguments()
|
||||
lastReadAt := time.Now()
|
||||
if t := params.GetOptionalTime(args, "last_read_at"); t != nil {
|
||||
lastReadAt = *t
|
||||
}
|
||||
opt := gitea_sdk.MarkNotificationOptions{
|
||||
LastReadAt: lastReadAt,
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
owner := params.GetOptionalString(args, "owner", "")
|
||||
repo := params.GetOptionalString(args, "repo", "")
|
||||
if owner != "" && repo != "" {
|
||||
threads, _, err := client.ReadRepoNotifications(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("mark %v/%v/notifications read err: %v", owner, repo, err))
|
||||
}
|
||||
if threads != nil {
|
||||
return to.TextResult(slimThreads(threads))
|
||||
}
|
||||
return to.TextResult("All repository notifications marked as read")
|
||||
}
|
||||
|
||||
threads, _, err := client.ReadNotifications(opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("mark all notifications read err: %v", err))
|
||||
}
|
||||
if threads != nil {
|
||||
return to.TextResult(slimThreads(threads))
|
||||
}
|
||||
return to.TextResult("All notifications marked as read")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
func slimThread(t *gitea_sdk.NotificationThread) map[string]any {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
m := map[string]any{
|
||||
"id": t.ID,
|
||||
"unread": t.Unread,
|
||||
"updated_at": t.UpdatedAt,
|
||||
}
|
||||
if t.Pinned {
|
||||
m["pinned"] = true
|
||||
}
|
||||
if t.Repository != nil {
|
||||
m["repository"] = t.Repository.FullName
|
||||
}
|
||||
if t.Subject != nil {
|
||||
subject := map[string]any{
|
||||
"title": t.Subject.Title,
|
||||
"type": t.Subject.Type,
|
||||
"state": t.Subject.State,
|
||||
}
|
||||
if t.Subject.HTMLURL != "" {
|
||||
subject["html_url"] = t.Subject.HTMLURL
|
||||
}
|
||||
if t.Subject.LatestCommentHTMLURL != "" {
|
||||
subject["latest_comment_html_url"] = t.Subject.LatestCommentHTMLURL
|
||||
}
|
||||
m["subject"] = subject
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func slimThreads(threads []*gitea_sdk.NotificationThread) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(threads))
|
||||
for _, t := range threads {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
m := map[string]any{
|
||||
"id": t.ID,
|
||||
"unread": t.Unread,
|
||||
"updated_at": t.UpdatedAt,
|
||||
}
|
||||
if t.Pinned {
|
||||
m["pinned"] = true
|
||||
}
|
||||
if t.Repository != nil {
|
||||
m["repository"] = t.Repository.FullName
|
||||
}
|
||||
if t.Subject != nil {
|
||||
m["subject"] = map[string]any{
|
||||
"title": t.Subject.Title,
|
||||
"type": t.Subject.Type,
|
||||
"state": t.Subject.State,
|
||||
}
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
+97
-30
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
"gitea.com/gitea/gitea-mcp/operation/issue"
|
||||
"gitea.com/gitea/gitea-mcp/operation/label"
|
||||
"gitea.com/gitea/gitea-mcp/operation/milestone"
|
||||
"gitea.com/gitea/gitea-mcp/operation/notification"
|
||||
"gitea.com/gitea/gitea-mcp/operation/org"
|
||||
"gitea.com/gitea/gitea-mcp/operation/pull"
|
||||
"gitea.com/gitea/gitea-mcp/operation/repo"
|
||||
"gitea.com/gitea/gitea-mcp/operation/search"
|
||||
@@ -26,45 +29,109 @@ import (
|
||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var mcpServer *server.MCPServer
|
||||
|
||||
// serverNames returns a sorted list of configured server names for use in
|
||||
// enum validation and the list_servers tool.
|
||||
func serverNames() []string {
|
||||
names := make([]string, 0, len(flag.Servers))
|
||||
for name := range flag.Servers {
|
||||
names = append(names, name)
|
||||
}
|
||||
// Deterministic ordering for tool schemas.
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// injectServerParam adds an optional "server" parameter to a tool and wraps
|
||||
// its handler so the chosen server's host/token/insecure are injected into
|
||||
// the context before the original handler runs.
|
||||
func injectServerParam(tools []server.ServerTool) []server.ServerTool {
|
||||
names := serverNames()
|
||||
for i, st := range tools {
|
||||
// Add "server" to the JSON Schema properties.
|
||||
if st.Tool.InputSchema.Properties == nil {
|
||||
st.Tool.InputSchema.Properties = make(map[string]any)
|
||||
}
|
||||
st.Tool.InputSchema.Properties["server"] = map[string]any{
|
||||
"type": "string",
|
||||
"description": "Target Gitea server name. Available: " + strings.Join(names, ", ") + ". Omit to use the default server.",
|
||||
"enum": names,
|
||||
}
|
||||
tools[i].Tool = st.Tool
|
||||
|
||||
// Wrap the handler to resolve server config → context values.
|
||||
origHandler := st.Handler
|
||||
tools[i].Handler = func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if serverName, ok := req.GetArguments()["server"].(string); ok && serverName != "" {
|
||||
cfg, exists := flag.Servers[serverName]
|
||||
if !exists {
|
||||
return mcp.NewToolResultText("unknown server: " + serverName + ". Available: " + strings.Join(serverNames(), ", ")), nil
|
||||
}
|
||||
ctx = context.WithValue(ctx, mcpContext.HostContextKey, cfg.Host)
|
||||
ctx = context.WithValue(ctx, mcpContext.TokenContextKey, cfg.Token)
|
||||
ctx = context.WithValue(ctx, mcpContext.InsecureContextKey, cfg.Insecure)
|
||||
}
|
||||
return origHandler(ctx, req)
|
||||
}
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// listServersFn handles the "list_servers" tool — returns the configured servers.
|
||||
func listServersFn(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if len(flag.Servers) == 0 {
|
||||
defaultInfo := fmt.Sprintf("default: %s (no additional servers configured)", flag.Host)
|
||||
return mcp.NewToolResultText(defaultInfo), nil
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("default: %s\n", flag.Host))
|
||||
for _, name := range serverNames() {
|
||||
cfg := flag.Servers[name]
|
||||
b.WriteString(fmt.Sprintf("%s: %s", name, cfg.Host))
|
||||
if cfg.Insecure {
|
||||
b.WriteString(" (insecure)")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return mcp.NewToolResultText(b.String()), nil
|
||||
}
|
||||
|
||||
func RegisterTool(s *server.MCPServer) {
|
||||
// User Tool
|
||||
s.AddTools(user.Tool.Tools()...)
|
||||
// Collect all tools from every domain package.
|
||||
allTools := make([]server.ServerTool, 0, 200)
|
||||
allTools = append(allTools, user.Tool.Tools()...)
|
||||
allTools = append(allTools, actions.Tool.Tools()...)
|
||||
allTools = append(allTools, repo.Tool.Tools()...)
|
||||
allTools = append(allTools, notification.Tool.Tools()...)
|
||||
allTools = append(allTools, issue.Tool.Tools()...)
|
||||
allTools = append(allTools, label.Tool.Tools()...)
|
||||
allTools = append(allTools, milestone.Tool.Tools()...)
|
||||
allTools = append(allTools, pull.Tool.Tools()...)
|
||||
allTools = append(allTools, search.Tool.Tools()...)
|
||||
allTools = append(allTools, version.Tool.Tools()...)
|
||||
allTools = append(allTools, wiki.Tool.Tools()...)
|
||||
allTools = append(allTools, timetracking.Tool.Tools()...)
|
||||
allTools = append(allTools, org.Tool.Tools()...)
|
||||
|
||||
// Actions Tool
|
||||
s.AddTools(actions.Tool.Tools()...)
|
||||
// If multiple servers are configured, inject the "server" parameter into every tool.
|
||||
if len(flag.Servers) > 0 {
|
||||
allTools = injectServerParam(allTools)
|
||||
|
||||
// Repo Tool
|
||||
s.AddTools(repo.Tool.Tools()...)
|
||||
|
||||
// Issue Tool
|
||||
s.AddTools(issue.Tool.Tools()...)
|
||||
|
||||
// Label Tool
|
||||
s.AddTools(label.Tool.Tools()...)
|
||||
|
||||
// Milestone Tool
|
||||
s.AddTools(milestone.Tool.Tools()...)
|
||||
|
||||
// Pull Tool
|
||||
s.AddTools(pull.Tool.Tools()...)
|
||||
|
||||
// Search Tool
|
||||
s.AddTools(search.Tool.Tools()...)
|
||||
|
||||
// Version Tool
|
||||
s.AddTools(version.Tool.Tools()...)
|
||||
|
||||
// Wiki Tool
|
||||
s.AddTools(wiki.Tool.Tools()...)
|
||||
|
||||
// Time Tracking Tool
|
||||
s.AddTools(timetracking.Tool.Tools()...)
|
||||
// Add a list_servers meta-tool so callers can discover available servers.
|
||||
allTools = append(allTools, server.ServerTool{
|
||||
Tool: mcp.NewTool("list_servers",
|
||||
mcp.WithDescription("List all configured Gitea server connections"),
|
||||
),
|
||||
Handler: listServersFn,
|
||||
})
|
||||
}
|
||||
|
||||
s.AddTools(allTools...)
|
||||
s.DeleteTools("")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/params"
|
||||
"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 (
|
||||
OrgReadToolName = "org_read"
|
||||
OrgWriteToolName = "org_write"
|
||||
)
|
||||
|
||||
var (
|
||||
OrgReadTool = mcp.NewTool(
|
||||
OrgReadToolName,
|
||||
mcp.WithDescription("Read organization and team information. Methods: 'get' org details, 'list_teams', 'get_team', 'list_team_members', 'list_team_repos'."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "list_teams", "get_team", "list_team_members", "list_team_repos")),
|
||||
mcp.WithString("org", mcp.Description("organization name (required for 'get', 'list_teams')")),
|
||||
mcp.WithNumber("team_id", mcp.Description("team ID (required for 'get_team', 'list_team_members', 'list_team_repos')")),
|
||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
OrgWriteTool = mcp.NewTool(
|
||||
OrgWriteToolName,
|
||||
mcp.WithDescription("Manage organization teams and membership. Methods: 'create_team', 'delete_team', 'add_team_member', 'remove_team_member', 'add_team_repo', 'remove_team_repo'."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create_team", "delete_team", "add_team_member", "remove_team_member", "add_team_repo", "remove_team_repo")),
|
||||
mcp.WithString("org", mcp.Description("organization name (required for 'create_team', 'add_team_repo', 'remove_team_repo')")),
|
||||
mcp.WithNumber("team_id", mcp.Description("team ID (required for 'delete_team', 'add_team_member', 'remove_team_member', 'add_team_repo', 'remove_team_repo')")),
|
||||
mcp.WithString("username", mcp.Description("username (required for 'add_team_member', 'remove_team_member')")),
|
||||
mcp.WithString("repo", mcp.Description("repository name (required for 'add_team_repo', 'remove_team_repo')")),
|
||||
mcp.WithString("name", mcp.Description("team name (required for 'create_team')")),
|
||||
mcp.WithString("description", mcp.Description("team description (for 'create_team')")),
|
||||
mcp.WithString("permission", mcp.Description("team permission level (for 'create_team')"), mcp.Enum("read", "write", "admin")),
|
||||
mcp.WithArray("units", mcp.Description("team units (for 'create_team'), e.g. repo.code, repo.issues, repo.pulls"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithBoolean("includes_all_repositories", mcp.Description("include all org repos (for 'create_team')")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: OrgReadTool,
|
||||
Handler: orgReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: OrgWriteTool,
|
||||
Handler: orgWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func orgReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "get":
|
||||
return getOrgFn(ctx, req)
|
||||
case "list_teams":
|
||||
return listTeamsFn(ctx, req)
|
||||
case "get_team":
|
||||
return getTeamFn(ctx, req)
|
||||
case "list_team_members":
|
||||
return listTeamMembersFn(ctx, req)
|
||||
case "list_team_repos":
|
||||
return listTeamReposFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func getOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getOrgFn")
|
||||
args := req.GetArguments()
|
||||
orgName, err := params.GetString(args, "org")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
o, _, err := client.GetOrg(orgName)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get org %v err: %v", orgName, err))
|
||||
}
|
||||
return to.TextResult(slimOrg(o))
|
||||
}
|
||||
|
||||
func listTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listTeamsFn")
|
||||
args := req.GetArguments()
|
||||
orgName, err := params.GetString(args, "org")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
teams, _, err := client.ListOrgTeams(orgName, gitea_sdk.ListTeamsOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list teams for org %v err: %v", orgName, err))
|
||||
}
|
||||
return to.TextResult(slimTeams(teams))
|
||||
}
|
||||
|
||||
func getTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getTeamFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
team, _, err := client.GetTeam(teamID)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get team %v err: %v", teamID, err))
|
||||
}
|
||||
return to.TextResult(slimTeam(team))
|
||||
}
|
||||
|
||||
func listTeamMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listTeamMembersFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
members, _, err := client.ListTeamMembers(teamID, gitea_sdk.ListTeamMembersOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list team %v members err: %v", teamID, err))
|
||||
}
|
||||
return to.TextResult(slimUsers(members))
|
||||
}
|
||||
|
||||
func listTeamReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listTeamReposFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
repos, _, err := client.ListTeamRepositories(teamID, gitea_sdk.ListTeamRepositoriesOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list team %v repos err: %v", teamID, err))
|
||||
}
|
||||
return to.TextResult(slimRepos(repos))
|
||||
}
|
||||
|
||||
func orgWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "create_team":
|
||||
return createTeamFn(ctx, req)
|
||||
case "delete_team":
|
||||
return deleteTeamFn(ctx, req)
|
||||
case "add_team_member":
|
||||
return addTeamMemberFn(ctx, req)
|
||||
case "remove_team_member":
|
||||
return removeTeamMemberFn(ctx, req)
|
||||
case "add_team_repo":
|
||||
return addTeamRepoFn(ctx, req)
|
||||
case "remove_team_repo":
|
||||
return removeTeamRepoFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func createTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called createTeamFn")
|
||||
args := req.GetArguments()
|
||||
orgName, err := params.GetString(args, "org")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
name, err := params.GetString(args, "name")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
opt := gitea_sdk.CreateTeamOption{
|
||||
Name: name,
|
||||
}
|
||||
if v, ok := args["description"].(string); ok {
|
||||
opt.Description = v
|
||||
}
|
||||
if v, ok := args["permission"].(string); ok {
|
||||
switch v {
|
||||
case "read":
|
||||
opt.Permission = gitea_sdk.AccessModeRead
|
||||
case "write":
|
||||
opt.Permission = gitea_sdk.AccessModeWrite
|
||||
case "admin":
|
||||
opt.Permission = gitea_sdk.AccessModeAdmin
|
||||
}
|
||||
}
|
||||
if unitStrings := params.GetStringSlice(args, "units"); unitStrings != nil {
|
||||
units := make([]gitea_sdk.RepoUnitType, 0, len(unitStrings))
|
||||
for _, u := range unitStrings {
|
||||
units = append(units, gitea_sdk.RepoUnitType(u))
|
||||
}
|
||||
opt.Units = units
|
||||
}
|
||||
if v, ok := args["includes_all_repositories"].(bool); ok {
|
||||
opt.IncludesAllRepositories = v
|
||||
}
|
||||
|
||||
team, _, err := client.CreateTeam(orgName, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create team in org %v err: %v", orgName, err))
|
||||
}
|
||||
return to.TextResult(slimTeam(team))
|
||||
}
|
||||
|
||||
func deleteTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteTeamFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteTeam(teamID)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete team %v err: %v", teamID, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Team %d deleted", teamID)), nil
|
||||
}
|
||||
|
||||
func addTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called addTeamMemberFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
username, err := params.GetString(args, "username")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.AddTeamMember(teamID, username)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("add %v to team %v err: %v", username, teamID, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("User %s added to team %d", username, teamID)), nil
|
||||
}
|
||||
|
||||
func removeTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called removeTeamMemberFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
username, err := params.GetString(args, "username")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.RemoveTeamMember(teamID, username)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("remove %v from team %v err: %v", username, teamID, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("User %s removed from team %d", username, teamID)), nil
|
||||
}
|
||||
|
||||
func addTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called addTeamRepoFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
orgName, err := params.GetString(args, "org")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repoName, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.AddTeamRepository(teamID, orgName, repoName)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("add repo %v/%v to team %v err: %v", orgName, repoName, teamID, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Repo %s/%s added to team %d", orgName, repoName, teamID)), nil
|
||||
}
|
||||
|
||||
func removeTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called removeTeamRepoFn")
|
||||
args := req.GetArguments()
|
||||
teamID, err := params.GetIndex(args, "team_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
orgName, err := params.GetString(args, "org")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repoName, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.RemoveTeamRepository(teamID, orgName, repoName)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("remove repo %v/%v from team %v err: %v", orgName, repoName, teamID, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Repo %s/%s removed from team %d", orgName, repoName, teamID)), nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
func slimOrg(o *gitea_sdk.Organization) map[string]any {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": o.ID,
|
||||
"username": o.UserName,
|
||||
"full_name": o.FullName,
|
||||
"description": o.Description,
|
||||
"website": o.Website,
|
||||
"location": o.Location,
|
||||
"visibility": o.Visibility,
|
||||
"avatar_url": o.AvatarURL,
|
||||
}
|
||||
}
|
||||
|
||||
func slimTeam(t *gitea_sdk.Team) map[string]any {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": t.ID,
|
||||
"name": t.Name,
|
||||
"description": t.Description,
|
||||
"permission": t.Permission,
|
||||
"units": t.Units,
|
||||
"includes_all_repositories": t.IncludesAllRepositories,
|
||||
"can_create_org_repo": t.CanCreateOrgRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func slimTeams(teams []*gitea_sdk.Team) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(teams))
|
||||
for _, t := range teams {
|
||||
out = append(out, slimTeam(t))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimUser(u *gitea_sdk.User) map[string]any {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": u.ID,
|
||||
"login": u.UserName,
|
||||
"full_name": u.FullName,
|
||||
"email": u.Email,
|
||||
"avatar_url": u.AvatarURL,
|
||||
}
|
||||
}
|
||||
|
||||
func slimUsers(users []*gitea_sdk.User) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(users))
|
||||
for _, u := range users {
|
||||
out = append(out, slimUser(u))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimRepos(repos []*gitea_sdk.Repository) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(repos))
|
||||
for _, r := range repos {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
m := map[string]any{
|
||||
"id": r.ID,
|
||||
"full_name": r.FullName,
|
||||
"html_url": r.HTMLURL,
|
||||
"private": r.Private,
|
||||
}
|
||||
if r.Owner != nil {
|
||||
m["owner"] = r.Owner.UserName
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
+76
-2
@@ -40,8 +40,8 @@ var (
|
||||
|
||||
PullRequestReadTool = mcp.NewTool(
|
||||
PullRequestReadToolName,
|
||||
mcp.WithDescription("Get pull request information. Use method 'get' for PR details, 'get_diff' for diff, 'get_reviews'/'get_review'/'get_review_comments' for review data."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments")),
|
||||
mcp.WithDescription("Get pull request information. Use method 'get' for PR details, 'get_diff' for diff, 'get_reviews'/'get_review'/'get_review_comments' for review data, 'list_files' for changed files, 'is_merged' to check merge status."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments", "list_files", "is_merged")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||
@@ -140,11 +140,85 @@ func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
||||
return getPullRequestReviewFn(ctx, req)
|
||||
case "get_review_comments":
|
||||
return listPullRequestReviewCommentsFn(ctx, req)
|
||||
case "list_files":
|
||||
return listPullRequestFilesFn(ctx, req)
|
||||
case "is_merged":
|
||||
return isPullRequestMergedFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func listPullRequestFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listPullRequestFilesFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repoName, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "index")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
files, _, err := client.ListPullRequestFiles(owner, repoName, index, gitea_sdk.ListPullRequestFilesOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list PR files %v/%v#%v err: %v", owner, repoName, index, err))
|
||||
}
|
||||
|
||||
out := make([]map[string]any, 0, len(files))
|
||||
for _, f := range files {
|
||||
out = append(out, map[string]any{
|
||||
"filename": f.Filename,
|
||||
"status": f.Status,
|
||||
"additions": f.Additions,
|
||||
"deletions": f.Deletions,
|
||||
"changes": f.Changes,
|
||||
})
|
||||
}
|
||||
return to.TextResult(out)
|
||||
}
|
||||
|
||||
func isPullRequestMergedFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called isPullRequestMergedFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repoName, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
index, err := params.GetIndex(args, "index")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
merged, _, err := client.IsPullRequestMerged(owner, repoName, index)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("check PR merged %v/%v#%v err: %v", owner, repoName, index, err))
|
||||
}
|
||||
return to.TextResult(map[string]any{"merged": merged})
|
||||
}
|
||||
|
||||
func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
method, err := params.GetString(req.GetArguments(), "method")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
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/params"
|
||||
"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 (
|
||||
BranchProtectionReadToolName = "branch_protection_read"
|
||||
BranchProtectionWriteToolName = "branch_protection_write"
|
||||
)
|
||||
|
||||
var (
|
||||
BranchProtectionReadTool = mcp.NewTool(
|
||||
BranchProtectionReadToolName,
|
||||
mcp.WithDescription("Read branch protection rules. Use method 'list' to list all rules, 'get' to get a specific rule."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("name", mcp.Description("branch protection rule name (required for 'get')")),
|
||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
BranchProtectionWriteTool = mcp.NewTool(
|
||||
BranchProtectionWriteToolName,
|
||||
mcp.WithDescription("Create, edit, or delete branch protection rules."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "edit", "delete")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("branch_name", mcp.Description("branch name or pattern (required for 'create')")),
|
||||
mcp.WithString("rule_name", mcp.Description("rule name (required for 'edit', 'delete')")),
|
||||
mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")),
|
||||
mcp.WithArray("push_whitelist_usernames", mcp.Description("usernames allowed to push"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithArray("push_whitelist_teams", mcp.Description("team names allowed to push"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")),
|
||||
mcp.WithArray("merge_whitelist_usernames", mcp.Description("usernames allowed to merge"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithArray("merge_whitelist_teams", mcp.Description("team names allowed to merge"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")),
|
||||
mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")),
|
||||
mcp.WithBoolean("block_on_outdated_branch", mcp.Description("block merge on outdated branch")),
|
||||
mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")),
|
||||
mcp.WithBoolean("require_signed_commits", mcp.Description("require signed commits")),
|
||||
mcp.WithString("protected_file_patterns", mcp.Description("protected file patterns (semicolon separated)")),
|
||||
mcp.WithString("unprotected_file_patterns", mcp.Description("unprotected file patterns (semicolon separated)")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: BranchProtectionReadTool,
|
||||
Handler: branchProtectionReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: BranchProtectionWriteTool,
|
||||
Handler: branchProtectionWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func branchProtectionReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "list":
|
||||
return listBranchProtectionsFn(ctx, req)
|
||||
case "get":
|
||||
return getBranchProtectionFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func listBranchProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listBranchProtectionsFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
bps, _, err := client.ListBranchProtections(owner, repo, gitea_sdk.ListBranchProtectionsOptions{})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list branch protections %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimBranchProtections(bps))
|
||||
}
|
||||
|
||||
func getBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getBranchProtectionFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
name, err := params.GetString(args, "name")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
bp, _, err := client.GetBranchProtection(owner, repo, name)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get branch protection %v in %v/%v err: %v", name, owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimBranchProtection(bp))
|
||||
}
|
||||
|
||||
func branchProtectionWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "create":
|
||||
return createBranchProtectionFn(ctx, req)
|
||||
case "edit":
|
||||
return editBranchProtectionFn(ctx, req)
|
||||
case "delete":
|
||||
return deleteBranchProtectionFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func createBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called createBranchProtectionFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
branchName, err := params.GetString(args, "branch_name")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
opt := gitea_sdk.CreateBranchProtectionOption{
|
||||
BranchName: branchName,
|
||||
}
|
||||
if v, ok := args["enable_push"].(bool); ok {
|
||||
opt.EnablePush = v
|
||||
}
|
||||
opt.PushWhitelistUsernames = params.GetStringSlice(args, "push_whitelist_usernames")
|
||||
opt.PushWhitelistTeams = params.GetStringSlice(args, "push_whitelist_teams")
|
||||
if v, ok := args["enable_merge_whitelist"].(bool); ok {
|
||||
opt.EnableMergeWhitelist = v
|
||||
}
|
||||
opt.MergeWhitelistUsernames = params.GetStringSlice(args, "merge_whitelist_usernames")
|
||||
opt.MergeWhitelistTeams = params.GetStringSlice(args, "merge_whitelist_teams")
|
||||
if val, exists := args["required_approvals"]; exists {
|
||||
if i, ok := params.ToInt64(val); ok {
|
||||
opt.RequiredApprovals = i
|
||||
}
|
||||
}
|
||||
if v, ok := args["block_on_rejected_reviews"].(bool); ok {
|
||||
opt.BlockOnRejectedReviews = v
|
||||
}
|
||||
if v, ok := args["block_on_outdated_branch"].(bool); ok {
|
||||
opt.BlockOnOutdatedBranch = v
|
||||
}
|
||||
if v, ok := args["dismiss_stale_approvals"].(bool); ok {
|
||||
opt.DismissStaleApprovals = v
|
||||
}
|
||||
if v, ok := args["require_signed_commits"].(bool); ok {
|
||||
opt.RequireSignedCommits = v
|
||||
}
|
||||
if v, ok := args["protected_file_patterns"].(string); ok {
|
||||
opt.ProtectedFilePatterns = v
|
||||
}
|
||||
if v, ok := args["unprotected_file_patterns"].(string); ok {
|
||||
opt.UnprotectedFilePatterns = v
|
||||
}
|
||||
|
||||
bp, _, err := client.CreateBranchProtection(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create branch protection in %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimBranchProtection(bp))
|
||||
}
|
||||
|
||||
func editBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called editBranchProtectionFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
ruleName, err := params.GetString(args, "rule_name")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
opt := gitea_sdk.EditBranchProtectionOption{}
|
||||
if v, ok := args["enable_push"].(bool); ok {
|
||||
opt.EnablePush = &v
|
||||
}
|
||||
if v := params.GetStringSlice(args, "push_whitelist_usernames"); v != nil {
|
||||
opt.PushWhitelistUsernames = v
|
||||
}
|
||||
if v := params.GetStringSlice(args, "push_whitelist_teams"); v != nil {
|
||||
opt.PushWhitelistTeams = v
|
||||
}
|
||||
if v, ok := args["enable_merge_whitelist"].(bool); ok {
|
||||
opt.EnableMergeWhitelist = &v
|
||||
}
|
||||
if v := params.GetStringSlice(args, "merge_whitelist_usernames"); v != nil {
|
||||
opt.MergeWhitelistUsernames = v
|
||||
}
|
||||
if v := params.GetStringSlice(args, "merge_whitelist_teams"); v != nil {
|
||||
opt.MergeWhitelistTeams = v
|
||||
}
|
||||
if val, exists := args["required_approvals"]; exists {
|
||||
if i, ok := params.ToInt64(val); ok {
|
||||
opt.RequiredApprovals = &i
|
||||
}
|
||||
}
|
||||
if v, ok := args["block_on_rejected_reviews"].(bool); ok {
|
||||
opt.BlockOnRejectedReviews = &v
|
||||
}
|
||||
if v, ok := args["block_on_outdated_branch"].(bool); ok {
|
||||
opt.BlockOnOutdatedBranch = &v
|
||||
}
|
||||
if v, ok := args["dismiss_stale_approvals"].(bool); ok {
|
||||
opt.DismissStaleApprovals = &v
|
||||
}
|
||||
if v, ok := args["require_signed_commits"].(bool); ok {
|
||||
opt.RequireSignedCommits = &v
|
||||
}
|
||||
if v, ok := args["protected_file_patterns"].(string); ok {
|
||||
opt.ProtectedFilePatterns = &v
|
||||
}
|
||||
if v, ok := args["unprotected_file_patterns"].(string); ok {
|
||||
opt.UnprotectedFilePatterns = &v
|
||||
}
|
||||
|
||||
bp, _, err := client.EditBranchProtection(owner, repo, ruleName, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("edit branch protection %v in %v/%v err: %v", ruleName, owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimBranchProtection(bp))
|
||||
}
|
||||
|
||||
func deleteBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteBranchProtectionFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
ruleName, err := params.GetString(args, "rule_name")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteBranchProtection(owner, repo, ruleName)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete branch protection %v from %v/%v err: %v", ruleName, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Branch protection rule '%s' deleted from %s/%s", ruleName, owner, repo)), nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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/params"
|
||||
"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 (
|
||||
CollaboratorReadToolName = "collaborator_read"
|
||||
CollaboratorWriteToolName = "collaborator_write"
|
||||
)
|
||||
|
||||
var (
|
||||
CollaboratorReadTool = mcp.NewTool(
|
||||
CollaboratorReadToolName,
|
||||
mcp.WithDescription("List repository collaborators."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list")),
|
||||
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.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
CollaboratorWriteTool = mcp.NewTool(
|
||||
CollaboratorWriteToolName,
|
||||
mcp.WithDescription("Add or remove repository collaborators. Use method 'add' to grant access, 'delete' to revoke."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("add", "delete")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("username", mcp.Required(), mcp.Description("collaborator username")),
|
||||
mcp.WithString("permission", mcp.Description("permission level (for 'add')"), mcp.Enum("read", "write", "admin")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: CollaboratorReadTool,
|
||||
Handler: collaboratorReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: CollaboratorWriteTool,
|
||||
Handler: collaboratorWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func collaboratorReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "list":
|
||||
return listCollaboratorsFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func listCollaboratorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listCollaboratorsFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
users, _, err := client.ListCollaborators(owner, repo, gitea_sdk.ListCollaboratorsOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list collaborators %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimUsers(users))
|
||||
}
|
||||
|
||||
func collaboratorWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "add":
|
||||
return addCollaboratorFn(ctx, req)
|
||||
case "delete":
|
||||
return deleteCollaboratorFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func addCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called addCollaboratorFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
username, err := params.GetString(args, "username")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
opt := gitea_sdk.AddCollaboratorOption{}
|
||||
if perm, ok := args["permission"].(string); ok {
|
||||
am := gitea_sdk.AccessMode(perm)
|
||||
opt.Permission = &am
|
||||
}
|
||||
_, err = client.AddCollaborator(owner, repo, username, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("add collaborator %v to %v/%v err: %v", username, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Collaborator %s added to %s/%s", username, owner, repo)), nil
|
||||
}
|
||||
|
||||
func deleteCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteCollaboratorFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
username, err := params.GetString(args, "username")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteCollaborator(owner, repo, username)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete collaborator %v from %v/%v err: %v", username, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Collaborator %s removed from %s/%s", username, owner, repo)), nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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/params"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
GetCompareToolName = "get_compare"
|
||||
)
|
||||
|
||||
var (
|
||||
GetCompareTool = mcp.NewTool(
|
||||
GetCompareToolName,
|
||||
mcp.WithDescription("Compare two branches, tags, or commits. Returns commits and diff stats between the base and head."),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("basehead", mcp.Required(), mcp.Description("comparison in format 'base...head' (e.g. 'main...feature-branch')")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: GetCompareTool,
|
||||
Handler: getCompareFn,
|
||||
})
|
||||
}
|
||||
|
||||
func getCompareFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getCompareFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
basehead, err := params.GetString(args, "basehead")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
_, err = gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/compare/%s", owner, repo, basehead), nil, nil, &result)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("compare %v in %v/%v err: %v", basehead, owner, repo, err))
|
||||
}
|
||||
return to.TextResult(result)
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
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/params"
|
||||
"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 (
|
||||
DeployKeyReadToolName = "deploy_key_read"
|
||||
DeployKeyWriteToolName = "deploy_key_write"
|
||||
)
|
||||
|
||||
var (
|
||||
DeployKeyReadTool = mcp.NewTool(
|
||||
DeployKeyReadToolName,
|
||||
mcp.WithDescription("Read deploy keys. Use method 'list' to list all, 'get' to get a specific key."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithNumber("id", mcp.Description("deploy key ID (required for 'get')")),
|
||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
DeployKeyWriteTool = mcp.NewTool(
|
||||
DeployKeyWriteToolName,
|
||||
mcp.WithDescription("Create or delete deploy keys. Use method 'create' to add a new key, 'delete' to remove."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "delete")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithNumber("id", mcp.Description("deploy key ID (required for 'delete')")),
|
||||
mcp.WithString("title", mcp.Description("key title (required for 'create')")),
|
||||
mcp.WithString("key", mcp.Description("SSH public key content (required for 'create')")),
|
||||
mcp.WithBoolean("read_only", mcp.Description("whether the key has read-only access (default true)")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: DeployKeyReadTool,
|
||||
Handler: deployKeyReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: DeployKeyWriteTool,
|
||||
Handler: deployKeyWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func deployKeyReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "list":
|
||||
return listDeployKeysFn(ctx, req)
|
||||
case "get":
|
||||
return getDeployKeyFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func listDeployKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listDeployKeysFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
keys, _, err := client.ListDeployKeys(owner, repo, gitea_sdk.ListDeployKeysOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list deploy keys %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimDeployKeys(keys))
|
||||
}
|
||||
|
||||
func getDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getDeployKeyFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
id, err := params.GetIndex(args, "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
key, _, err := client.GetDeployKey(owner, repo, id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get deploy key %v in %v/%v err: %v", id, owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimDeployKey(key))
|
||||
}
|
||||
|
||||
func deployKeyWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "create":
|
||||
return createDeployKeyFn(ctx, req)
|
||||
case "delete":
|
||||
return deleteDeployKeyFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func createDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called createDeployKeyFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
title, err := params.GetString(args, "title")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
key, err := params.GetString(args, "key")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
readOnly := true
|
||||
if v, ok := args["read_only"].(bool); ok {
|
||||
readOnly = v
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
dk, _, err := client.CreateDeployKey(owner, repo, gitea_sdk.CreateKeyOption{
|
||||
Title: title,
|
||||
Key: key,
|
||||
ReadOnly: readOnly,
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create deploy key in %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimDeployKey(dk))
|
||||
}
|
||||
|
||||
func deleteDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteDeployKeyFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
id, err := params.GetIndex(args, "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteDeployKey(owner, repo, id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete deploy key %v from %v/%v err: %v", id, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Deploy key %d deleted from %s/%s", id, owner, repo)), nil
|
||||
}
|
||||
@@ -23,6 +23,9 @@ const (
|
||||
ForkRepoToolName = "fork_repo"
|
||||
ListMyReposToolName = "list_my_repos"
|
||||
ListOrgReposToolName = "list_org_repos"
|
||||
GetRepoToolName = "get_repo"
|
||||
RepoWriteToolName = "repo_write"
|
||||
ListForksToolName = "list_forks"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -67,6 +70,40 @@ var (
|
||||
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)),
|
||||
)
|
||||
|
||||
GetRepoTool = mcp.NewTool(
|
||||
GetRepoToolName,
|
||||
mcp.WithDescription("Get repository details including settings, permissions, and metadata"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
)
|
||||
|
||||
RepoWriteTool = mcp.NewTool(
|
||||
RepoWriteToolName,
|
||||
mcp.WithDescription("Edit, delete, or transfer a repository. Use method 'edit' to change settings, 'delete' to remove, 'transfer' to change ownership."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("edit", "delete", "transfer")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("description", mcp.Description("repository description (for 'edit')")),
|
||||
mcp.WithString("website", mcp.Description("repository website URL (for 'edit')")),
|
||||
mcp.WithBoolean("private", mcp.Description("whether the repository is private (for 'edit')")),
|
||||
mcp.WithBoolean("has_issues", mcp.Description("enable issues (for 'edit')")),
|
||||
mcp.WithBoolean("has_wiki", mcp.Description("enable wiki (for 'edit')")),
|
||||
mcp.WithBoolean("has_pull_requests", mcp.Description("enable pull requests (for 'edit')")),
|
||||
mcp.WithString("default_branch", mcp.Description("default branch name (for 'edit')")),
|
||||
mcp.WithBoolean("archived", mcp.Description("archive/unarchive the repository (for 'edit')")),
|
||||
mcp.WithString("new_owner", mcp.Description("new owner username or org (required for 'transfer')")),
|
||||
mcp.WithArray("team_ids", mcp.Description("team IDs to grant access to (for 'transfer')"), mcp.Items(map[string]any{"type": "number"})),
|
||||
)
|
||||
|
||||
ListForksTool = mcp.NewTool(
|
||||
ListForksToolName,
|
||||
mcp.WithDescription("List forks of a repository"),
|
||||
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("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -86,6 +123,18 @@ func init() {
|
||||
Tool: ListOrgReposTool,
|
||||
Handler: ListOrgReposFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: GetRepoTool,
|
||||
Handler: GetRepoFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: RepoWriteTool,
|
||||
Handler: repoWriteFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: ListForksTool,
|
||||
Handler: ListForksFn,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
@@ -229,3 +278,174 @@ func ListOrgReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
|
||||
}
|
||||
return to.TextResult(repos)
|
||||
}
|
||||
|
||||
func GetRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called GetRepoFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
r, _, err := client.GetRepo(owner, repo)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get repo %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimRepoDetail(r))
|
||||
}
|
||||
|
||||
func repoWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "edit":
|
||||
return editRepoFn(ctx, req)
|
||||
case "delete":
|
||||
return deleteRepoFn(ctx, req)
|
||||
case "transfer":
|
||||
return transferRepoFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func editRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called editRepoFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
opt := gitea_sdk.EditRepoOption{}
|
||||
if v, ok := args["description"].(string); ok {
|
||||
opt.Description = &v
|
||||
}
|
||||
if v, ok := args["website"].(string); ok {
|
||||
opt.Website = &v
|
||||
}
|
||||
if v, ok := args["private"].(bool); ok {
|
||||
opt.Private = &v
|
||||
}
|
||||
if v, ok := args["has_issues"].(bool); ok {
|
||||
opt.HasIssues = &v
|
||||
}
|
||||
if v, ok := args["has_wiki"].(bool); ok {
|
||||
opt.HasWiki = &v
|
||||
}
|
||||
if v, ok := args["has_pull_requests"].(bool); ok {
|
||||
opt.HasPullRequests = &v
|
||||
}
|
||||
if v, ok := args["default_branch"].(string); ok {
|
||||
opt.DefaultBranch = &v
|
||||
}
|
||||
if v, ok := args["archived"].(bool); ok {
|
||||
opt.Archived = &v
|
||||
}
|
||||
|
||||
r, _, err := client.EditRepo(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("edit repo %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimRepo(r))
|
||||
}
|
||||
|
||||
func deleteRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteRepoFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteRepo(owner, repo)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete repo %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Repository %s/%s deleted successfully", owner, repo)), nil
|
||||
}
|
||||
|
||||
func transferRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called transferRepoFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
newOwner, err := params.GetString(args, "new_owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
opt := gitea_sdk.TransferRepoOption{
|
||||
NewOwner: newOwner,
|
||||
}
|
||||
if teamIDs, err := params.GetInt64Slice(args, "team_ids"); err == nil {
|
||||
opt.TeamIDs = &teamIDs
|
||||
}
|
||||
r, _, err := client.TransferRepo(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("transfer repo %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimRepo(r))
|
||||
}
|
||||
|
||||
func ListForksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called ListForksFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
forks, _, err := client.ListForks(owner, repo, gitea_sdk.ListForksOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list forks %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimRepos(forks))
|
||||
}
|
||||
|
||||
@@ -206,6 +206,146 @@ func slimTree(t *gitea_sdk.GitTreeResponse) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func slimRepoDetail(r *gitea_sdk.Repository) map[string]any {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
m := slimRepo(r)
|
||||
m["website"] = r.Website
|
||||
m["has_issues"] = r.HasIssues
|
||||
m["has_wiki"] = r.HasWiki
|
||||
m["has_pull_requests"] = r.HasPullRequests
|
||||
m["has_projects"] = r.HasProjects
|
||||
m["mirror"] = r.Mirror
|
||||
m["internal"] = r.Internal
|
||||
m["empty"] = r.Empty
|
||||
m["size"] = r.Size
|
||||
if r.Permissions != nil {
|
||||
m["permissions"] = map[string]any{
|
||||
"admin": r.Permissions.Admin,
|
||||
"push": r.Permissions.Push,
|
||||
"pull": r.Permissions.Pull,
|
||||
}
|
||||
}
|
||||
if r.Parent != nil {
|
||||
m["parent"] = map[string]any{
|
||||
"full_name": r.Parent.FullName,
|
||||
"html_url": r.Parent.HTMLURL,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func slimUser(u *gitea_sdk.User) map[string]any {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": u.ID,
|
||||
"login": u.UserName,
|
||||
"full_name": u.FullName,
|
||||
"email": u.Email,
|
||||
"avatar_url": u.AvatarURL,
|
||||
"is_admin": u.IsAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
func slimUsers(users []*gitea_sdk.User) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(users))
|
||||
for _, u := range users {
|
||||
out = append(out, slimUser(u))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimDeployKey(k *gitea_sdk.DeployKey) map[string]any {
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": k.ID,
|
||||
"title": k.Title,
|
||||
"fingerprint": k.Fingerprint,
|
||||
"key": k.Key,
|
||||
"url": k.URL,
|
||||
"read_only": k.ReadOnly,
|
||||
"created_at": k.Created,
|
||||
}
|
||||
}
|
||||
|
||||
func slimDeployKeys(keys []*gitea_sdk.DeployKey) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
out = append(out, slimDeployKey(k))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimBranchProtection(bp *gitea_sdk.BranchProtection) map[string]any {
|
||||
if bp == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"branch_name": bp.BranchName,
|
||||
"rule_name": bp.RuleName,
|
||||
"enable_push": bp.EnablePush,
|
||||
"push_whitelist_usernames": bp.PushWhitelistUsernames,
|
||||
"push_whitelist_teams": bp.PushWhitelistTeams,
|
||||
"enable_merge_whitelist": bp.EnableMergeWhitelist,
|
||||
"merge_whitelist_usernames": bp.MergeWhitelistUsernames,
|
||||
"merge_whitelist_teams": bp.MergeWhitelistTeams,
|
||||
"required_approvals": bp.RequiredApprovals,
|
||||
"enable_approvals_whitelist": bp.EnableApprovalsWhitelist,
|
||||
"block_on_rejected_reviews": bp.BlockOnRejectedReviews,
|
||||
"block_on_outdated_branch": bp.BlockOnOutdatedBranch,
|
||||
"dismiss_stale_approvals": bp.DismissStaleApprovals,
|
||||
"require_signed_commits": bp.RequireSignedCommits,
|
||||
"protected_file_patterns": bp.ProtectedFilePatterns,
|
||||
"unprotected_file_patterns": bp.UnprotectedFilePatterns,
|
||||
"created_at": bp.Created,
|
||||
"updated_at": bp.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
func slimBranchProtections(bps []*gitea_sdk.BranchProtection) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(bps))
|
||||
for _, bp := range bps {
|
||||
out = append(out, slimBranchProtection(bp))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimHook(h *gitea_sdk.Hook) map[string]any {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
m := map[string]any{
|
||||
"id": h.ID,
|
||||
"type": h.Type,
|
||||
"active": h.Active,
|
||||
"events": h.Events,
|
||||
"created_at": h.Created,
|
||||
"updated_at": h.Updated,
|
||||
}
|
||||
if h.Config != nil {
|
||||
if url, ok := h.Config["url"]; ok {
|
||||
m["url"] = url
|
||||
}
|
||||
if ct, ok := h.Config["content_type"]; ok {
|
||||
m["content_type"] = ct
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func slimHooks(hooks []*gitea_sdk.Hook) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(hooks))
|
||||
for _, h := range hooks {
|
||||
out = append(out, slimHook(h))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimDirEntries(entries []*gitea_sdk.ContentsResponse) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(entries))
|
||||
for _, c := range entries {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
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/params"
|
||||
"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 (
|
||||
TopicReadToolName = "topic_read"
|
||||
TopicWriteToolName = "topic_write"
|
||||
)
|
||||
|
||||
var (
|
||||
TopicReadTool = mcp.NewTool(
|
||||
TopicReadToolName,
|
||||
mcp.WithDescription("List repository topics."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
)
|
||||
|
||||
TopicWriteTool = mcp.NewTool(
|
||||
TopicWriteToolName,
|
||||
mcp.WithDescription("Manage repository topics. Use 'add' to add one, 'delete' to remove one, 'set' to replace all topics."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("add", "delete", "set")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("topic", mcp.Description("single topic name (required for 'add', 'delete')")),
|
||||
mcp.WithArray("topics", mcp.Description("list of topic names (required for 'set')"), mcp.Items(map[string]any{"type": "string"})),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: TopicReadTool,
|
||||
Handler: topicReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: TopicWriteTool,
|
||||
Handler: topicWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func topicReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called topicReadFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
topics, _, err := client.ListRepoTopics(owner, repo, gitea_sdk.ListRepoTopicsOptions{})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list topics %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(topics)
|
||||
}
|
||||
|
||||
func topicWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "add":
|
||||
return addTopicFn(ctx, req)
|
||||
case "delete":
|
||||
return deleteTopicFn(ctx, req)
|
||||
case "set":
|
||||
return setTopicsFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func addTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called addTopicFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
topic, err := params.GetString(args, "topic")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.AddRepoTopic(owner, repo, topic)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("add topic %v to %v/%v err: %v", topic, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Topic '%s' added to %s/%s", topic, owner, repo)), nil
|
||||
}
|
||||
|
||||
func deleteTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteTopicFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
topic, err := params.GetString(args, "topic")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteRepoTopic(owner, repo, topic)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete topic %v from %v/%v err: %v", topic, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Topic '%s' removed from %s/%s", topic, owner, repo)), nil
|
||||
}
|
||||
|
||||
func setTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called setTopicsFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
topics := params.GetStringSlice(args, "topics")
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.SetRepoTopics(owner, repo, topics)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("set topics for %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(map[string]any{
|
||||
"message": fmt.Sprintf("Topics set for %s/%s", owner, repo),
|
||||
"topics": topics,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
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/params"
|
||||
"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 (
|
||||
WebhookReadToolName = "webhook_read"
|
||||
WebhookWriteToolName = "webhook_write"
|
||||
)
|
||||
|
||||
var (
|
||||
WebhookReadTool = mcp.NewTool(
|
||||
WebhookReadToolName,
|
||||
mcp.WithDescription("Read repository webhooks. Use method 'list' to list all, 'get' to get a specific webhook."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithNumber("id", mcp.Description("webhook ID (required for 'get')")),
|
||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
WebhookWriteTool = mcp.NewTool(
|
||||
WebhookWriteToolName,
|
||||
mcp.WithDescription("Create, edit, delete, or test repository webhooks."),
|
||||
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "edit", "delete", "test")),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithNumber("id", mcp.Description("webhook ID (required for 'edit', 'delete', 'test')")),
|
||||
mcp.WithString("type", mcp.Description("webhook type (for 'create')"), mcp.Enum("gitea", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "wechatwork", "packagist")),
|
||||
mcp.WithString("url", mcp.Description("target URL (required for 'create', optional for 'edit')")),
|
||||
mcp.WithString("content_type", mcp.Description("content type"), mcp.Enum("json", "form")),
|
||||
mcp.WithString("secret", mcp.Description("webhook secret")),
|
||||
mcp.WithArray("events", mcp.Description("event types to trigger on (e.g. push, pull_request, issues)"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithBoolean("active", mcp.Description("whether the webhook is active")),
|
||||
mcp.WithString("branch_filter", mcp.Description("branch filter pattern")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: WebhookReadTool,
|
||||
Handler: webhookReadFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: WebhookWriteTool,
|
||||
Handler: webhookWriteFn,
|
||||
})
|
||||
}
|
||||
|
||||
func webhookReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "list":
|
||||
return listWebhooksFn(ctx, req)
|
||||
case "get":
|
||||
return getWebhookFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func listWebhooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called listWebhooksFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(args, 30)
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
hooks, _, err := client.ListRepoHooks(owner, repo, gitea_sdk.ListHooksOptions{
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list webhooks %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimHooks(hooks))
|
||||
}
|
||||
|
||||
func getWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called getWebhookFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
id, err := params.GetIndex(args, "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
hook, _, err := client.GetRepoHook(owner, repo, id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get webhook %v in %v/%v err: %v", id, owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimHook(hook))
|
||||
}
|
||||
|
||||
func webhookWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
method, err := params.GetString(args, "method")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
switch method {
|
||||
case "create":
|
||||
return createWebhookFn(ctx, req)
|
||||
case "edit":
|
||||
return editWebhookFn(ctx, req)
|
||||
case "delete":
|
||||
return deleteWebhookFn(ctx, req)
|
||||
case "test":
|
||||
return testWebhookFn(ctx, req)
|
||||
default:
|
||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
||||
}
|
||||
}
|
||||
|
||||
func createWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called createWebhookFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
hookType := params.GetOptionalString(args, "type", "gitea")
|
||||
url, err := params.GetString(args, "url")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
contentType := params.GetOptionalString(args, "content_type", "json")
|
||||
secret, _ := args["secret"].(string)
|
||||
events := params.GetStringSlice(args, "events")
|
||||
active := true
|
||||
if v, ok := args["active"].(bool); ok {
|
||||
active = v
|
||||
}
|
||||
branchFilter, _ := args["branch_filter"].(string)
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
opt := gitea_sdk.CreateHookOption{
|
||||
Type: gitea_sdk.HookType(hookType),
|
||||
Config: map[string]string{
|
||||
"url": url,
|
||||
"content_type": contentType,
|
||||
"secret": secret,
|
||||
},
|
||||
Events: events,
|
||||
Active: active,
|
||||
BranchFilter: branchFilter,
|
||||
}
|
||||
|
||||
hook, _, err := client.CreateRepoHook(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create webhook in %v/%v err: %v", owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimHook(hook))
|
||||
}
|
||||
|
||||
func editWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called editWebhookFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
id, err := params.GetIndex(args, "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
opt := gitea_sdk.EditHookOption{}
|
||||
config := map[string]string{}
|
||||
if v, ok := args["url"].(string); ok {
|
||||
config["url"] = v
|
||||
}
|
||||
if v, ok := args["content_type"].(string); ok {
|
||||
config["content_type"] = v
|
||||
}
|
||||
if v, ok := args["secret"].(string); ok {
|
||||
config["secret"] = v
|
||||
}
|
||||
if len(config) > 0 {
|
||||
opt.Config = config
|
||||
}
|
||||
if events := params.GetStringSlice(args, "events"); events != nil {
|
||||
opt.Events = events
|
||||
}
|
||||
if v, ok := args["active"].(bool); ok {
|
||||
opt.Active = &v
|
||||
}
|
||||
if v, ok := args["branch_filter"].(string); ok {
|
||||
opt.BranchFilter = v
|
||||
}
|
||||
|
||||
_, err = client.EditRepoHook(owner, repo, id, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("edit webhook %v in %v/%v err: %v", id, owner, repo, err))
|
||||
}
|
||||
// EditRepoHook doesn't return the hook, fetch it
|
||||
hook, _, err := client.GetRepoHook(owner, repo, id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get webhook %v in %v/%v after edit err: %v", id, owner, repo, err))
|
||||
}
|
||||
return to.TextResult(slimHook(hook))
|
||||
}
|
||||
|
||||
func deleteWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called deleteWebhookFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
id, err := params.GetIndex(args, "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
_, err = client.DeleteRepoHook(owner, repo, id)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete webhook %v from %v/%v err: %v", id, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Webhook %d deleted from %s/%s", id, owner, repo)), nil
|
||||
}
|
||||
|
||||
func testWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called testWebhookFn")
|
||||
args := req.GetArguments()
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
id, err := params.GetIndex(args, "id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
// TestRepoHook is not in the SDK - use raw REST
|
||||
_, err = gitea.DoJSON(ctx, "POST", fmt.Sprintf("repos/%s/%s/hooks/%d/tests", owner, repo, id), nil, nil, nil)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("test webhook %v in %v/%v err: %v", id, owner, repo, err))
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Webhook %d test triggered for %s/%s", id, owner, repo)), nil
|
||||
}
|
||||
@@ -3,5 +3,7 @@ package context
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
TokenContextKey = contextKey("token")
|
||||
TokenContextKey = contextKey("token")
|
||||
HostContextKey = contextKey("host")
|
||||
InsecureContextKey = contextKey("insecure")
|
||||
)
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package flag
|
||||
|
||||
// ServerConfig holds connection details for a named Gitea server.
|
||||
type ServerConfig struct {
|
||||
Host string `json:"host"`
|
||||
Token string `json:"token"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
Host string
|
||||
Port int
|
||||
@@ -10,4 +17,8 @@ var (
|
||||
Insecure bool
|
||||
ReadOnly bool
|
||||
Debug bool
|
||||
|
||||
// Servers maps a friendly name to its connection config.
|
||||
// Populated from the GITEA_SERVERS env var (JSON object).
|
||||
Servers map[string]ServerConfig
|
||||
)
|
||||
|
||||
+24
-9
@@ -12,16 +12,16 @@ import (
|
||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||
)
|
||||
|
||||
func NewClient(token string) (*gitea.Client, error) {
|
||||
func newClientForHost(host, token string, insecure bool) (*gitea.Client, error) {
|
||||
httpClient := &http.Client{
|
||||
Transport: http.DefaultTransport,
|
||||
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
CheckRedirect: checkRedirect,
|
||||
}
|
||||
|
||||
opts := []gitea.ClientOption{
|
||||
gitea.SetToken(token),
|
||||
}
|
||||
if flag.Insecure {
|
||||
if insecure {
|
||||
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
@@ -30,16 +30,20 @@ func NewClient(token string) (*gitea.Client, error) {
|
||||
if flag.Debug {
|
||||
opts = append(opts, gitea.SetDebugMode())
|
||||
}
|
||||
client, err := gitea.NewClient(flag.Host, opts...)
|
||||
client, err := gitea.NewClient(host, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create gitea client err: %w", err)
|
||||
}
|
||||
|
||||
// Set user agent for the client
|
||||
client.SetUserAgent("gitea-mcp-server/" + flag.Version)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewClient creates a Gitea client using the default host and the given token.
|
||||
func NewClient(token string) (*gitea.Client, error) {
|
||||
return newClientForHost(flag.Host, token, flag.Insecure)
|
||||
}
|
||||
|
||||
// checkRedirect prevents Go from silently changing mutating requests (POST, PATCH, etc.)
|
||||
// to GET when following 301/302/303 redirects, which would drop the request body and
|
||||
// make writes appear to succeed when they didn't.
|
||||
@@ -53,10 +57,21 @@ func checkRedirect(_ *http.Request, via []*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClientFromContext creates a Gitea client using connection details from the
|
||||
// context (set by the multi-server wrapper) or falling back to global defaults.
|
||||
func ClientFromContext(ctx context.Context) (*gitea.Client, error) {
|
||||
token, ok := ctx.Value(mcpContext.TokenContextKey).(string)
|
||||
if !ok {
|
||||
token = flag.Token
|
||||
host := flag.Host
|
||||
token := flag.Token
|
||||
insecure := flag.Insecure
|
||||
|
||||
if h, ok := ctx.Value(mcpContext.HostContextKey).(string); ok && h != "" {
|
||||
host = h
|
||||
}
|
||||
return NewClient(token)
|
||||
if t, ok := ctx.Value(mcpContext.TokenContextKey).(string); ok && t != "" {
|
||||
token = t
|
||||
}
|
||||
if i, ok := ctx.Value(mcpContext.InsecureContextKey).(bool); ok {
|
||||
insecure = i
|
||||
}
|
||||
return newClientForHost(host, token, insecure)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user