Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a519798e69 | |||
| 12f0e872c1 | |||
| 9d41be6c4f | |||
| 8a314a8a5b | |||
| f9cefeecd8 | |||
| f5837d0569 | |||
| 943bc4a063 | |||
| a2eeab9719 | |||
| 842502ca5a | |||
| 3ac04e69a8 | |||
| 58d2feb54a | |||
| 4b4b5ee247 | |||
| 08128b9471 | |||
| 133fe487cd | |||
| 05682e2afa | |||
| a5dd03c7f0 | |||
| 9056a5ef27 | |||
| c8004e9198 |
@@ -3,3 +3,4 @@ gitea-mcp
|
|||||||
gitea-mcp.exe
|
gitea-mcp.exe
|
||||||
*.log
|
*.log
|
||||||
tmp
|
tmp
|
||||||
|
gitea-mcp-new
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
|||||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Print this help message.
|
help: ## print this help message
|
||||||
@echo "Usage: make [target]"
|
@echo "Usage: make [target]"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@@ -16,7 +16,7 @@ help: ## Print this help message.
|
|||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: build ## Install the application.
|
install: build ## install the application
|
||||||
@echo "Installing $(EXECUTABLE)..."
|
@echo "Installing $(EXECUTABLE)..."
|
||||||
@mkdir -p $(GOPATH)/bin
|
@mkdir -p $(GOPATH)/bin
|
||||||
@cp $(EXECUTABLE) $(GOPATH)/bin/$(EXECUTABLE)
|
@cp $(EXECUTABLE) $(GOPATH)/bin/$(EXECUTABLE)
|
||||||
@@ -24,23 +24,23 @@ install: build ## Install the application.
|
|||||||
@echo "Please add $(GOPATH)/bin to your PATH if it is not already there."
|
@echo "Please add $(GOPATH)/bin to your PATH if it is not already there."
|
||||||
|
|
||||||
.PHONY: uninstall
|
.PHONY: uninstall
|
||||||
uninstall: ## Uninstall the application.
|
uninstall: ## uninstall the application
|
||||||
@echo "Uninstalling $(EXECUTABLE)..."
|
@echo "Uninstalling $(EXECUTABLE)..."
|
||||||
@rm -f $(GOPATH)/bin/$(EXECUTABLE)
|
@rm -f $(GOPATH)/bin/$(EXECUTABLE)
|
||||||
@echo "Uninstalled $(EXECUTABLE) from $(GOPATH)/bin/$(EXECUTABLE)"
|
@echo "Uninstalled $(EXECUTABLE) from $(GOPATH)/bin/$(EXECUTABLE)"
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Clean the build artifacts.
|
clean: ## delete build artifacts
|
||||||
@echo "Cleaning up build artifacts..."
|
@echo "Cleaning up build artifacts..."
|
||||||
@rm -f $(EXECUTABLE)
|
@rm -f $(EXECUTABLE)
|
||||||
@echo "Cleaned up $(EXECUTABLE)"
|
@echo "Cleaned up $(EXECUTABLE)"
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: ## Build the application.
|
build: ## build the application
|
||||||
$(GO) build -v -ldflags '-s -w $(LDFLAGS)' -o $(EXECUTABLE)
|
$(GO) build -v -ldflags '-s -w $(LDFLAGS)' -o $(EXECUTABLE)
|
||||||
|
|
||||||
.PHONY: air
|
.PHONY: air
|
||||||
air: ## Install air for hot reload.
|
air: ## install air for hot reload
|
||||||
@hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
$(GO) install github.com/air-verse/air@latest; \
|
$(GO) install github.com/air-verse/air@latest; \
|
||||||
fi
|
fi
|
||||||
@@ -49,6 +49,10 @@ air: ## Install air for hot reload.
|
|||||||
dev: air ## run the application with hot reload
|
dev: air ## run the application with hot reload
|
||||||
air --build.cmd "make build" --build.bin ./gitea-mcp
|
air --build.cmd "make build" --build.bin ./gitea-mcp
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt: ## format the Go code
|
||||||
|
$(GO) run $(GOFUMPT_PACKAGE) -w .
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-go ## lint everything
|
lint: lint-go ## lint everything
|
||||||
|
|
||||||
|
|||||||
@@ -1,284 +1,135 @@
|
|||||||
# Gitea MCP Server
|
# 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
|
```bash
|
||||||
claude mcp add --transport stdio --scope user gitea \
|
curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash
|
||||||
--env GITEA_ACCESS_TOKEN=token \
|
|
||||||
--env GITEA_HOST=https://gitea.com \
|
|
||||||
-- go run gitea.com/gitea/gitea-mcp@latest -t stdio
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```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
|
> **חשוב:** כדי שההגדרה תהיה **גלובלית** (זמינה בכל פרויקט), יש לכתוב אותה ב-`~/.claude/settings.json` תחת `mcpServers`.
|
||||||
- Golang (Go 1.24 or later recommended)
|
> אל תשתמשו ב-`.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
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"gitea": {
|
"gitea-dev": {
|
||||||
"command": "gitea-mcp",
|
"command": "gitea-mcp",
|
||||||
"args": [
|
"args": ["-t", "stdio", "--host", "https://gitea.example.com"],
|
||||||
"-t",
|
|
||||||
"stdio",
|
|
||||||
"--host",
|
|
||||||
"https://gitea.com"
|
|
||||||
// "--token", "<your personal access token>"
|
|
||||||
],
|
|
||||||
"env": {
|
"env": {
|
||||||
// "GITEA_HOST": "https://gitea.com",
|
"GITEA_ACCESS_TOKEN": "<token>"
|
||||||
// "GITEA_INSECURE": "true",
|
|
||||||
"GITEA_ACCESS_TOKEN": "<your personal access token>"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **http mode**
|
### כמה שרתים
|
||||||
|
|
||||||
|
הוסף את `GITEA_SERVERS` כ-JSON object עם שם → `{host, token, insecure}`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"gitea": {
|
"gitea-dev": {
|
||||||
"url": "http://localhost:8080/mcp",
|
"command": "gitea-mcp",
|
||||||
"headers": {
|
"args": ["-t", "stdio", "--host", "https://gitea.example.com"],
|
||||||
"Authorization": "Bearer <your personal access token>"
|
"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.
|
list_my_repos # ברירת מחדל
|
||||||
> Command-line arguments have the highest priority
|
list_my_repos(server: "prod") # שרת ספציפי
|
||||||
|
list_servers # הצג שרתים מוגדרים
|
||||||
Once everything is set up, try typing the following in your MCP-compatible chatbox:
|
|
||||||
|
|
||||||
```text
|
|
||||||
list all my repositories
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✅ Available Tools
|
## משתני סביבה
|
||||||
|
|
||||||
The Gitea MCP Server supports the following tools:
|
| משתנה | תיאור |
|
||||||
|
|---|---|
|
||||||
|
| `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:
|
MIT
|
||||||
|
|
||||||
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!
|
|
||||||
|
|||||||
+4
-1
@@ -163,9 +163,12 @@ cp gitea-mcp /usr/local/bin/
|
|||||||
**默认日志路径**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
**默认日志路径**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||||
|
|
||||||
> [!注意]
|
> [!注意]
|
||||||
> 可通过命令行参数或环境变量提供 Gitea 主机和访问令牌。
|
> 可通过命令行参数或环境变量提供 Gitea 主机和访问令牌。
|
||||||
> 命令行参数优先。
|
> 命令行参数优先。
|
||||||
|
|
||||||
|
> [!注意]
|
||||||
|
> 许多工具支持 `page` 和 `perPage` 分页参数。最大有效页面大小由 Gitea 服务器的 `[api].MAX_RESPONSE_ITEMS` 设置决定(默认值:**50**)。请求超过此限制的 `perPage` 值将被服务器静默截断。
|
||||||
|
|
||||||
一切设置完成后,可在 MCP 聊天框输入:
|
一切设置完成后,可在 MCP 聊天框输入:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
+4
-1
@@ -163,9 +163,12 @@ cp gitea-mcp /usr/local/bin/
|
|||||||
**預設日誌路徑**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
**預設日誌路徑**: `$HOME/.gitea-mcp/gitea-mcp.log`
|
||||||
|
|
||||||
> [!注意]
|
> [!注意]
|
||||||
> 可用命令列參數或環境變數提供 Gitea 主機與存取令牌。
|
> 可用命令列參數或環境變數提供 Gitea 主機與存取令牌。
|
||||||
> 命令列參數優先。
|
> 命令列參數優先。
|
||||||
|
|
||||||
|
> [!注意]
|
||||||
|
> 許多工具支援 `page` 和 `perPage` 分頁參數。最大有效頁面大小由 Gitea 伺服器的 `[api].MAX_RESPONSE_ITEMS` 設定決定(預設值:**50**)。請求超過此限制的 `perPage` 值將被伺服器靜默截斷。
|
||||||
|
|
||||||
一切設定完成後,可在 MCP 聊天框輸入:
|
一切設定完成後,可在 MCP 聊天框輸入:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
+13
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -57,6 +58,7 @@ func init() {
|
|||||||
fmt.Fprintf(w, " GITEA_HOST\tOverride Gitea host URL\n")
|
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_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_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")
|
fmt.Fprintf(w, " MCP_MODE\tOverride transport mode\n")
|
||||||
w.Flush()
|
w.Flush()
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,17 @@ func init() {
|
|||||||
if os.Getenv("GITEA_INSECURE") == "true" {
|
if os.Getenv("GITEA_INSECURE") == "true" {
|
||||||
flagPkg.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() {
|
func Execute() {
|
||||||
|
|||||||
+4
-3
@@ -6,11 +6,12 @@
|
|||||||
"-t", "stdio",
|
"-t", "stdio",
|
||||||
"--host", "https://gitea.com",
|
"--host", "https://gitea.com",
|
||||||
"--token", "<your personal access token>"
|
"--token", "<your personal access token>"
|
||||||
]
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"GITEA_HOST": "https://gitea.com",
|
"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>\"}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ go 1.26.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.23.2
|
code.gitea.io/sdk/gitea v0.23.2
|
||||||
github.com/mark3labs/mcp-go v0.44.0
|
github.com/mark3labs/mcp-go v0.45.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mark3labs/mcp-go v0.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I=
|
github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
|
||||||
github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
|||||||
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 ""
|
||||||
@@ -54,7 +54,7 @@ var (
|
|||||||
mcp.WithString("org", mcp.Description("organization name (required for org methods)")),
|
mcp.WithString("org", mcp.Description("organization name (required for org methods)")),
|
||||||
mcp.WithString("name", mcp.Description("variable name (required for get methods)")),
|
mcp.WithString("name", mcp.Description("variable name (required for get methods)")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
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)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30), mcp.Min(1)),
|
||||||
)
|
)
|
||||||
|
|
||||||
ActionsConfigWriteTool = mcp.NewTool(
|
ActionsConfigWriteTool = mcp.NewTool(
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ var (
|
|||||||
mcp.WithNumber("max_bytes", mcp.Description("max bytes to return (for 'get_job_log_preview')"), mcp.DefaultNumber(65536), mcp.Min(1024)),
|
mcp.WithNumber("max_bytes", mcp.Description("max bytes to return (for 'get_job_log_preview')"), mcp.DefaultNumber(65536), mcp.Min(1024)),
|
||||||
mcp.WithString("output_path", mcp.Description("output file path (for 'download_job_log')")),
|
mcp.WithString("output_path", mcp.Description("output file path (for 'download_job_log')")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
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)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30), mcp.Min(1)),
|
||||||
)
|
)
|
||||||
|
|
||||||
ActionsRunWriteTool = mcp.NewTool(
|
ActionsRunWriteTool = mcp.NewTool(
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ var (
|
|||||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")),
|
mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")),
|
||||||
|
mcp.WithArray("labels", mcp.Description("filter by label names"), mcp.Items(map[string]any{"type": "string"})),
|
||||||
|
mcp.WithString("since", mcp.Description("filter issues updated after this ISO 8601 timestamp")),
|
||||||
|
mcp.WithString("before", mcp.Description("filter issues updated before this ISO 8601 timestamp")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
IssueReadTool = mcp.NewTool(
|
IssueReadTool = mcp.NewTool(
|
||||||
@@ -56,8 +59,11 @@ var (
|
|||||||
mcp.WithNumber("milestone", mcp.Description("milestone number (for 'create', 'update')")),
|
mcp.WithNumber("milestone", mcp.Description("milestone number (for 'create', 'update')")),
|
||||||
mcp.WithString("state", mcp.Description("issue state, one of open, closed, all (for 'update')")),
|
mcp.WithString("state", mcp.Description("issue state, one of open, closed, all (for 'update')")),
|
||||||
mcp.WithNumber("commentID", mcp.Description("id of issue comment (required for 'edit_comment')")),
|
mcp.WithNumber("commentID", mcp.Description("id of issue comment (required for 'edit_comment')")),
|
||||||
mcp.WithArray("labels", mcp.Description("array of label IDs (for 'add_labels', 'replace_labels')"), mcp.Items(map[string]any{"type": "number"})),
|
mcp.WithArray("labels", mcp.Description("array of label IDs (for 'create', 'add_labels', 'replace_labels')"), mcp.Items(map[string]any{"type": "number"})),
|
||||||
mcp.WithNumber("label_id", mcp.Description("label ID to remove (required for 'remove_label')")),
|
mcp.WithNumber("label_id", mcp.Description("label ID to remove (required for 'remove_label')")),
|
||||||
|
mcp.WithString("ref", mcp.Description("branch name to associate with the issue (for 'create', 'update')")),
|
||||||
|
mcp.WithString("deadline", mcp.Description("due date in ISO 8601 format (for 'create', 'update')")),
|
||||||
|
mcp.WithBoolean("remove_deadline", mcp.Description("unset due date (for 'update')")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,14 +168,22 @@ func listRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
if !ok {
|
if !ok {
|
||||||
state = "all"
|
state = "all"
|
||||||
}
|
}
|
||||||
|
labels := params.GetStringSlice(req.GetArguments(), "labels")
|
||||||
page, pageSize := params.GetPagination(req.GetArguments(), 30)
|
page, pageSize := params.GetPagination(req.GetArguments(), 30)
|
||||||
opt := gitea_sdk.ListIssueOption{
|
opt := gitea_sdk.ListIssueOption{
|
||||||
State: gitea_sdk.StateType(state),
|
State: gitea_sdk.StateType(state),
|
||||||
|
Labels: labels,
|
||||||
ListOptions: gitea_sdk.ListOptions{
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if t := params.GetOptionalTime(req.GetArguments(), "since"); t != nil {
|
||||||
|
opt.Since = *t
|
||||||
|
}
|
||||||
|
if t := params.GetOptionalTime(req.GetArguments(), "before"); t != nil {
|
||||||
|
opt.Before = *t
|
||||||
|
}
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
@@ -213,6 +227,13 @@ func createIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
|
|||||||
opt.Milestone = milestone
|
opt.Milestone = milestone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if labelIDs, err := params.GetInt64Slice(req.GetArguments(), "labels"); err == nil {
|
||||||
|
opt.Labels = labelIDs
|
||||||
|
}
|
||||||
|
if ref, ok := req.GetArguments()["ref"].(string); ok {
|
||||||
|
opt.Ref = ref
|
||||||
|
}
|
||||||
|
opt.Deadline = params.GetOptionalTime(req.GetArguments(), "deadline")
|
||||||
issue, _, err := client.CreateIssue(owner, repo, opt)
|
issue, _, err := client.CreateIssue(owner, repo, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err))
|
return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err))
|
||||||
@@ -289,6 +310,13 @@ func editIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
|
|||||||
if ok {
|
if ok {
|
||||||
opt.State = new(gitea_sdk.StateType(state))
|
opt.State = new(gitea_sdk.StateType(state))
|
||||||
}
|
}
|
||||||
|
if ref, ok := req.GetArguments()["ref"].(string); ok {
|
||||||
|
opt.Ref = &ref
|
||||||
|
}
|
||||||
|
opt.Deadline = params.GetOptionalTime(req.GetArguments(), "deadline")
|
||||||
|
if removeDeadline, ok := req.GetArguments()["remove_deadline"].(bool); ok {
|
||||||
|
opt.RemoveDeadline = &removeDeadline
|
||||||
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_listRepoIssuesFn_filters(t *testing.T) {
|
||||||
|
const (
|
||||||
|
owner = "octo"
|
||||||
|
repo = "demo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
gotQuery string
|
||||||
|
)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/version":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
|
||||||
|
case r.URL.Path == fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"private":false}`))
|
||||||
|
case r.URL.Path == fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner, repo):
|
||||||
|
mu.Lock()
|
||||||
|
gotQuery = r.URL.RawQuery
|
||||||
|
mu.Unlock()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`[]`))
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
origHost := flag.Host
|
||||||
|
origToken := flag.Token
|
||||||
|
origVersion := flag.Version
|
||||||
|
flag.Host = server.URL
|
||||||
|
flag.Token = ""
|
||||||
|
flag.Version = "test"
|
||||||
|
defer func() {
|
||||||
|
flag.Host = origHost
|
||||||
|
flag.Token = origToken
|
||||||
|
flag.Version = origVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := mcp.CallToolRequest{
|
||||||
|
Params: mcp.CallToolParams{
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"owner": owner,
|
||||||
|
"repo": repo,
|
||||||
|
"labels": []any{"bug", "enhancement"},
|
||||||
|
"since": "2026-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := listRepoIssuesFn(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listRepoIssuesFn() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if !strings.Contains(gotQuery, "labels=bug%2Cenhancement") {
|
||||||
|
t.Fatalf("expected labels query param, got %s", gotQuery)
|
||||||
|
}
|
||||||
|
if !strings.Contains(gotQuery, "since=2026-01-01") {
|
||||||
|
t.Fatalf("expected since query param, got %s", gotQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_createIssueFn_labels(t *testing.T) {
|
||||||
|
const (
|
||||||
|
owner = "octo"
|
||||||
|
repo = "demo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
gotBody map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/version":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"private":false}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner, repo):
|
||||||
|
mu.Lock()
|
||||||
|
var body map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
gotBody = body
|
||||||
|
mu.Unlock()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"number":1,"title":"test","state":"open"}`))
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
origHost := flag.Host
|
||||||
|
origToken := flag.Token
|
||||||
|
origVersion := flag.Version
|
||||||
|
flag.Host = server.URL
|
||||||
|
flag.Token = ""
|
||||||
|
flag.Version = "test"
|
||||||
|
defer func() {
|
||||||
|
flag.Host = origHost
|
||||||
|
flag.Token = origToken
|
||||||
|
flag.Version = origVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := mcp.CallToolRequest{
|
||||||
|
Params: mcp.CallToolParams{
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"owner": owner,
|
||||||
|
"repo": repo,
|
||||||
|
"title": "test issue",
|
||||||
|
"body": "body",
|
||||||
|
"labels": []any{float64(10), float64(20)},
|
||||||
|
"deadline": "2026-06-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := createIssueFn(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createIssueFn() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
labels, ok := gotBody["labels"].([]any)
|
||||||
|
if !ok || len(labels) != 2 {
|
||||||
|
t.Fatalf("expected 2 labels, got %v", gotBody["labels"])
|
||||||
|
}
|
||||||
|
if labels[0] != float64(10) || labels[1] != float64(20) {
|
||||||
|
t.Fatalf("expected labels [10,20], got %v", labels)
|
||||||
|
}
|
||||||
|
if gotBody["due_date"] == nil {
|
||||||
|
t.Fatalf("expected due_date to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,12 @@ func slimIssue(i *gitea_sdk.Issue) map[string]any {
|
|||||||
"title": i.Milestone.Title,
|
"title": i.Milestone.Title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if i.Ref != "" {
|
||||||
|
m["ref"] = i.Ref
|
||||||
|
}
|
||||||
|
if i.Deadline != nil {
|
||||||
|
m["deadline"] = i.Deadline
|
||||||
|
}
|
||||||
if i.PullRequest != nil {
|
if i.PullRequest != nil {
|
||||||
m["is_pull"] = true
|
m["is_pull"] = true
|
||||||
}
|
}
|
||||||
@@ -88,6 +94,12 @@ func slimIssues(issues []*gitea_sdk.Issue) []map[string]any {
|
|||||||
if len(i.Labels) > 0 {
|
if len(i.Labels) > 0 {
|
||||||
m["labels"] = labelNames(i.Labels)
|
m["labels"] = labelNames(i.Labels)
|
||||||
}
|
}
|
||||||
|
if i.Ref != "" {
|
||||||
|
m["ref"] = i.Ref
|
||||||
|
}
|
||||||
|
if i.Deadline != nil {
|
||||||
|
m["deadline"] = i.Deadline
|
||||||
|
}
|
||||||
out = append(out, m)
|
out = append(out, m)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var (
|
|||||||
mcp.WithString("org", mcp.Description("organization name (required for 'list_org')")),
|
mcp.WithString("org", mcp.Description("organization name (required for 'list_org')")),
|
||||||
mcp.WithNumber("id", mcp.Description("label ID (required for 'get_repo')")),
|
mcp.WithNumber("id", mcp.Description("label ID (required for 'get_repo')")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
LabelWriteTool = mcp.NewTool(
|
LabelWriteTool = mcp.NewTool(
|
||||||
@@ -47,6 +47,7 @@ var (
|
|||||||
mcp.WithString("color", mcp.Description("label color hex code e.g. #RRGGBB (required for create, optional for edit)")),
|
mcp.WithString("color", mcp.Description("label color hex code e.g. #RRGGBB (required for create, optional for edit)")),
|
||||||
mcp.WithString("description", mcp.Description("label description")),
|
mcp.WithString("description", mcp.Description("label description")),
|
||||||
mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive (org labels only)")),
|
mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive (org labels only)")),
|
||||||
|
mcp.WithBoolean("is_archived", mcp.Description("whether the label is archived (for create/edit repo label methods)")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,10 +179,13 @@ func createRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
|||||||
}
|
}
|
||||||
description, _ := req.GetArguments()["description"].(string) // Optional
|
description, _ := req.GetArguments()["description"].(string) // Optional
|
||||||
|
|
||||||
|
isArchived, _ := req.GetArguments()["is_archived"].(bool)
|
||||||
|
|
||||||
opt := gitea_sdk.CreateLabelOption{
|
opt := gitea_sdk.CreateLabelOption{
|
||||||
Name: name,
|
Name: name,
|
||||||
Color: color,
|
Color: color,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
IsArchived: isArchived,
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
@@ -220,6 +224,9 @@ func editRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
|
|||||||
if description, ok := req.GetArguments()["description"].(string); ok {
|
if description, ok := req.GetArguments()["description"].(string); ok {
|
||||||
opt.Description = new(description)
|
opt.Description = new(description)
|
||||||
}
|
}
|
||||||
|
if isArchived, ok := req.GetArguments()["is_archived"].(bool); ok {
|
||||||
|
opt.IsArchived = &isArchived
|
||||||
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ var (
|
|||||||
mcp.WithString("state", mcp.Description("milestone state (for 'list')"), mcp.DefaultString("all")),
|
mcp.WithString("state", mcp.Description("milestone state (for 'list')"), mcp.DefaultString("all")),
|
||||||
mcp.WithString("name", mcp.Description("milestone name filter (for 'list')")),
|
mcp.WithString("name", mcp.Description("milestone name filter (for 'list')")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
MilestoneWriteTool = mcp.NewTool(
|
MilestoneWriteTool = mcp.NewTool(
|
||||||
|
|||||||
@@ -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"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -15,6 +16,8 @@ import (
|
|||||||
"gitea.com/gitea/gitea-mcp/operation/issue"
|
"gitea.com/gitea/gitea-mcp/operation/issue"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/label"
|
"gitea.com/gitea/gitea-mcp/operation/label"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/milestone"
|
"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/pull"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/repo"
|
"gitea.com/gitea/gitea-mcp/operation/repo"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/search"
|
"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/flag"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mcpServer *server.MCPServer
|
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) {
|
func RegisterTool(s *server.MCPServer) {
|
||||||
// User Tool
|
// Collect all tools from every domain package.
|
||||||
s.AddTools(user.Tool.Tools()...)
|
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
|
// If multiple servers are configured, inject the "server" parameter into every tool.
|
||||||
s.AddTools(actions.Tool.Tools()...)
|
if len(flag.Servers) > 0 {
|
||||||
|
allTools = injectServerParam(allTools)
|
||||||
|
|
||||||
// Repo Tool
|
// Add a list_servers meta-tool so callers can discover available servers.
|
||||||
s.AddTools(repo.Tool.Tools()...)
|
allTools = append(allTools, server.ServerTool{
|
||||||
|
Tool: mcp.NewTool("list_servers",
|
||||||
// Issue Tool
|
mcp.WithDescription("List all configured Gitea server connections"),
|
||||||
s.AddTools(issue.Tool.Tools()...)
|
),
|
||||||
|
Handler: listServersFn,
|
||||||
// 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()...)
|
|
||||||
|
|
||||||
|
s.AddTools(allTools...)
|
||||||
s.DeleteTools("")
|
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
|
||||||
|
}
|
||||||
+105
-6
@@ -35,20 +35,20 @@ var (
|
|||||||
mcp.WithString("sort", mcp.Description("sort"), mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")),
|
mcp.WithString("sort", mcp.Description("sort"), mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")),
|
||||||
mcp.WithNumber("milestone", mcp.Description("milestone")),
|
mcp.WithNumber("milestone", mcp.Description("milestone")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
PullRequestReadTool = mcp.NewTool(
|
PullRequestReadTool = mcp.NewTool(
|
||||||
PullRequestReadToolName,
|
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.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")),
|
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("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
mcp.WithNumber("review_id", mcp.Description("review ID (required for 'get_review', 'get_review_comments')")),
|
mcp.WithNumber("review_id", mcp.Description("review ID (required for 'get_review', 'get_review_comments')")),
|
||||||
mcp.WithBoolean("binary", mcp.Description("whether to include binary file changes (for 'get_diff')")),
|
mcp.WithBoolean("binary", mcp.Description("whether to include binary file changes (for 'get_diff')")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
PullRequestWriteTool = mcp.NewTool(
|
PullRequestWriteTool = mcp.NewTool(
|
||||||
@@ -67,9 +67,15 @@ var (
|
|||||||
mcp.WithNumber("milestone", mcp.Description("milestone number (for 'update')")),
|
mcp.WithNumber("milestone", mcp.Description("milestone number (for 'update')")),
|
||||||
mcp.WithString("state", mcp.Description("PR state (for 'update')"), mcp.Enum("open", "closed")),
|
mcp.WithString("state", mcp.Description("PR state (for 'update')"), mcp.Enum("open", "closed")),
|
||||||
mcp.WithBoolean("allow_maintainer_edit", mcp.Description("allow maintainer to edit (for 'update')")),
|
mcp.WithBoolean("allow_maintainer_edit", mcp.Description("allow maintainer to edit (for 'update')")),
|
||||||
|
mcp.WithArray("labels", mcp.Description("array of label IDs (for 'create', 'update')"), mcp.Items(map[string]any{"type": "number"})),
|
||||||
|
mcp.WithString("deadline", mcp.Description("due date in ISO 8601 format (for 'create', 'update')")),
|
||||||
|
mcp.WithBoolean("remove_deadline", mcp.Description("unset due date (for 'update')")),
|
||||||
mcp.WithString("merge_style", mcp.Description("merge style (for 'merge')"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
|
mcp.WithString("merge_style", mcp.Description("merge style (for 'merge')"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
|
||||||
mcp.WithString("message", mcp.Description("merge commit message (for 'merge') or dismissal reason")),
|
mcp.WithString("message", mcp.Description("merge commit message (for 'merge') or dismissal reason")),
|
||||||
mcp.WithBoolean("delete_branch", mcp.Description("delete branch after merge (for 'merge')")),
|
mcp.WithBoolean("delete_branch", mcp.Description("delete branch after merge (for 'merge')")),
|
||||||
|
mcp.WithBoolean("force_merge", mcp.Description("force merge even if checks are not passing (for 'merge')")),
|
||||||
|
mcp.WithBoolean("merge_when_checks_succeed", mcp.Description("auto-merge when checks succeed (for 'merge')")),
|
||||||
|
mcp.WithString("head_commit_id", mcp.Description("expected head commit SHA for merge conflict detection (for 'merge')")),
|
||||||
mcp.WithArray("reviewers", mcp.Description("reviewer usernames (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
|
mcp.WithArray("reviewers", mcp.Description("reviewer usernames (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
|
||||||
mcp.WithArray("team_reviewers", mcp.Description("team reviewer names (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
|
mcp.WithArray("team_reviewers", mcp.Description("team reviewer names (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
|
||||||
mcp.WithBoolean("draft", mcp.Description("mark PR as draft (for 'create', 'update'). Gitea uses a 'WIP: ' title prefix for drafts.")),
|
mcp.WithBoolean("draft", mcp.Description("mark PR as draft (for 'create', 'update'). Gitea uses a 'WIP: ' title prefix for drafts.")),
|
||||||
@@ -134,11 +140,85 @@ func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
|||||||
return getPullRequestReviewFn(ctx, req)
|
return getPullRequestReviewFn(ctx, req)
|
||||||
case "get_review_comments":
|
case "get_review_comments":
|
||||||
return listPullRequestReviewCommentsFn(ctx, req)
|
return listPullRequestReviewCommentsFn(ctx, req)
|
||||||
|
case "list_files":
|
||||||
|
return listPullRequestFilesFn(ctx, req)
|
||||||
|
case "is_merged":
|
||||||
|
return isPullRequestMergedFn(ctx, req)
|
||||||
default:
|
default:
|
||||||
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
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) {
|
func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
method, err := params.GetString(req.GetArguments(), "method")
|
method, err := params.GetString(req.GetArguments(), "method")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -331,12 +411,17 @@ func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
pr, _, err := client.CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{
|
opt := gitea_sdk.CreatePullRequestOption{
|
||||||
Title: title,
|
Title: title,
|
||||||
Body: body,
|
Body: body,
|
||||||
Head: head,
|
Head: head,
|
||||||
Base: base,
|
Base: base,
|
||||||
})
|
}
|
||||||
|
if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil {
|
||||||
|
opt.Labels = labelIDs
|
||||||
|
}
|
||||||
|
opt.Deadline = params.GetOptionalTime(args, "deadline")
|
||||||
|
pr, _, err := client.CreatePullRequest(owner, repo, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err))
|
return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err))
|
||||||
}
|
}
|
||||||
@@ -751,11 +836,18 @@ func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceMerge, _ := args["force_merge"].(bool)
|
||||||
|
mergeWhenChecksSucceed, _ := args["merge_when_checks_succeed"].(bool)
|
||||||
|
headCommitID, _ := args["head_commit_id"].(string)
|
||||||
|
|
||||||
opt := gitea_sdk.MergePullRequestOption{
|
opt := gitea_sdk.MergePullRequestOption{
|
||||||
Style: gitea_sdk.MergeStyle(mergeStyle),
|
Style: gitea_sdk.MergeStyle(mergeStyle),
|
||||||
Title: title,
|
Title: title,
|
||||||
Message: message,
|
Message: message,
|
||||||
DeleteBranchAfterMerge: deleteBranch,
|
DeleteBranchAfterMerge: deleteBranch,
|
||||||
|
ForceMerge: forceMerge,
|
||||||
|
MergeWhenChecksSucceed: mergeWhenChecksSucceed,
|
||||||
|
HeadCommitId: headCommitID,
|
||||||
}
|
}
|
||||||
|
|
||||||
merged, resp, err := client.MergePullRequest(owner, repo, index, opt)
|
merged, resp, err := client.MergePullRequest(owner, repo, index, opt)
|
||||||
@@ -842,6 +934,13 @@ func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
|||||||
if allowMaintainerEdit, ok := args["allow_maintainer_edit"].(bool); ok {
|
if allowMaintainerEdit, ok := args["allow_maintainer_edit"].(bool); ok {
|
||||||
opt.AllowMaintainerEdit = new(allowMaintainerEdit)
|
opt.AllowMaintainerEdit = new(allowMaintainerEdit)
|
||||||
}
|
}
|
||||||
|
if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil {
|
||||||
|
opt.Labels = labelIDs
|
||||||
|
}
|
||||||
|
opt.Deadline = params.GetOptionalTime(args, "deadline")
|
||||||
|
if removeDeadline, ok := args["remove_deadline"].(bool); ok {
|
||||||
|
opt.RemoveDeadline = &removeDeadline
|
||||||
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -254,6 +254,168 @@ func Test_mergePullRequestFn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_mergePullRequestFn_newParams(t *testing.T) {
|
||||||
|
const (
|
||||||
|
owner = "octo"
|
||||||
|
repo = "demo"
|
||||||
|
index = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
gotBody map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/version":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"private":false}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index):
|
||||||
|
mu.Lock()
|
||||||
|
var body map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
gotBody = body
|
||||||
|
mu.Unlock()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
origHost := flag.Host
|
||||||
|
origToken := flag.Token
|
||||||
|
origVersion := flag.Version
|
||||||
|
flag.Host = server.URL
|
||||||
|
flag.Token = ""
|
||||||
|
flag.Version = "test"
|
||||||
|
defer func() {
|
||||||
|
flag.Host = origHost
|
||||||
|
flag.Token = origToken
|
||||||
|
flag.Version = origVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := mcp.CallToolRequest{
|
||||||
|
Params: mcp.CallToolParams{
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"owner": owner,
|
||||||
|
"repo": repo,
|
||||||
|
"index": float64(index),
|
||||||
|
"merge_style": "merge",
|
||||||
|
"force_merge": true,
|
||||||
|
"merge_when_checks_succeed": true,
|
||||||
|
"head_commit_id": "abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mergePullRequestFn(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergePullRequestFn() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if gotBody["force_merge"] != true {
|
||||||
|
t.Fatalf("expected force_merge true, got %v", gotBody["force_merge"])
|
||||||
|
}
|
||||||
|
if gotBody["merge_when_checks_succeed"] != true {
|
||||||
|
t.Fatalf("expected merge_when_checks_succeed true, got %v", gotBody["merge_when_checks_succeed"])
|
||||||
|
}
|
||||||
|
if gotBody["head_commit_id"] != "abc123" {
|
||||||
|
t.Fatalf("expected head_commit_id 'abc123', got %v", gotBody["head_commit_id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_createPullRequestFn_labels(t *testing.T) {
|
||||||
|
const (
|
||||||
|
owner = "octo"
|
||||||
|
repo = "demo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
gotBody map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/version":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"private":false}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner, repo):
|
||||||
|
mu.Lock()
|
||||||
|
var body map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
gotBody = body
|
||||||
|
mu.Unlock()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"number":1,"title":"test","state":"open"}`))
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
origHost := flag.Host
|
||||||
|
origToken := flag.Token
|
||||||
|
origVersion := flag.Version
|
||||||
|
flag.Host = server.URL
|
||||||
|
flag.Token = ""
|
||||||
|
flag.Version = "test"
|
||||||
|
defer func() {
|
||||||
|
flag.Host = origHost
|
||||||
|
flag.Token = origToken
|
||||||
|
flag.Version = origVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := mcp.CallToolRequest{
|
||||||
|
Params: mcp.CallToolParams{
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"owner": owner,
|
||||||
|
"repo": repo,
|
||||||
|
"title": "test",
|
||||||
|
"body": "body",
|
||||||
|
"head": "feature",
|
||||||
|
"base": "main",
|
||||||
|
"labels": []any{float64(1), float64(2)},
|
||||||
|
"deadline": "2026-06-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := createPullRequestFn(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createPullRequestFn() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
labels, ok := gotBody["labels"].([]any)
|
||||||
|
if !ok || len(labels) != 2 {
|
||||||
|
t.Fatalf("expected 2 labels, got %v", gotBody["labels"])
|
||||||
|
}
|
||||||
|
if labels[0] != float64(1) || labels[1] != float64(2) {
|
||||||
|
t.Fatalf("expected labels [1,2], got %v", labels)
|
||||||
|
}
|
||||||
|
if gotBody["due_date"] == nil {
|
||||||
|
t.Fatalf("expected due_date to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_applyDraftPrefix(t *testing.T) {
|
func Test_applyDraftPrefix(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ var (
|
|||||||
mcp.WithDescription("List branches"),
|
mcp.WithDescription("List branches"),
|
||||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
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 (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -131,10 +133,11 @@ func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(err)
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
page, pageSize := params.GetPagination(args, 30)
|
||||||
opt := gitea_sdk.ListRepoBranchesOptions{
|
opt := gitea_sdk.ListRepoBranchesOptions{
|
||||||
ListOptions: gitea_sdk.ListOptions{
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
Page: 1,
|
Page: page,
|
||||||
PageSize: 30,
|
PageSize: pageSize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -16,17 +16,28 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ListRepoCommitsToolName = "list_commits"
|
ListRepoCommitsToolName = "list_commits"
|
||||||
|
GetCommitToolName = "get_commit"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ListRepoCommitsTool = mcp.NewTool(
|
var (
|
||||||
ListRepoCommitsToolName,
|
ListRepoCommitsTool = mcp.NewTool(
|
||||||
mcp.WithDescription("List repository commits"),
|
ListRepoCommitsToolName,
|
||||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
mcp.WithDescription("List repository commits"),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("sha", mcp.Description("SHA or branch to start listing commits from")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithString("path", mcp.Description("path indicates that only commits that include the path's file/dir should be returned.")),
|
mcp.WithString("sha", mcp.Description("SHA or branch to start listing commits from")),
|
||||||
mcp.WithNumber("page", mcp.Required(), mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
mcp.WithString("path", mcp.Description("path indicates that only commits that include the path's file/dir should be returned.")),
|
||||||
mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)),
|
mcp.WithNumber("page", mcp.Required(), mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetCommitTool = mcp.NewTool(
|
||||||
|
GetCommitToolName,
|
||||||
|
mcp.WithDescription("Get details of a specific commit"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -34,6 +45,10 @@ func init() {
|
|||||||
Tool: ListRepoCommitsTool,
|
Tool: ListRepoCommitsTool,
|
||||||
Handler: ListRepoCommitsFn,
|
Handler: ListRepoCommitsFn,
|
||||||
})
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetCommitTool,
|
||||||
|
Handler: GetCommitFn,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
@@ -75,3 +90,29 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
|||||||
}
|
}
|
||||||
return to.TextResult(slimCommits(commits))
|
return to.TextResult(slimCommits(commits))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCommitFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetCommitFn")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
sha, err := params.GetString(args, "sha")
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
commit, _, err := client.GetSingleCommit(owner, repo, sha)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get commit %v err: %v", sha, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(slimCommit(commit))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ var (
|
|||||||
mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)),
|
mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)),
|
||||||
mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)),
|
mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(20), mcp.Min(1)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(20), mcp.Min(1)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+237
-11
@@ -23,6 +23,9 @@ const (
|
|||||||
ForkRepoToolName = "fork_repo"
|
ForkRepoToolName = "fork_repo"
|
||||||
ListMyReposToolName = "list_my_repos"
|
ListMyReposToolName = "list_my_repos"
|
||||||
ListOrgReposToolName = "list_org_repos"
|
ListOrgReposToolName = "list_org_repos"
|
||||||
|
GetRepoToolName = "get_repo"
|
||||||
|
RepoWriteToolName = "repo_write"
|
||||||
|
ListForksToolName = "list_forks"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -39,6 +42,8 @@ var (
|
|||||||
mcp.WithString("license", mcp.Description("License to use")),
|
mcp.WithString("license", mcp.Description("License to use")),
|
||||||
mcp.WithString("readme", mcp.Description("Readme of the repository to create")),
|
mcp.WithString("readme", mcp.Description("Readme of the repository to create")),
|
||||||
mcp.WithString("default_branch", mcp.Description("DefaultBranch of the repository (used when initializes and in template)")),
|
mcp.WithString("default_branch", mcp.Description("DefaultBranch of the repository (used when initializes and in template)")),
|
||||||
|
mcp.WithString("trust_model", mcp.Description("Trust model for verifying GPG signatures"), mcp.Enum("default", "collaborator", "committer", "collaboratorcommitter")),
|
||||||
|
mcp.WithString("object_format_name", mcp.Description("Object format: sha1 or sha256"), mcp.Enum("sha1", "sha256")),
|
||||||
mcp.WithString("organization", mcp.Description("Organization name to create repository in (optional - defaults to personal account)")),
|
mcp.WithString("organization", mcp.Description("Organization name to create repository in (optional - defaults to personal account)")),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,7 +60,7 @@ var (
|
|||||||
ListMyReposToolName,
|
ListMyReposToolName,
|
||||||
mcp.WithDescription("List my repositories"),
|
mcp.WithDescription("List my repositories"),
|
||||||
mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)),
|
mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30), mcp.Min(1)),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListOrgReposTool = mcp.NewTool(
|
ListOrgReposTool = mcp.NewTool(
|
||||||
@@ -65,6 +70,40 @@ var (
|
|||||||
mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
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)),
|
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() {
|
func init() {
|
||||||
@@ -84,6 +123,18 @@ func init() {
|
|||||||
Tool: ListOrgReposTool,
|
Tool: ListOrgReposTool,
|
||||||
Handler: ListOrgReposFn,
|
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) {
|
func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
@@ -102,19 +153,23 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
|
|||||||
license, _ := args["license"].(string)
|
license, _ := args["license"].(string)
|
||||||
readme, _ := args["readme"].(string)
|
readme, _ := args["readme"].(string)
|
||||||
defaultBranch, _ := args["default_branch"].(string)
|
defaultBranch, _ := args["default_branch"].(string)
|
||||||
|
trustModel, _ := args["trust_model"].(string)
|
||||||
|
objectFormatName, _ := args["object_format_name"].(string)
|
||||||
organization, _ := args["organization"].(string)
|
organization, _ := args["organization"].(string)
|
||||||
|
|
||||||
opt := gitea_sdk.CreateRepoOption{
|
opt := gitea_sdk.CreateRepoOption{
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: description,
|
Description: description,
|
||||||
Private: private,
|
Private: private,
|
||||||
IssueLabels: issueLabels,
|
IssueLabels: issueLabels,
|
||||||
AutoInit: autoInit,
|
AutoInit: autoInit,
|
||||||
Template: template,
|
Template: template,
|
||||||
Gitignores: gitignores,
|
Gitignores: gitignores,
|
||||||
License: license,
|
License: license,
|
||||||
Readme: readme,
|
Readme: readme,
|
||||||
DefaultBranch: defaultBranch,
|
DefaultBranch: defaultBranch,
|
||||||
|
TrustModel: gitea_sdk.TrustModel(trustModel),
|
||||||
|
ObjectFormatName: objectFormatName,
|
||||||
}
|
}
|
||||||
|
|
||||||
var repo *gitea_sdk.Repository
|
var repo *gitea_sdk.Repository
|
||||||
@@ -223,3 +278,174 @@ func ListOrgReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
|
|||||||
}
|
}
|
||||||
return to.TextResult(repos)
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,6 +184,168 @@ func slimContents(c *gitea_sdk.ContentsResponse) map[string]any {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func slimTree(t *gitea_sdk.GitTreeResponse) map[string]any {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries := make([]map[string]any, 0, len(t.Entries))
|
||||||
|
for _, e := range t.Entries {
|
||||||
|
entries = append(entries, map[string]any{
|
||||||
|
"path": e.Path,
|
||||||
|
"mode": e.Mode,
|
||||||
|
"type": e.Type,
|
||||||
|
"size": e.Size,
|
||||||
|
"sha": e.SHA,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"sha": t.SHA,
|
||||||
|
"truncated": t.Truncated,
|
||||||
|
"total_count": t.TotalCount,
|
||||||
|
"tree": entries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
func slimDirEntries(entries []*gitea_sdk.ContentsResponse) []map[string]any {
|
||||||
out := make([]map[string]any, 0, len(entries))
|
out := make([]map[string]any, 0, len(entries))
|
||||||
for _, c := range entries {
|
for _, c := range entries {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ var (
|
|||||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(20), mcp.Min(1)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(20), mcp.Min(1)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,74 @@
|
|||||||
|
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 (
|
||||||
|
GetRepoTreeToolName = "get_repository_tree"
|
||||||
|
)
|
||||||
|
|
||||||
|
var GetRepoTreeTool = mcp.NewTool(
|
||||||
|
GetRepoTreeToolName,
|
||||||
|
mcp.WithDescription("Get the file tree of a repository"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("tree_sha", mcp.Required(), mcp.Description("SHA, branch name, or tag name")),
|
||||||
|
mcp.WithBoolean("recursive", mcp.Description("whether to get the tree recursively")),
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetRepoTreeTool,
|
||||||
|
Handler: GetRepoTreeFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRepoTreeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetRepoTreeFn")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
treeSHA, err := params.GetString(args, "tree_sha")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
recursive, _ := args["recursive"].(bool)
|
||||||
|
page, pageSize := params.GetPagination(args, 30)
|
||||||
|
|
||||||
|
opt := gitea_sdk.ListTreeOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
},
|
||||||
|
Ref: treeSHA,
|
||||||
|
Recursive: recursive,
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
tree, _, err := client.GetTrees(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get repository tree err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(slimTree(tree))
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSlimTree(t *testing.T) {
|
||||||
|
tree := &gitea_sdk.GitTreeResponse{
|
||||||
|
SHA: "abc123",
|
||||||
|
TotalCount: 2,
|
||||||
|
Truncated: false,
|
||||||
|
Entries: []gitea_sdk.GitEntry{
|
||||||
|
{Path: "src", Mode: "040000", Type: "tree", Size: 0, SHA: "def456"},
|
||||||
|
{Path: "main.go", Mode: "100644", Type: "blob", Size: 42, SHA: "789abc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := slimTree(tree)
|
||||||
|
if m["sha"] != "abc123" {
|
||||||
|
t.Errorf("expected sha abc123, got %v", m["sha"])
|
||||||
|
}
|
||||||
|
if m["total_count"] != 2 {
|
||||||
|
t.Errorf("expected total_count 2, got %v", m["total_count"])
|
||||||
|
}
|
||||||
|
entries := m["tree"].([]map[string]any)
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("expected 2 entries, got %d", len(entries))
|
||||||
|
}
|
||||||
|
if entries[0]["path"] != "src" {
|
||||||
|
t.Errorf("expected first entry path src, got %v", entries[0]["path"])
|
||||||
|
}
|
||||||
|
if entries[1]["type"] != "blob" {
|
||||||
|
t.Errorf("expected second entry type blob, got %v", entries[1]["type"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlimTreeNil(t *testing.T) {
|
||||||
|
if m := slimTree(nil); m != nil {
|
||||||
|
t.Errorf("expected nil, got %v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRepoTreeToolRequired(t *testing.T) {
|
||||||
|
for _, field := range []string{"owner", "repo", "tree_sha"} {
|
||||||
|
if !slices.Contains(GetRepoTreeTool.InputSchema.Required, field) {
|
||||||
|
t.Errorf("expected %q to be required", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,6 +3,7 @@ package search
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
@@ -21,6 +22,7 @@ const (
|
|||||||
SearchUsersToolName = "search_users"
|
SearchUsersToolName = "search_users"
|
||||||
SearchOrgTeamsToolName = "search_org_teams"
|
SearchOrgTeamsToolName = "search_org_teams"
|
||||||
SearchReposToolName = "search_repos"
|
SearchReposToolName = "search_repos"
|
||||||
|
SearchIssuesToolName = "search_issues"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -29,7 +31,7 @@ var (
|
|||||||
mcp.WithDescription("search users"),
|
mcp.WithDescription("search users"),
|
||||||
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
|
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
|
||||||
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
SearOrgTeamsTool = mcp.NewTool(
|
SearOrgTeamsTool = mcp.NewTool(
|
||||||
@@ -39,7 +41,7 @@ var (
|
|||||||
mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")),
|
mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")),
|
||||||
mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
|
mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
|
||||||
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
SearchReposTool = mcp.NewTool(
|
SearchReposTool = mcp.NewTool(
|
||||||
@@ -54,7 +56,19 @@ var (
|
|||||||
mcp.WithString("sort", mcp.Description("Sort")),
|
mcp.WithString("sort", mcp.Description("Sort")),
|
||||||
mcp.WithString("order", mcp.Description("Order")),
|
mcp.WithString("order", mcp.Description("Order")),
|
||||||
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
|
)
|
||||||
|
|
||||||
|
SearchIssuesTool = mcp.NewTool(
|
||||||
|
SearchIssuesToolName,
|
||||||
|
mcp.WithDescription("Search for issues and pull requests across all accessible repositories"),
|
||||||
|
mcp.WithString("query", mcp.Required(), mcp.Description("search keyword")),
|
||||||
|
mcp.WithString("state", mcp.Description("filter by state: open, closed, all"), mcp.Enum("open", "closed", "all")),
|
||||||
|
mcp.WithString("type", mcp.Description("filter by type: issues, pulls"), mcp.Enum("issues", "pulls")),
|
||||||
|
mcp.WithString("labels", mcp.Description("comma-separated list of label names")),
|
||||||
|
mcp.WithString("owner", mcp.Description("filter by repository owner")),
|
||||||
|
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)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,6 +85,10 @@ func init() {
|
|||||||
Tool: SearchReposTool,
|
Tool: SearchReposTool,
|
||||||
Handler: ReposFn,
|
Handler: ReposFn,
|
||||||
})
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: SearchIssuesTool,
|
||||||
|
Handler: IssuesFn,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
@@ -175,3 +193,42 @@ func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult,
|
|||||||
}
|
}
|
||||||
return to.TextResult(slimRepos(repos))
|
return to.TextResult(slimRepos(repos))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called IssuesFn")
|
||||||
|
args := req.GetArguments()
|
||||||
|
query, err := params.GetString(args, "query")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
page, pageSize := params.GetPagination(args, 30)
|
||||||
|
|
||||||
|
opt := gitea_sdk.ListIssueOption{
|
||||||
|
KeyWord: query,
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if state, ok := args["state"].(string); ok {
|
||||||
|
opt.State = gitea_sdk.StateType(state)
|
||||||
|
}
|
||||||
|
if issueType, ok := args["type"].(string); ok {
|
||||||
|
opt.Type = gitea_sdk.IssueType(issueType)
|
||||||
|
}
|
||||||
|
if labels, ok := args["labels"].(string); ok && labels != "" {
|
||||||
|
opt.Labels = strings.Split(labels, ",")
|
||||||
|
}
|
||||||
|
if owner, ok := args["owner"].(string); ok {
|
||||||
|
opt.Owner = owner
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
issues, _, err := client.ListIssues(opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("search issues err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(slimIssues(issues))
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,3 +86,59 @@ func slimRepos(repos []*gitea_sdk.Repository) []map[string]any {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userLogin(u *gitea_sdk.User) string {
|
||||||
|
if u == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return u.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelNames(labels []*gitea_sdk.Label) []string {
|
||||||
|
if len(labels) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(labels))
|
||||||
|
for _, l := range labels {
|
||||||
|
if l != nil {
|
||||||
|
out = append(out, l.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func slimIssues(issues []*gitea_sdk.Issue) []map[string]any {
|
||||||
|
out := make([]map[string]any, 0, len(issues))
|
||||||
|
for _, i := range issues {
|
||||||
|
if i == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m := map[string]any{
|
||||||
|
"number": i.Index,
|
||||||
|
"title": i.Title,
|
||||||
|
"state": i.State,
|
||||||
|
"html_url": i.HTMLURL,
|
||||||
|
"user": userLogin(i.Poster),
|
||||||
|
"comments": i.Comments,
|
||||||
|
"created_at": i.Created,
|
||||||
|
"updated_at": i.Updated,
|
||||||
|
}
|
||||||
|
if len(i.Labels) > 0 {
|
||||||
|
m["labels"] = labelNames(i.Labels)
|
||||||
|
}
|
||||||
|
if i.Repository != nil {
|
||||||
|
m["repository"] = i.Repository.FullName
|
||||||
|
}
|
||||||
|
if i.Ref != "" {
|
||||||
|
m["ref"] = i.Ref
|
||||||
|
}
|
||||||
|
if i.Deadline != nil {
|
||||||
|
m["deadline"] = i.Deadline
|
||||||
|
}
|
||||||
|
if i.PullRequest != nil {
|
||||||
|
m["is_pull"] = true
|
||||||
|
}
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSlimIssues(t *testing.T) {
|
||||||
|
issues := []*gitea_sdk.Issue{
|
||||||
|
{
|
||||||
|
Index: 1,
|
||||||
|
Title: "Bug report",
|
||||||
|
State: gitea_sdk.StateOpen,
|
||||||
|
HTMLURL: "https://gitea.com/org/repo/issues/1",
|
||||||
|
Poster: &gitea_sdk.User{UserName: "alice"},
|
||||||
|
Labels: []*gitea_sdk.Label{{Name: "bug"}},
|
||||||
|
Repository: &gitea_sdk.RepositoryMeta{FullName: "org/repo"},
|
||||||
|
PullRequest: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Index: 2,
|
||||||
|
Title: "Add feature",
|
||||||
|
State: gitea_sdk.StateOpen,
|
||||||
|
Poster: &gitea_sdk.User{UserName: "bob"},
|
||||||
|
Repository: &gitea_sdk.RepositoryMeta{FullName: "org/repo"},
|
||||||
|
PullRequest: &gitea_sdk.PullRequestMeta{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := slimIssues(issues)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Fatalf("expected 2 issues, got %d", len(result))
|
||||||
|
}
|
||||||
|
if result[0]["repository"] != "org/repo" {
|
||||||
|
t.Errorf("expected repository org/repo, got %v", result[0]["repository"])
|
||||||
|
}
|
||||||
|
if result[0]["labels"].([]string)[0] != "bug" {
|
||||||
|
t.Errorf("expected label bug, got %v", result[0]["labels"])
|
||||||
|
}
|
||||||
|
if _, ok := result[0]["is_pull"]; ok {
|
||||||
|
t.Error("issue should not have is_pull")
|
||||||
|
}
|
||||||
|
if result[1]["is_pull"] != true {
|
||||||
|
t.Error("PR should have is_pull=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchIssuesToolRequired(t *testing.T) {
|
||||||
|
if !slices.Contains(SearchIssuesTool.InputSchema.Required, "query") {
|
||||||
|
t.Error("search_issues should require query")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ var (
|
|||||||
mcp.WithString("repo", mcp.Description("repository name (required for 'list_issue_times', 'list_repo_times')")),
|
mcp.WithString("repo", mcp.Description("repository name (required for 'list_issue_times', 'list_repo_times')")),
|
||||||
mcp.WithNumber("index", mcp.Description("issue index (required for 'list_issue_times')")),
|
mcp.WithNumber("index", mcp.Description("issue index (required for 'list_issue_times')")),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
TimetrackingWriteTool = mcp.NewTool(
|
TimetrackingWriteTool = mcp.NewTool(
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ var (
|
|||||||
GetUserOrgsToolName,
|
GetUserOrgsToolName,
|
||||||
mcp.WithDescription("Get organizations associated with the authenticated user"),
|
mcp.WithDescription("Get organizations associated with the authenticated user"),
|
||||||
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)),
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)),
|
||||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(defaultPageSize)),
|
mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(defaultPageSize)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ package context
|
|||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TokenContextKey = contextKey("token")
|
TokenContextKey = contextKey("token")
|
||||||
|
HostContextKey = contextKey("host")
|
||||||
|
InsecureContextKey = contextKey("insecure")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package flag
|
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 (
|
var (
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
@@ -10,4 +17,8 @@ var (
|
|||||||
Insecure bool
|
Insecure bool
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
Debug 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"
|
"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{
|
httpClient := &http.Client{
|
||||||
Transport: http.DefaultTransport,
|
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||||
CheckRedirect: checkRedirect,
|
CheckRedirect: checkRedirect,
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []gitea.ClientOption{
|
opts := []gitea.ClientOption{
|
||||||
gitea.SetToken(token),
|
gitea.SetToken(token),
|
||||||
}
|
}
|
||||||
if flag.Insecure {
|
if insecure {
|
||||||
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
@@ -30,16 +30,20 @@ func NewClient(token string) (*gitea.Client, error) {
|
|||||||
if flag.Debug {
|
if flag.Debug {
|
||||||
opts = append(opts, gitea.SetDebugMode())
|
opts = append(opts, gitea.SetDebugMode())
|
||||||
}
|
}
|
||||||
client, err := gitea.NewClient(flag.Host, opts...)
|
client, err := gitea.NewClient(host, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create gitea client err: %w", err)
|
return nil, fmt.Errorf("create gitea client err: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set user agent for the client
|
|
||||||
client.SetUserAgent("gitea-mcp-server/" + flag.Version)
|
client.SetUserAgent("gitea-mcp-server/" + flag.Version)
|
||||||
return client, nil
|
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.)
|
// 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
|
// to GET when following 301/302/303 redirects, which would drop the request body and
|
||||||
// make writes appear to succeed when they didn't.
|
// make writes appear to succeed when they didn't.
|
||||||
@@ -53,10 +57,21 @@ func checkRedirect(_ *http.Request, via []*http.Request) error {
|
|||||||
return nil
|
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) {
|
func ClientFromContext(ctx context.Context) (*gitea.Client, error) {
|
||||||
token, ok := ctx.Value(mcpContext.TokenContextKey).(string)
|
host := flag.Host
|
||||||
if !ok {
|
token := flag.Token
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package params
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetString extracts a required string parameter from MCP tool arguments.
|
// GetString extracts a required string parameter from MCP tool arguments.
|
||||||
@@ -101,6 +102,18 @@ func GetInt64Slice(args map[string]any, key string) ([]int64, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOptionalTime extracts an optional RFC3339 timestamp parameter, returning nil if missing or unparseable.
|
||||||
|
func GetOptionalTime(args map[string]any, key string) *time.Time {
|
||||||
|
val, ok := args[key].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t, err := time.Parse(time.RFC3339, val); err == nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetOptionalInt extracts an optional integer parameter from MCP tool arguments.
|
// GetOptionalInt extracts an optional integer parameter from MCP tool arguments.
|
||||||
// Returns defaultVal if the key is missing or the value cannot be parsed.
|
// Returns defaultVal if the key is missing or the value cannot be parsed.
|
||||||
// Accepts both float64 (JSON number) and string representations.
|
// Accepts both float64 (JSON number) and string representations.
|
||||||
|
|||||||
Reference in New Issue
Block a user